Skip to content

Commit 3672e17

Browse files
Terminate generating events early if the session is closed (#2783)
* Return early if no awake events * Add to changelog * Add test * Clarify when events are cancelled in the docs
1 parent a580a93 commit 3672e17

5 files changed

Lines changed: 63 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ by by [@abidlabs](https://github.com/abidlabs) in [PR 2745](https://github.com/g
119119
* Fixed bug loading audio input models from the hub by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2779](https://github.com/gradio-app/gradio/pull/2779).
120120
* Fixed issue where entities were not merged when highlighted text was generated from the
121121
dictionary inputs [@payoto](https://github.com/payoto) in [PR 2767](https://github.com/gradio-app/gradio/pull/2767)
122-
122+
* Fixed bug where generating events did not finish running even if the websocket connection was closed by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 2783](https://github.com/gradio-app/gradio/pull/2783).
123123

124124
## Documentation Changes:
125125
No changes to highlight.

gradio/events.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def change(
6767
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
6868
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
6969
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
70-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
70+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
7171
"""
7272
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
7373
if status_tracker:
@@ -130,7 +130,7 @@ def click(
130130
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
131131
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
132132
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
133-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
133+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
134134
"""
135135
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
136136
if status_tracker:
@@ -195,7 +195,7 @@ def submit(
195195
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
196196
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
197197
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
198-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
198+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
199199
"""
200200
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
201201
if status_tracker:
@@ -259,7 +259,7 @@ def edit(
259259
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
260260
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
261261
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
262-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
262+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
263263
"""
264264
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
265265
if status_tracker:
@@ -323,7 +323,7 @@ def clear(
323323
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
324324
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
325325
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
326-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
326+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
327327
"""
328328
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
329329
if status_tracker:
@@ -387,7 +387,7 @@ def play(
387387
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
388388
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
389389
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
390-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
390+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
391391
"""
392392
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
393393
if status_tracker:
@@ -449,7 +449,7 @@ def pause(
449449
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
450450
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
451451
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
452-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
452+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
453453
"""
454454
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
455455
if status_tracker:
@@ -511,7 +511,7 @@ def stop(
511511
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
512512
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
513513
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
514-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
514+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
515515
"""
516516
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
517517
if status_tracker:
@@ -575,7 +575,7 @@ def stream(
575575
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
576576
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
577577
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
578-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
578+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
579579
"""
580580
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
581581
self.streaming = True
@@ -639,7 +639,7 @@ def blur(
639639
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
640640
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
641641
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
642-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
642+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
643643
"""
644644
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
645645

@@ -696,7 +696,7 @@ def upload(
696696
preprocess: If False, will not run preprocessing of component data before running 'fn' (e.g. leaving it as a base64 string if this method is called with the `Image` component).
697697
postprocess: If False, will not run postprocessing of component data before returning 'fn' output to the browser.
698698
cancels: A list of other events to cancel when this event is triggered. For example, setting cancels=[click_event] will cancel the click_event, where click_event is the return value of another components .click method.
699-
every: Run this event 'every' number of seconds. Interpreted in seconds. Queue must be enabled.
699+
every: Run this event 'every' number of seconds while the client connection is open. Interpreted in seconds. Queue must be enabled.
700700
"""
701701
# _js: Optional frontend js method to run before running 'fn'. Input arguments for js method are values of 'inputs' and 'outputs', return should be a list of values for output components.
702702

gradio/queue.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,15 +314,22 @@ async def process_events(self, events: List[Event], batch: bool) -> None:
314314
if not is_alive:
315315
return
316316
old_response = response
317+
open_ws = []
317318
for event in awake_events:
318-
await self.send_message(
319+
open = await self.send_message(
319320
event,
320321
{
321322
"msg": "process_generating",
322323
"output": old_response.json,
323324
"success": old_response.status == 200,
324325
},
325326
)
327+
open_ws.append(open)
328+
awake_events = [
329+
e for e, is_open in zip(awake_events, open_ws) if is_open
330+
]
331+
if not awake_events:
332+
return
326333
response = await self.call_prediction(awake_events, batch)
327334
for event in awake_events:
328335
await self.send_message(

guides/03_building_with_blocks/01_blocks_and_event_listeners.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Instead of being triggered by a click, the `welcome` function is triggered by ty
3333
## Running Events Continuously
3434

3535
You can run events on a fixed schedule using the `every` parameter of the event listener. This will run the event
36-
`every` number of seconds. Note that this does not take into account the runtime of the event itself. So a function
36+
`every` number of seconds while the client connection is open. If the connection is closed, the event will stop running after the following iteration.
37+
Note that this does not take into account the runtime of the event itself. So a function
3738
with a 1 second runtime running with `every=5`, would actually run every 6 seconds.
3839

3940
Here is an example of a sine curve that updates every second!

test/test_blocks.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,47 @@ async def test_every_does_not_block_queue(self):
979979
else:
980980
break
981981

982+
@pytest.mark.asyncio
983+
async def test_generating_event_cancelled_if_ws_closed(self, capsys):
984+
def generation():
985+
for i in range(10):
986+
time.sleep(0.1)
987+
print(f"At step {i}")
988+
yield i
989+
return "Hello!"
990+
991+
with gr.Blocks() as demo:
992+
greeting = gr.Textbox()
993+
button = gr.Button(value="Greet")
994+
button.click(generation, None, greeting)
995+
996+
app, _, _ = demo.queue(max_size=1).launch(prevent_thread_lock=True)
997+
998+
async with websockets.connect(
999+
f"{demo.local_url.replace('http', 'ws')}queue/join"
1000+
) as ws:
1001+
completed = False
1002+
n_steps = 0
1003+
while not completed:
1004+
msg = json.loads(await ws.recv())
1005+
if msg["msg"] == "send_data":
1006+
await ws.send(json.dumps({"data": [0], "fn_index": 0}))
1007+
elif msg["msg"] == "send_hash":
1008+
await ws.send(json.dumps({"fn_index": 0, "session_hash": "shdce"}))
1009+
elif msg["msg"] == "process_generating":
1010+
if n_steps == 2:
1011+
# Close the websocket
1012+
break
1013+
n_steps += 1
1014+
else:
1015+
continue
1016+
await asyncio.sleep(1)
1017+
# If the generation function did not get cancelled
1018+
# it would have finished running and `At step 9` would
1019+
# have been printed
1020+
captured = capsys.readouterr()
1021+
assert "At step 9" not in captured.out
1022+
9821023

9831024
class TestAddRequests:
9841025
def test_no_type_hints(self):

0 commit comments

Comments
 (0)