Merge pull request #25 from openHPI/webpython-hybrid

Fixed container destruction
This commit is contained in:
rteusner
2015-10-19 15:55:53 +02:00
7 changed files with 63 additions and 21 deletions

View File

@ -25,8 +25,9 @@ In order to execute code submissions using Docker, source code files are written
- create *config/sendmail.yml* - create *config/sendmail.yml*
- create *config/smtp.yml* - create *config/smtp.yml*
- use boot2docker if there is no native support for docker on your OS - use boot2docker or vagrant if there is no native support for docker on your OS
- create seed data by executing `rake db:seed` - create seed data by executing `rake db:seed`
- if you already created a configuration for your local installation and want to use vagrant, too, be sure to log into the vagrant instance via ssh and add your database user manually to the database. Afterwards, create, migrate and seed.
## Production Setup ## Production Setup
@ -46,8 +47,8 @@ The application is compatible with MRI and JRuby. Due to superior parallelism, w
1.1 1.1
[ ] WebSocket Suppport [x] WebSocket Suppport
[ ] Interactive Exercises [x] Interactive Exercises
[ ] Allow Disabling of File Creation [ ] Allow Disabling of File Creation
[ ] Set Container Recyling per Environment [ ] Set Container Recyling per Environment

View File

@ -13,6 +13,9 @@ $(function() {
var THEME = 'ace/theme/textmate'; var THEME = 'ace/theme/textmate';
var REMEMBER_TAB = false; var REMEMBER_TAB = false;
var AUTOSAVE_INTERVAL = 15 * 1000; var AUTOSAVE_INTERVAL = 15 * 1000;
var NONE = 0;
var WEBSOCKET = 1;
var SERVER_SEND_EVENT = 2;
var editors = []; var editors = [];
var active_file = undefined; var active_file = undefined;
@ -20,6 +23,7 @@ $(function() {
var running = false; var running = false;
var qa_api = undefined; var qa_api = undefined;
var output_mode_is_streaming = true; var output_mode_is_streaming = true;
var runmode = NONE;
var websocket, var websocket,
turtlescreen, turtlescreen,
@ -873,6 +877,7 @@ $(function() {
var runCode = function(event) { var runCode = function(event) {
event.preventDefault(); event.preventDefault();
if ($('#run').is(':visible')) { if ($('#run').is(':visible')) {
runmode = WEBSOCKET;
createSubmission(this, null, function(response) { createSubmission(this, null, function(response) {
$('#stop').data('url', response.stop_url); $('#stop').data('url', response.stop_url);
running = true; running = true;
@ -910,6 +915,7 @@ $(function() {
var scoreCode = function(event) { var scoreCode = function(event) {
event.preventDefault(); event.preventDefault();
runmode = SERVER_SEND_EVENT;
createSubmission(this, null, function(response) { createSubmission(this, null, function(response) {
showSpinner($('#assess')); showSpinner($('#assess'));
var url = response.score_url; var url = response.score_url;
@ -1018,8 +1024,28 @@ $(function() {
var stopCode = function(event) { var stopCode = function(event) {
event.preventDefault(); event.preventDefault();
if ($('#stop').is(':visible')) { if ($('#stop').is(':visible')) {
if(runmode == WEBSOCKET){
killWebsocketAndContainer(); killWebsocketAndContainer();
} else if (runmode == SERVER_SEND_EVENT) {
stopCodeServerSendEvent(event);
} }
runmode = NONE;
}
};
var stopCodeServerSendEvent = function(event){
var jqxhr = ajax({
data: {
container_id: $('#stop').data('container').id
},
url: $('#stop').data('url')
});
jqxhr.always(function() {
hideSpinner();
running = false;
toggleButtonStates();
});
jqxhr.fail(ajaxError);
}; };
var killWebsocketAndContainer = function() { var killWebsocketAndContainer = function() {

View File

@ -112,8 +112,8 @@ class SubmissionsController < ApplicationController
begin begin
parsed = JSON.parse(data) parsed = JSON.parse(data)
if parsed['cmd'] == 'exit' if parsed['cmd'] == 'exit'
Rails.logger.info("Client killed container.") Rails.logger.info("Client exited container.")
@docker_client.kill_container(result[:container]) @docker_client.exit_container(result[:container])
else else
socket.send data socket.send data
end end

View File

@ -39,10 +39,10 @@
#output-col1 #output-col1
// todo set to full width if turtle isnt used // todo set to full width if turtle isnt used
#prompt.input-group.hidden #prompt.input-group.hidden
span.input-group-addon = 'Your input' span.input-group-addon = t('exercises.editor.input')
input#prompt-input.form-control type='text' input#prompt-input.form-control type='text'
span.input-group-btn span.input-group-btn
button#prompt-submit.btn.btn-primary type="button" = 'Send' button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
#output #output
pre = t('.no_output_yet') pre = t('.no_output_yet')
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]

View File

@ -176,6 +176,7 @@ de:
destroy_file: Datei löschen destroy_file: Datei löschen
download: Herunterladen download: Herunterladen
dummy: Keine Aktion dummy: Keine Aktion
input: Ihre Eingabe
lastsaved: 'Zuletzt gespeichert: ' lastsaved: 'Zuletzt gespeichert: '
network: 'Während Ihr Code läuft, ist Port %{port} unter folgender Adresse erreichbar: <a href="%{address}" target="_blank">%{address}</a>.' network: 'Während Ihr Code läuft, ist Port %{port} unter folgender Adresse erreichbar: <a href="%{address}" target="_blank">%{address}</a>.'
render: Anzeigen render: Anzeigen
@ -185,6 +186,7 @@ de:
requestComments: Kommentare erbitten requestComments: Kommentare erbitten
save: Speichern save: Speichern
score: Bewerten score: Bewerten
send: Senden
start_over: Von vorne anfangen start_over: Von vorne anfangen
stop: Stoppen stop: Stoppen
submit: Code zur Bewertung abgeben submit: Code zur Bewertung abgeben

View File

@ -176,6 +176,7 @@ en:
destroy_file: Delete File destroy_file: Delete File
download: Download download: Download
dummy: No Action dummy: No Action
input: Your input
lastsaved: 'Last saved: ' lastsaved: 'Last saved: '
network: 'While your code is running, port %{port} is accessible using the following address: <a href="%{address}" target="_blank">%{address}</a>.' network: 'While your code is running, port %{port} is accessible using the following address: <a href="%{address}" target="_blank">%{address}</a>.'
render: Render render: Render
@ -185,6 +186,7 @@ en:
requestComments: Request comments requestComments: Request comments
save: Save save: Save
score: Score score: Score
send: Send
start_over: Start over start_over: Start over
stop: Stop stop: Stop
submit: Submit Code For Assessment submit: Submit Code For Assessment

View File

@ -93,6 +93,7 @@ class DockerClient
FileUtils.mkdir(local_workspace_path) FileUtils.mkdir(local_workspace_path)
container.start(container_start_options(execution_environment, local_workspace_path)) container.start(container_start_options(execution_environment, local_workspace_path))
container.start_time = Time.now container.start_time = Time.now
container.status = :created
container container
rescue Docker::Error::NotFoundError => error rescue Docker::Error::NotFoundError => error
destroy_container(container) destroy_container(container)
@ -125,8 +126,10 @@ class DockerClient
container.stop.kill container.stop.kill
container.port_bindings.values.each { |port| PortPool.release(port) } container.port_bindings.values.each { |port| PortPool.release(port) }
clean_container_workspace(container) clean_container_workspace(container)
if(container)
container.delete(force: true, v: true) container.delete(force: true, v: true)
end end
end
def execute_arbitrary_command(command, &block) def execute_arbitrary_command(command, &block)
execute_command(command, nil, block) execute_command(command, nil, block)
@ -135,6 +138,7 @@ class DockerClient
def execute_command(command, before_execution_block, output_consuming_block) def execute_command(command, before_execution_block, output_consuming_block)
#tries ||= 0 #tries ||= 0
@container = DockerContainerPool.get_container(@execution_environment) @container = DockerContainerPool.get_container(@execution_environment)
@container.status = :executing
if @container if @container
before_execution_block.try(:call) before_execution_block.try(:call)
send_command(command, @container, &output_consuming_block) send_command(command, @container, &output_consuming_block)
@ -149,6 +153,7 @@ class DockerClient
def execute_websocket_command(command, before_execution_block, output_consuming_block) def execute_websocket_command(command, before_execution_block, output_consuming_block)
@container = DockerContainerPool.get_container(@execution_environment) @container = DockerContainerPool.get_container(@execution_environment)
@container.status = :executing
if @container if @container
before_execution_block.try(:call) before_execution_block.try(:call)
# todo catch exception if socket could not be created # todo catch exception if socket could not be created
@ -169,21 +174,24 @@ class DockerClient
Thread.new do Thread.new do
timeout = @execution_environment.permitted_execution_time.to_i # seconds timeout = @execution_environment.permitted_execution_time.to_i # seconds
sleep(timeout) sleep(timeout)
if container.status != :returned
Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.") Rails.logger.info("Killing container after timeout of " + timeout.to_s + " seconds.")
kill_container(container) kill_container(container)
end end
end end
end
def exit_container(container)
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
end
def kill_container(container) def kill_container(container)
"""
Please note that we cannot properly recycle containers when using
websockets because it is impossible to determine whether a program
is still running.
"""
# remove container from pool, then destroy it # remove container from pool, then destroy it
(DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : if (DockerContainerPool.config[:active])
DockerContainerPool.remove_from_all_containers(container, @execution_environment)
end
#destroy container
self.class.destroy_container(container) self.class.destroy_container(container)
# if we recylce containers, we start a fresh one # if we recylce containers, we start a fresh one
@ -267,6 +275,7 @@ class DockerClient
def self.return_container(container, execution_environment) def self.return_container(container, execution_environment)
clean_container_workspace(container) clean_container_workspace(container)
DockerContainerPool.return_container(container, execution_environment) DockerContainerPool.return_container(container, execution_environment)
container.status = :returned
end end
#private :return_container #private :return_container
@ -285,7 +294,9 @@ class DockerClient
Rails.logger.info('got timeout error for container ' + container.to_s) Rails.logger.info('got timeout error for container ' + container.to_s)
# remove container from pool, then destroy it # remove container from pool, then destroy it
(DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) : if (DockerContainerPool.config[:active])
DockerContainerPool.remove_from_all_containers(container, @execution_environment)
end
# destroy container # destroy container
self.class.destroy_container(container) self.class.destroy_container(container)