Refactor code execution to use async functions

This refactoring is required for Sentry tracing. It ensures that the respective functions only return as soon as a code execution finished. With this approach, we can then instrument the duration of the functions, so that Sentry spans are created as desired.

Co-authored-by: Jan Graichen <jgraichen@altimos.de>
This commit is contained in:
Sebastian Serth
2024-05-24 12:53:11 +02:00
committed by Sebastian Serth
parent c8609e5392
commit 86c67f3c9a
6 changed files with 205 additions and 158 deletions

View File

@ -28,13 +28,13 @@ $(document).on('turbolinks:load', function() {
function submitCode(event) { function submitCode(event) {
const button = $(event.target) || $('#submit'); const button = $(event.target) || $('#submit');
this.startSentryTransaction(button); this.startSentryTransaction(button);
this.createSubmission(button, null, function (response) { const submission = await this.createSubmission(button, null).catch(this.ajaxError.bind(this));
if (response.redirect) { if (!submission) return;
if (!submission.redirect) return;
this.autosaveIfChanged(); this.autosaveIfChanged();
this.stopCode(event); this.stopCode(event);
this.editors = []; this.editors = [];
Turbolinks.clearCache(); Turbolinks.clearCache();
Turbolinks.visit(response.redirect); Turbolinks.visit(submission.redirect);
}
})
} }

View File

@ -894,12 +894,14 @@ var CodeOceanEditor = {
Sentry.captureException(JSON.stringify(error, ["message", "arguments", "type", "name", "data"])); Sentry.captureException(JSON.stringify(error, ["message", "arguments", "type", "name", "data"]));
}, },
showFileDialog: function (event) { showFileDialog: async function (event) {
event.preventDefault(); event.preventDefault();
this.createSubmission('#create-file', null, function (response) {
$('#code_ocean_file_context_id').val(response.id); const submission = await this.createSubmission('#create-file', null).catch(this.ajaxError.bind(this));
if (!submission) return;
$('#code_ocean_file_context_id').val(submission.id);
new bootstrap.Modal($('#modal-file')).show(); new bootstrap.Modal($('#modal-file')).show();
}.bind(this));
}, },
initializeOutputBarToggle: function () { initializeOutputBarToggle: function () {

View File

@ -8,17 +8,19 @@ CodeOceanEditorEvaluation = {
* Scoring-Functions * Scoring-Functions
*/ */
scoreCode: function (event) { scoreCode: function (event) {
event.preventDefault();
const cause = $('#assess'); const cause = $('#assess');
this.startSentryTransaction(cause); this.startSentryTransaction(cause);
event.preventDefault();
this.stopCode(event); this.stopCode(event);
this.clearScoringOutput(); this.clearScoringOutput();
$('#submit').addClass("d-none"); $('#submit').addClass("d-none");
this.createSubmission(cause, null, function (submission) {
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
this.showSpinner($('#assess')); this.showSpinner($('#assess'));
$('#score_div').removeClass('d-none'); $('#score_div').removeClass('d-none');
this.initializeSocketForScoring(submission.id); await this.socketScoreCode(submission.id);
}.bind(this));
}, },
handleScoringResponse: function (results) { handleScoringResponse: function (results) {

View File

@ -3,7 +3,7 @@ CodeOceanEditorWebsocket = {
// Replace `http` with `ws` for the WebSocket connection. This also works with `https` and `wss`. // Replace `http` with `ws` for the WebSocket connection. This also works with `https` and `wss`.
webSocketProtocol: window.location.protocol.replace(/^http/, 'ws').split(':')[0], webSocketProtocol: window.location.protocol.replace(/^http/, 'ws').split(':')[0],
initializeSocket: function(urlHelper, params, closeCallback) { runSocket: function(urlHelper, params, setupFunction) {
// 1. Specify the protocol for all URLs to generate // 1. Specify the protocol for all URLs to generate
params.protocol = this.webSocketProtocol; params.protocol = this.webSocketProtocol;
params._options = true; params._options = true;
@ -36,44 +36,69 @@ CodeOceanEditorWebsocket = {
this.resetOutputTab(); this.resetOutputTab();
}.bind(this) }.bind(this)
); );
// Attach custom handlers for messages received.
setupFunction(this.websocket);
CodeOceanEditorWebsocket.websocket = this.websocket; CodeOceanEditorWebsocket.websocket = this.websocket;
// Create and return a new Promise. It will only resolve (or fail) once the connection has ended.
return new Promise((resolve, reject) => {
this.websocket.onError(this.showWebsocketError.bind(this)); this.websocket.onError(this.showWebsocketError.bind(this));
this.websocket.onClose(function(span, callback){
// Remove event listeners for Promise handling.
// This is especially useful in case of an error, where a `close` event might follow the `error` event.
const teardown = () => {
this.websocket.websocket.removeEventListener(closeListener);
this.websocket.websocket.removeEventListener(errorListener);
};
// We are using event listeners (and not `onError` or `onClose`) here, since these listeners should never be overwritten.
// With `onError` or `onClose`, a new assignment would overwrite a previous one.
const closeListener = this.websocket.websocket.addEventListener('close', () => {
span?.finish(); span?.finish();
if(callback != null){ resolve();
callback(); teardown();
} });
}.bind(this, span, closeCallback)); const errorListener = this.websocket.websocket.addEventListener('error', (error) => {
reject(error);
teardown();
this.websocket.killWebSocket(); // In case of error, ensure we always close the connection.
});
});
}, },
initializeSocketForTesting: function(submissionID, filename) { socketTestCode: function(submissionID, filename) {
this.initializeSocket(Routes.test_submission_url, {id: submissionID, filename: filename}); return this.runSocket(Routes.test_submission_url, {id: submissionID, filename: filename}, (websocket) => {
this.websocket.on('default',this.handleTestResponse.bind(this)); websocket.on('default', this.handleTestResponse.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this)); websocket.on('exit', this.handleExitCommand.bind(this));
});
}, },
initializeSocketForScoring: function(submissionID) { socketScoreCode: function(submissionID) {
this.initializeSocket(Routes.score_submission_url, {id: submissionID}, function() { return this.runSocket(Routes.score_submission_url, {id: submissionID}, (websocket) => {
$('#assess').one('click', this.scoreCode.bind(this)) websocket.on('default', this.handleScoringResponse.bind(this));
}.bind(this)); websocket.on('hint', this.showHint.bind(this));
this.websocket.on('default',this.handleScoringResponse.bind(this)); websocket.on('exit', this.handleExitCommand.bind(this));
this.websocket.on('hint', this.showHint.bind(this)); websocket.on('status', this.showStatus.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this)); }).then(() => {
this.websocket.on('status', this.showStatus.bind(this)); $('#assess').one('click', this.scoreCode.bind(this));
});
}, },
initializeSocketForRunning: function(submissionID, filename) { socketRunCode: function(submissionID, filename) {
this.initializeSocket(Routes.run_submission_url, {id: submissionID, filename: filename}); return this.runSocket(Routes.run_submission_url, {id: submissionID, filename: filename}, (websocket) => {
this.websocket.on('input',this.showPrompt.bind(this)); websocket.on('input', this.showPrompt.bind(this));
this.websocket.on('write', this.printWebsocketOutput.bind(this)); websocket.on('write', this.printWebsocketOutput.bind(this));
this.websocket.on('clear', this.clearOutput.bind(this)); websocket.on('clear', this.clearOutput.bind(this));
this.websocket.on('turtle', this.handleTurtleCommand.bind(this)); websocket.on('turtle', this.handleTurtleCommand.bind(this));
this.websocket.on('turtlebatch', this.handleTurtlebatchCommand.bind(this)); websocket.on('turtlebatch', this.handleTurtlebatchCommand.bind(this));
this.websocket.on('render', this.printWebsocketOutput.bind(this)); websocket.on('render', this.printWebsocketOutput.bind(this));
this.websocket.on('exit', this.handleExitCommand.bind(this)); websocket.on('exit', this.handleExitCommand.bind(this));
this.websocket.on('status', this.showStatus.bind(this)); websocket.on('status', this.showStatus.bind(this));
this.websocket.on('hint', this.showHint.bind(this)); websocket.on('hint', this.showHint.bind(this));
this.websocket.on('files', this.prepareFileDownloads.bind(this)); websocket.on('files', this.prepareFileDownloads.bind(this));
});
}, },
handleExitCommand: function() { handleExitCommand: function() {

View File

@ -114,13 +114,16 @@ CodeOceanEditorRequestForComments = {
questionElement.prop("disabled", true); questionElement.prop("disabled", true);
$('#closeAskForCommentsButton').addClass('d-none'); $('#closeAskForCommentsButton').addClass('d-none');
var exercise_id = editor.data('exercise-id'); const exercise_id = editor.data('exercise-id');
var file_id = $('.editor').data('id'); const file_id = $('.editor').data('id');
var question = questionElement.val(); const question = questionElement.val();
const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
var createRequestForComments = function (submission) {
this.showSpinner($('#askForCommentsButton')); this.showSpinner($('#askForCommentsButton'));
$.ajax({
const response = await $.ajax({
method: 'POST', method: 'POST',
url: Routes.request_for_comments_path(), url: Routes.request_for_comments_path(),
data: { data: {
@ -131,24 +134,22 @@ CodeOceanEditorRequestForComments = {
question: question question: question
} }
} }
}).done(function() { }).catch(this.ajaxError.bind(this));
// trigger a run
this.runSubmission.call(this, submission);
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
}.bind(this)).fail(this.ajaxError.bind(this))
.always(function () {
bootstrap.Modal.getInstance($('#comment-modal')).hide(); bootstrap.Modal.getInstance($('#comment-modal')).hide();
this.hideSpinner(); this.hideSpinner();
$('#question').prop("disabled", false).val(''); $('#question').prop("disabled", false).val('');
$('#closeAskForCommentsButton').removeClass('d-none'); $('#closeAskForCommentsButton').removeClass('d-none');
$('#askForCommentsButton').one('click', this.requestComments.bind(this)); $('#askForCommentsButton').one('click', this.requestComments.bind(this));
}.bind(this));
};
this.createSubmission(cause, null, createRequestForComments.bind(this));
// we disabled the button to prevent that the user spams RFCs, but decided against this now. // we disabled the button to prevent that the user spams RFCs, but decided against this now.
//var button = $('#requestComments'); //var button = $('#requestComments');
//button.prop('disabled', true); //button.prop('disabled', true);
if (response) {
await this.runSubmission(submission);
$.flash.success({text: $('#askForCommentsButton').data('message-success')});
}
} }
}; };

