Files
codeocean/app/models/runner.rb
Felix Auringer d5b274c9f2 Introduce new error types for runners
The errors are raised in the runner model and in the runner connection
class. In the submission controller the errors are rescued and,
depending on the error, the status timeout / container depleted is
sent to the client.
2021-11-01 17:12:48 +01:00

139 lines
4.5 KiB
Ruby

# frozen_string_literal: true
class Runner < ApplicationRecord
BASE_URL = CodeOcean::Config.new(:code_ocean).read[:runner_management][:url]
HEADERS = {'Content-Type' => 'application/json'}.freeze
UNUSED_EXPIRATION_TIME = CodeOcean::Config.new(:code_ocean).read[:runner_management][:unused_runner_expiration_time].seconds
ERRORS = %w[NOMAD_UNREACHABLE NOMAD_OVERLOAD NOMAD_INTERNAL_SERVER_ERROR UNKNOWN].freeze
ERRORS.each do |error|
define_singleton_method :"error_#{error.downcase}" do
error
end
end
belongs_to :execution_environment
belongs_to :user, polymorphic: true
before_validation :request_remotely
validates :execution_environment, :user, :runner_id, presence: true
def self.for(user, exercise)
execution_environment = ExecutionEnvironment.find(exercise.execution_environment_id)
runner = find_or_create_by(user: user, execution_environment: execution_environment)
unless runner.persisted?
# runner was not saved in the database (was not valid)
raise Runner::Error::InternalServerError.new("Provided runner could not be saved: #{runner.errors.inspect}")
end
runner
end
def copy_files(files)
url = "#{runner_url}/files"
body = {copy: files.map {|filename, content| {path: filename, content: Base64.strict_encode64(content)} }}
response = Faraday.patch(url, body.to_json, HEADERS)
handle_error response unless response.status == 204
end
def execute_command(command)
url = "#{runner_url}/execute"
body = {command: command, timeLimit: execution_environment.permitted_execution_time}
response = Faraday.post(url, body.to_json, HEADERS)
if response.status == 200
response_body = parse response
websocket_url = response_body[:websocketUrl]
if websocket_url.present?
return websocket_url
else
raise Runner::Error::Unknown.new('Runner management sent unexpected response')
end
end
handle_error response
end
def execute_interactively(command)
starting_time = Time.zone.now
websocket_url = execute_command(command)
EventMachine.run do
socket = Runner::Connection.new(websocket_url)
yield(self, socket) if block_given?
end
Time.zone.now - starting_time # execution time
end
# This method is currently not used.
# This does *not* destroy the ActiveRecord model.
def destroy_remotely
response = Faraday.delete runner_url
return if response.status == 204
if response.status == 404
raise Runner::Error::NotFound.new('Runner not found')
else
handle_error response
end
end
private
def request_remotely
return if runner_id.present?
url = "#{BASE_URL}/runners"
body = {executionEnvironmentId: execution_environment.id, inactivityTimeout: UNUSED_EXPIRATION_TIME}
response = Faraday.post(url, body.to_json, HEADERS)
case response.status
when 200
response_body = parse response
runner_id = response_body[:runnerId]
throw(:abort) if runner_id.blank?
self.runner_id = response_body[:runnerId]
when 404
raise Runner::Error::NotFound.new('Execution environment not found')
else
handle_error response
end
end
def handle_error(response)
case response.status
when 400
response_body = parse response
raise Runner::Error::BadRequest.new(response_body[:message])
when 401
raise Runner::Error::Unauthorized.new('Authentication with runner management failed')
when 404
# The runner does not exist in the runner management (e.g. due to an inactivity timeout).
# Delete the runner model in this case as it can not be used anymore.
destroy
raise Runner::Error::NotFound.new('Runner not found')
when 500
response_body = parse response
error_code = response_body[:errorCode]
if error_code == Runner.error_nomad_overload
raise Runner::Error::NotAvailable.new("No runner available (#{error_code}): #{response_body[:message]}")
else
raise Runner::Error::InternalServerError.new("#{response_body[:errorCode]}: #{response_body[:message]}")
end
else
raise Runner::Error::Unknown.new('Runner management sent unexpected response')
end
end
def runner_url
"#{BASE_URL}/runners/#{runner_id}"
end
def parse(response)
JSON.parse(response.body).deep_symbolize_keys
rescue JSON::ParserError => e
# the runner management should not send invalid json
raise Runner::Error::Unknown.new("Error parsing response from runner management: #{e.message}")
end
end