-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Cancel server progress from the python client #8245
Conversation
@@ -247,7 +247,9 @@ def stream_messages( | |||
resp = json.loads(line[5:]) | |||
if resp["msg"] == ServerMessage.heartbeat: | |||
continue | |||
elif resp["msg"] == ServerMessage.server_stopped: | |||
elif ( | |||
resp.get("message", "") == ServerMessage.server_stopped |
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.
When the server stops unexpectedly it sends an unexpected_error
not a server_stopped
message.
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.
Just to make sure I understand, if the server stops unexpectedly, resp
will not have a msg
key but instead have a message
key? That seems weird, can we unify?
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.
Do we need to make any changes in the frontend, e.g.
gradio/client/js/src/helpers/api_info.ts
Line 210 in bed2f82
case "unexpected_error": |
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 sends a response with msg
and message
keys but the msg
key is unexpected_error
as opposed to server_stopped
so this if block never catches. I don't think it makes sense to check for unexpected_error
in that codeblock because unexpected errors should not close the entire stream since they can be isolated to a single event id.
🪼 branch checks and previews
Install Gradio from this PR pip install https://gradio-builds.s3.amazonaws.com/7b5ab761112faff49c5cb518aebed7bc2caf8097/gradio-4.29.0-py3-none-any.whl Install Gradio Python Client from this PR pip install "gradio-client @ git+https://github.com/gradio-app/gradio@7b5ab761112faff49c5cb518aebed7bc2caf8097#subdirectory=client/python" |
🦄 change detectedThis Pull Request includes changes to the following packages.
With the following changelog entry.
Maintainers or the PR author can modify the PR title to modify this entry.
|
What is a cancel event? |
An event that includes |
if inferred_fn_index in dep["cancels"]: | ||
cancellable = True | ||
cancel_fn_index = i |
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 don't think this will work if we have a single event that cancels multiple events. e.g. something like:
import gradio as gr
with gr.Blocks() as demo:
t1 = gr.Textbox()
t2 = gr.Textbox()
t3 = gr.Textbox()
t4 = gr.Textbox()
e = t1.change(lambda x:x, t1, t2)
f = t2.change(lambda x:x, t2, t3)
t4.change(lambda x:x, t4, t3, cancels=[e, f])
if I understand correctly, let's say a user starts, via api, jobs e and f. If they cancel one of them, it'll cancel the other as well
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 this is actually ok because when using the app via the UI, the t4.change
event will also cancel both events. So I don't think the client should do something different from the UI.
That being said, I will look into your suggestion of creating a unique cancel event for every dependency so that every event can be cancelled from a client going forward!
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.
So I would agree that if someone calls the route corresponding to the t4.change()
event, they may expect e
and f
to get canceled.
But if someone starts the e
job on its own via the api, and they cancel it using job_e.cancel()
, I don't think the expectation is that job_f
should get canceled as well
I like the approach in this PR @freddyaboulton! I noticed the issue above^ but what if we use essentially the same idea to create a "hidden" cancel event for every dependency, and then we call that via the client? |
2755d49
to
6bfa640
Compare
"/" + self.client.config["dependencies"][i].get("api_name") | ||
for i in other_cancelled | ||
] | ||
cancel_msg = ( |
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.
cancel_msg = None | ||
cancellable = fn_index is not None | ||
cancel_msg = ( | ||
"Cancelling this job will not stop the server from running. " |
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.
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.
In my testing, I noticed that this message appears even if the Gradio app (is running 4.29 and) already includes an event that cancels this event
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "gradio", | |||
"version": "4.29.0", | |||
"version": "4.30.0", |
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 this was for testing purposes, let's revert?
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.
Yea I needed this for the tests to test the new logic. I asked @pngwn and he thinks its ok cause the release pr will override this anyways. But I can revert if we don't mind not having the test coverage until we release!
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.
Ah got it, yea we can leave it
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "@gradio/lite", | |||
"version": "4.29.0", | |||
"version": "4.30.0", |
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.
Same as above
@@ -802,6 +803,11 @@ async def queue_join_helper( | |||
raise HTTPException(status_code=status_code, detail=event_id) | |||
return {"event_id": event_id} | |||
|
|||
@app.post("/cancel") |
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.
Nice
fn_index, other_cancelled = ( | ||
min(candidates, key=lambda x: len(x[1])) if candidates else (None, None) | ||
) | ||
cancel_msg = None |
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.
Unnecessary line?
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.
Needs to be set otherwise we'd reference an unbound variable below
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.
LGTM @freddyaboulton! Tested different apps and this works perfectly for Gradio apps going forward and I'm glad we were able to get some level of backwards-compatibility as well.
(see my note above about the warning message appearing even if the Gradio app already includes an event that cancels this event)
ab1f384
to
034e4f6
Compare
Thanks for the review @abidlabs ! |
Description
Closes: #8188
If the demo has a defined cancel event for the given function, it will be called from
job.cancel
automatically. This still won't match the 3.x behavior (.cancel
would always stop an iterator in the server even if no cancel event was defined) but I think this is the best we can do without restructuring how the client sets up the SSE stream.The issue is that we have one connection to the server for reading updates for all jobs so we can't close that stream if one job is cancelled because it would stop all currently submitted jobs.
I thought about using the "simple" API in the python client as that would let us have a different stream for each connection but that would not be backwards compatible for all versions of 4.x and would require restructuring the client further so I decided against that.
🎯 PRs Should Target Issues
Before your create a PR, please check to see if there is an existing issue for this change. If not, please create an issue before you create this PR, unless the fix is very small.
Not adhering to this guideline will result in the PR being closed.
Tests
PRs will only be merged if tests pass on CI. To run the tests locally, please set up your Gradio environment locally and run the tests:
bash scripts/run_all_tests.sh
You may need to run the linters:
bash scripts/format_backend.sh
andbash scripts/format_frontend.sh