Skip to content

Drain run-history writes on shutdown - #13828

Open
hysts wants to merge 3 commits into
mainfrom
fix/history-records-lost-on-shutdown
Open

Drain run-history writes on shutdown#13828
hysts wants to merge 3 commits into
mainfrom
fix/history-records-lost-on-shutdown

Conversation

@hysts

@hysts hysts commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Description

A run-history record is filed as a detached task, so a prediction never waits on the Hub. app.state.history_tasks collects those tasks, but nothing ever awaits them, so a shutdown cancels whatever is still in flight and the record is gone.

record_run awaits externalize_assets twice and then the write limiter before it reaches the Hub, and where the cancellation lands decides the outcome:

record pinned at task after shutdown record
the Hub write cancelled=True written
the async prelude, before any Hub call cancelled=True lost

close() returned in 0.20 s in both cases. The first row survives only because anyio.to_thread.run_sync cannot interrupt its worker thread, so the thread runs to completion and its result is discarded; that also means it would not survive the process exit that normally follows a shutdown. The prelude is the larger window in practice, since asset externalisation fetches remote media and the limiter can hold a task while other writes drain.

The fix adds a drain to the lifespan stack, entered last so it unwinds first and the records are flushed while the rest of the app is still up. It is bounded at five seconds so a shutdown cannot hang on an unreachable Hub, and anything still pending after that is cancelled as before.

Closes: #13825

Testing

Worth one note, because the obvious version of this test does not work. The regression test puts its delay in record_run's prelude rather than in the Hub call. A write that has already started finishes either way, for the anyio reason above, so slowing the Hub call gives a test that passes with or without the fix. Verified in both directions: it fails with AssertionError: the record was dropped by the shutdown against main's route_utils.py, and passes with the change.

test/test_history.py::TestServerSideRecording::test_a_normal_prediction_is_recorded can flake on this branch, which is cut from main. That is #13823, fixed by #13824, and unrelated to this change.

AI Disclosure

We encourage the use of AI tooling in creating PRs, but the any non-trivial use of AI needs be disclosed. E.g. if you used Claude to write a first draft, you should mention that. Trivial tab-completion doesn't need to be disclosed. You should self-review all PRs, especially if they were generated with AI.

  • I used AI to... I used AI to investigate the root cause and implement the fix.
  • I did not use AI

🎯 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.

Testing and Formatting Your Code

  1. PRs will only be merged if tests pass on CI. We recommend at least running the backend tests locally, please set up your Gradio environment locally and run the backed tests: bash scripts/run_backend_tests.sh

  2. Please run these bash scripts to automatically format your code: bash scripts/format_backend.sh, and (if you made any changes to non-Python files) bash scripts/format_frontend.sh

A record is filed as a detached task so a prediction never waits on the
Hub, and app.state.history_tasks collects those tasks, but nothing ever
awaits them. A shutdown therefore cancels whatever is still in flight.

Measured on a real launched server, with the record pinned open so it
could not finish by luck. record_run awaits externalize_assets twice
and then the write limiter before it reaches the Hub, and where the
cancellation lands decides the outcome: pinned at the Hub write the
record survives, because anyio.to_thread.run_sync cannot interrupt its
worker thread; pinned anywhere earlier it is lost outright. close()
returned in 0.20 s either way.

The prelude is the larger window in practice, since asset
externalisation fetches remote media and the limiter can hold a task
while other writes drain.

Enter the drain last in the lifespan stack so it unwinds first, while
the rest of the app is still up, and bound it at five seconds so a
shutdown cannot hang on an unreachable Hub.

The regression test puts its delay in the prelude rather than in the
Hub call for the reason above: a write that has already started
finishes either way and would not tell the two behaviours apart.
@hysts hysts self-assigned this Sep 5, 2026
@hysts
hysts requested a lite review from Copilot September 5, 2026 02:14
@gradio-pr-bot

gradio-pr-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/5ab37f8760e224b9e715d9e97a510d01894cf8c3/gradio-6.26.0-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@5ab37f8760e224b9e715d9e97a510d01894cf8c3#subdirectory=client/python"

Import Gradio JS Client from this PR via CDN

import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/5ab37f8760e224b9e715d9e97a510d01894cf8c3/browser.js";

@gradio-pr-bot

Copy link
Copy Markdown
Collaborator

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
gradio patch

  • Drain run-history writes on shutdown

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped, includes a targeted regression test for the failure mode described, and the shutdown drain is bounded to avoid hanging.

Pull request overview

Adds a bounded shutdown “drain” step to Gradio’s lifespan handling so detached run-history recording tasks have a chance to finish before the app fully tears down, preventing records from being dropped during shutdown.

Changes:

  • Introduces a lifespan context manager that waits (up to 5s) for in-flight app.state.history_tasks to complete and cancels any stragglers.
  • Ensures the drain is entered last in the lifespan stack so it runs first during shutdown (while other app resources are still available).
  • Adds a regression test that forces delay in the pre-Hub portion of record_run to validate the fix.
File summaries
File Description
gradio/route_utils.py Adds _drain_history_tasks() and wires it into the lifespan stack to flush run-history tasks on shutdown with a timeout.
test/test_history.py Adds an end-to-end regression test asserting an in-flight run-history record is persisted across lifespan shutdown.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The drain read app.state directly, guarding only the attribute on it.
The lifespan tests in test_routes.py pass a SimpleNamespace stand-in
that has no state at all, so the drain raised AttributeError during
their shutdown.
@hysts
hysts marked this pull request as ready for review September 5, 2026 02:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run-history records are lost when the server shuts down mid-write

3 participants