View File

@ -6,10 +6,10 @@ CodeOceanEditorSubmissions = {
/** /**
* Submission-Creation * Submission-Creation
*/ */
createSubmission: function (initiator, filter, callback) { createSubmission: async function (initiator, filter) {
const editor = $('#editor'); const editor = $('#editor');
this.showSpinner(initiator); this.showSpinner(initiator);
var url = $(initiator).data('url') || editor.data('submissions-url'); const url = $(initiator).data('url') || editor.data('submissions-url');
if (url === undefined) { if (url === undefined) {
const data = { const data = {
@ -19,7 +19,9 @@ CodeOceanEditorSubmissions = {
Sentry.captureException(JSON.stringify(data)); Sentry.captureException(JSON.stringify(data));
return; return;
} }
var jqxhr = this.ajax({
try {
const response = await this.ajax({
data: { data: {
submission: { submission: {
cause: $(initiator).data('cause') || $(initiator).prop('id'), cause: $(initiator).data('cause') || $(initiator).prop('id'),
@ -31,13 +33,15 @@ CodeOceanEditorSubmissions = {
method: $(initiator).data('http-method') || 'POST', method: $(initiator).data('http-method') || 'POST',
url: url, url: url,
}); });
jqxhr.always(this.hideSpinner.bind(this)); this.hideSpinner();
jqxhr.done(this.createSubmissionCallback.bind(this)); this.createSubmissionCallback(response);
if(callback != null){ return response;
jqxhr.done(callback.bind(this)); } catch (error) {
} this.hideSpinner();
jqxhr.fail(this.ajaxError.bind(this)); // We require the callee to handle this error, e.g., through `this.ajaxError(error)`
throw error;
}
}, },
collectFiles: function() { collectFiles: function() {
@ -81,34 +85,42 @@ CodeOceanEditorSubmissions = {
/** /**
* File-Management * File-Management
*/ */
destroyFile: function() { destroyFile: async function() {
this.createSubmission($('#destroy-file'), function(files) { const submission = await this.createSubmission($('#destroy-file'), function(files) {
return _.reject(files, function(file) { return _.reject(files, function(file) {
return file.file_id === CodeOceanEditor.active_file.id; return file.file_id === CodeOceanEditor.active_file.id;
}); });
}, window.CodeOcean.refresh); }).catch(this.ajaxError.bind(this));
if(!submission) return;
window.CodeOcean.refresh();
}, },
downloadCode: function(event) { downloadCode: async function(event) {
event.preventDefault(); event.preventDefault();
this.createSubmission('#download', null,function(submission) {
const submission = await this.createSubmission('#download', null).catch(this.ajaxError.bind(this));
if(!submission) return;
// to download just a single file, use the following url // to download just a single file, use the following url
// window.location = Routes.download_file_submission_url(submission.id, CodeOceanEditor.active_file.filename); // window.location = Routes.download_file_submission_url(submission.id, CodeOceanEditor.active_file.filename);
window.location = Routes.download_submission_url(submission.id); window.location = Routes.download_submission_url(submission.id);
});
}, },
resetCode: function(initiator, onlyActiveFile = false) { resetCode: function(initiator, onlyActiveFile = false) {
this.startSentryTransaction(initiator); this.startSentryTransaction(initiator);
this.showSpinner(initiator); this.showSpinner(initiator);
this.ajax({
const response = await this.ajax({
method: 'GET', method: 'GET',
url: $('#start-over').data('url') || $('#start-over-active-file').data('url') url: $('#start-over').data('url') || $('#start-over-active-file').data('url')
}).done(function(response) { }).catch(this.ajaxError.bind(this));
this.hideSpinner(); this.hideSpinner();
if (!response) return;
App.synchronized_editor?.reset_content(response); App.synchronized_editor?.reset_content(response);
this.setEditorContent(response, onlyActiveFile); this.setEditorContent(response, onlyActiveFile);
}.bind(this));
}, },
setEditorContent: function(new_content, onlyActiveFile = false) { setEditorContent: function(new_content, onlyActiveFile = false) {
@ -126,16 +138,19 @@ CodeOceanEditorSubmissions = {
}, },
renderCode: function(event) { renderCode: function(event) {
event.preventDefault();
const cause = $('#render'); const cause = $('#render');
this.startSentryTransaction(cause); this.startSentryTransaction(cause);
event.preventDefault(); if (!cause.is(':visible')) return;
if (cause.is(':visible')) {
this.createSubmission(cause, null, function (submission) { const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
if (submission.render_url === undefined) return; if (submission.render_url === undefined) return;
const active_file = CodeOceanEditor.active_file.filename; const active_file = CodeOceanEditor.active_file.filename;
const desired_file = submission.render_url.filter(hash => hash.filepath === active_file); const desired_file = submission.render_url.filter(hash => hash.filepath === active_file);
const url = desired_file[0].url; const url = desired_file[0].url;
// Allow to open the new tab even in Safari. // Allow to open the new tab even in Safari.
// See: https://stackoverflow.com/a/70463940 // See: https://stackoverflow.com/a/70463940
setTimeout(() => { setTimeout(() => {
@ -151,43 +166,45 @@ CodeOceanEditorSubmissions = {
}; };
} }
}) })
});
}
}, },
/** /**
* Execution-Logic * Execution-Logic
*/ */
runCode: function(event) { runCode: function(event) {
event.preventDefault();
const cause = $('#run'); const cause = $('#run');
this.startSentryTransaction(cause); this.startSentryTransaction(cause);
event.preventDefault();
this.stopCode(event); this.stopCode(event);
if (cause.is(':visible')) { if (!cause.is(':visible')) return;
this.createSubmission(cause, null, this.runSubmission.bind(this));
} const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
await this.runSubmission(submission);
}, },
runSubmission: function (submission) { runSubmission: async function (submission) {
//Run part starts here //Run part starts here
this.running = true; this.running = true;
this.showSpinner($('#run')); this.showSpinner($('#run'));
$('#score_div').addClass('d-none'); $('#score_div').addClass('d-none');
this.toggleButtonStates(); this.toggleButtonStates();
this.initializeSocketForRunning(submission.id, CodeOceanEditor.active_file.filename); await this.socketRunCode(submission.id, CodeOceanEditor.active_file.filename);
}, },
testCode: function(event) { testCode: function(event) {
event.preventDefault();
const cause = $('#test'); const cause = $('#test');
this.startSentryTransaction(cause); this.startSentryTransaction(cause);
event.preventDefault(); if (!cause.is(':visible')) return;
if (cause.is(':visible')) {
this.createSubmission(cause, null, function(submission) { const submission = await this.createSubmission(cause, null).catch(this.ajaxError.bind(this));
if (!submission) return;
this.showSpinner($('#test')); this.showSpinner($('#test'));
$('#score_div').addClass('d-none'); $('#score_div').addClass('d-none');
this.initializeSocketForTesting(submission.id, CodeOceanEditor.active_file.filename); await this.socketTestCode(submission.id, CodeOceanEditor.active_file.filename);
}.bind(this));
}
}, },
/** /**
@ -216,6 +233,6 @@ CodeOceanEditorSubmissions = {
autosave: function () { autosave: function () {
clearTimeout(this.autosaveTimer); clearTimeout(this.autosaveTimer);
this.autosaveTimer = null; this.autosaveTimer = null;
this.createSubmission($('#autosave'), null); this.createSubmission($('#autosave'), null).catch(this.ajaxError.bind(this));
} }
}; };