-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgrade workers #112
Upgrade workers #112
Conversation
src/workers/python/init.py
Outdated
elif event_type == "input": | ||
return cb("input", data["prompt"], contentType="text/plain") | ||
elif event_type == "sleep": | ||
return cb("sleep", str(data["seconds"]*1000), contentType="text/integer") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest using makeRunnerCallback
from pyodide-worker-runner and moving this event translation logic to JS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still unsure what the best approach is. The makeRunnerCallback method expects an array of callbacks, which becomes quite unwieldy. I moved to a PubSub system to prevent needing to pass many callbacks. I would think that adding a catch-all to makeRunnerCallback would make it a bit more user-friendly for this specific case, but I wouldn't call this part of my code stable yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should only need an object with two callbacks for the two event types input and output. The output part types aren't looked at.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, I meant that makeRunnerCallback expects an object with at least 2 callbacks and then extra callbacks per possible type. I have an error callback, the interrupt just in case, and potentially a debugging callback which makes this unwieldy to pass around. If more types get added, this also poses issues for makeRunnerCallback. The translation in Python is not exactly ideal either, so I'll think about a hybrid approach to clean this up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds like it would be better if makeRunnerCallback didn't try to be so helpful with other event types and just let you provide a function to act as a default. That way you could just let other events pass through to your own system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A catch-all would indeed be more practical in my opinion.
this.setState(RunState.Stopping); | ||
BackendManager.publish({ | ||
type: BackendEventType.End, | ||
data: "User cancelled run", contentType: "text/plain" | ||
}); | ||
await this.backend.then(b => b.interrupt()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still assuming that nothing will catch the KeyboardInterrupt. You can only do this if call
throws InterruptError
. Treat client.interrupt as a suggestion/request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is to fully interrupt. Ideally, some method should exist in synClient to force-interrupt (maybe a configurable timeout that terminates the worker if the interrupt was not successful?) Having to interpret client.interrupt as a suggestion is counter-intuitive to me.
*/ | ||
async setProgrammingLanguage(programmingLanguage: ProgrammingLanguage): Promise<void> { | ||
if (this.programmingLanguage !== programmingLanguage) { // Expensive, so ensure it is needed | ||
await this.backend.then(b => b.interrupt()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to be sure here then use client.terminate(), but then the client must be deleted and not reused.
const end = new Date().getTime(); | ||
this.setState(RunState.Ready, t("Papyros.finished", { time: (end - start) / 1000 })); | ||
if (terminated) { | ||
await this.start(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comsync already restarts the worker after terminating. Also, you can end up here if you interrupt/terminate the client for the sake of switching programming language.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restarting the worker is not enough, as the launch method must also be called. This is done in start, so that should be fine. For the second case, I don't think anything will go wrong if start is called twice.
@@ -121,7 +120,8 @@ class JavaScriptWorker extends Backend { | |||
return ret; | |||
} | |||
|
|||
override _runCodeInternal(code: string): Promise<any> { | |||
override runCode(extras: SyncExtras, code: string): Promise<any> { | |||
this.extras = extras; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that input is still supported in JS. What about sleeping?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sleeping doesn't really seem to be a thing in JS. What exists is setTimeout, which is non-blocking. In order to properly support that, I think I would have to store the timers internally and await them before giving control back to the user. Our focus is currently on Python, so I don't plan on changing this yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exists is setTimeout, which is non-blocking. In order to properly support that, I think I would have to store the timers internally and await them before giving control back to the user.
What exactly are you saying? Using setTimeout as usual doesn't work properly? Or do you mean making setTimeout blocking?
You could add a custom sleep
function using extras.syncSleep()
, which currently just means running the callback with the sleep event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I meant is that a blocking sleep call does not exist in JavaScript by default. It can be emulated with Promises and awaiting them, but I haven't played around a lot with async/await in the JavaScript runner.
setTimeout is non-blocking and works, but these calls are not "awaited", so if you write eg
setTimeout(() => console.log("Timer"), 5000)
then the run will be finished immediately and the log statement will appear later in the actual console instead of the output field. This is because of the global overrides being done at the start and being cleared at the end of a run. That's why some awaiting mechanism should be added for these timeouts.
if typ in ["stderr", "traceback", "syntax_error"]: | ||
cb("error", part["text"], contentType=part.get("contentType")) | ||
elif typ in ["input", "input_prompt"]: | ||
# Do not display values entered by user for input | ||
continue | ||
else: | ||
cb("output", part["text"], contentType=part.get("contentType")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should embrace the python_runner output part types approach. You're sending stderr and stdout to separate event types and code paths, but they're almost exactly the same thing, just different colours. Meanwhile you're combining stderr and tracebacks which get handled completely differently. Ultimately these are all just different types of output which all get rendered to the same output area, even if it's in different ways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These things are a bit too tied to Python, so they're not easily generalised to other languages (e.g. due to presence of snake_case). With the eventing system, I think it's nice to be able to only listen to error messages instead of getting all output. Finally, I don't think our target group will often write to stderr, so the focus is on the tracebacks as they will be the main reason for error displaying. The current translations seem fine to me.
This PR upgrades the workers to make use of the recently published packages Comsync and pyodide-worker-runner.
It also adds useful refactoring parts from #109 that are not strictly tied into debugging but made integrating it more feasible.
This should increase the general flexibility.
Support was added for the following:
sleeping
interrupting with restarting
clicking run before the load is finished (is awaited during run instead if needed)
generating a tar with dependencies to pre-load, allowing internal Python code to use proper packages instead of a raw string
Running code is refactored to be done within a separate component contained within the Papyros object
Closes #66
Closes #38
Closes #86