Skip to content

Fix RuntimeError: reentrant call inside _io.BufferedWriter on Ctrl+C - #15949

Merged
lukasmasuch merged 2 commits into
streamlit:developfrom
chang-pro:fix-sigint-reentrant-console-write
Jul 13, 2026
Merged

Fix RuntimeError: reentrant call inside _io.BufferedWriter on Ctrl+C#15949
lukasmasuch merged 2 commits into
streamlit:developfrom
chang-pro:fix-sigint-reentrant-console-write

Conversation

@chang-pro

@chang-pro chang-pro commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Describe your changes

Stopping the server directly inside the SIGINT/SIGTERM handler can interrupt the asyncio event loop in the middle of a buffered console write. Server.stop() immediately prints Stopping... via click.secho, which re-enters the same _io.BufferedWriter and trips CPython's reentrancy guard (RuntimeError: reentrant call inside <_io.BufferedWriter>), mainly on Windows where colorama flushes the console handle on every write.

The signal handler now schedules server.stop on the running event loop with call_soon_threadsafe, so the shutdown (and its console output) runs as a normal loop callback instead of inside the signal handler. When no loop is running (e.g. direct invocation in tests), the previous inline behavior is kept.

Screenshot or video (only for visual changes)

N/A

GitHub Issue Link (if applicable)

Fixes #15740

Testing Plan

  • Unit tests: added test_signal_handler_defers_stop_to_running_event_loop to lib/tests/streamlit/web/bootstrap_test.py, asserting the stop call does not run inline in the handler and executes on the next loop iteration. The existing signal handler tests cover the no-running-loop fallback. Full bootstrap_test.py passes (44 tests), ruff check/format clean.
  • The original crash is a timing-dependent race (signal must land mid-write), so it cannot be asserted deterministically; the test instead pins the invariant that no console I/O happens inside the handler while a loop is running.

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Note

Low Risk
Narrow change to shutdown scheduling on signals with a no-loop fallback; new unit test pins the deferral behavior.

Overview
Fixes Ctrl+C / SIGTERM crashes where shutdown could raise RuntimeError: reentrant call inside <_io.BufferedWriter> (especially on Windows) because the signal handler called server.stop() inline while the event loop was mid console write; Server.stop prints Stopping... via the CLI.

_set_up_signal_handler now uses asyncio.get_running_loop() and, when a loop exists, schedules server.stop with loop.call_soon_threadsafe so shutdown and its console output run on the next loop callback. If there is no running loop, behavior is unchanged: server.stop() is still invoked directly from the handler (covers tests and non-async paths).

Adds test_signal_handler_defers_stop_to_running_event_loop to assert stop is not called inside the handler and runs after await asyncio.sleep(0).

Reviewed by Cursor Bugbot for commit a75a075. Bugbot is set up for automated code reviews on this repo. Configure here.

Stopping the server directly inside the signal handler can interrupt
the event loop in the middle of a buffered console write, making the
console output triggered by Server.stop raise 'RuntimeError: reentrant
call inside <_io.BufferedWriter>' (mainly on Windows). Closes streamlit#15740
Copilot AI review requested due to automatic review settings July 12, 2026 18:14

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

Thanks for contributing to Streamlit! 🎈

Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki.

The review process:

  1. Initial triage: A maintainer will apply labels, approve CI to run, and trigger AI-assisted reviews. Your PR may be flagged with status:needs-product-approval if the feature requires product team sign-off.

  2. Code review: A core maintainer will start reviewing your PR once:

    • It is marked as 'ready for review', not 'draft'
    • It has status:product-approved (or doesn't need it)
    • All CI checks pass
    • All AI review comments are addressed

We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏

@snyk-io

snyk-io Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR changes Streamlit shutdown handling for Ctrl+C and termination signals.

  • Defers server.stop() onto the running asyncio loop from the signal handler.
  • Keeps the direct server.stop() fallback when no event loop is running.
  • Adds a test that verifies shutdown is scheduled after the handler returns.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
lib/streamlit/web/bootstrap.py Updates signal handling so server shutdown runs as an event-loop callback when possible.
lib/tests/streamlit/web/bootstrap_test.py Adds coverage for deferred shutdown behavior inside a running asyncio loop.

Reviews (2): Last reviewed commit: "Merge branch 'develop' into fix-sigint-r..." | Re-trigger Greptile

@lukasmasuch lukasmasuch added ai-review If applied to PR or issue will run AI review workflow change:bugfix PR contains bug fix implementation impact:users PR changes affect end users and removed ai-review If applied to PR or issue will run AI review workflow labels Jul 12, 2026

@github-actions github-actions Bot 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.

Summary

This PR fixes a RuntimeError: reentrant call inside <_io.BufferedWriter> crash that occurs when Ctrl+C (SIGINT) or SIGTERM is received while the asyncio event loop is mid-write to the console. The root cause is that Server.stop() calls cli_util.print_to_cli(" Stopping...", fg="blue"), which on Windows (with colorama) can re-enter the same buffered writer that was active when the signal fired.

The fix defers server.stop() to the event loop via loop.call_soon_threadsafe() when a running loop exists, falling back to the previous inline call when no loop is available.

Reviewer consensus: Both reviewers (claude-4.6-opus-high-thinking and gpt-5.3-codex-high) independently approved this PR with no blocking issues. All assessments below reflect full agreement between reviewers.

Code Quality

The implementation is clean, minimal, and follows Python best practices for signal handling with asyncio:

  • asyncio.get_running_loop() correctly detects whether an event loop is active.
  • call_soon_threadsafe() is specifically designed to be safely callable from signal handlers — it writes to an internal wakeup fd rather than touching the event loop's internal state directly.
  • The fallback to direct server.stop() when no loop is running preserves testability and handles edge cases (e.g., direct invocations in test harnesses).
  • Comments explain the why (reentrant write risk) rather than the what, following project conventions.
  • The function remains private (_-prefixed), and asyncio is already imported at module level — no new imports needed.

No maintainability concerns or pattern violations were identified by either reviewer.

Test Coverage

Both reviewers confirmed adequate coverage:

  • New test test_signal_handler_defers_stop_to_running_event_loop validates the core invariant: server.stop() is not called inline inside the handler when an event loop is running, and is executed on the next loop iteration via await asyncio.sleep(0).
  • Existing test test_signal_handler_stops_server naturally covers the no-running-loop fallback because it invokes the handler outside of asyncio.run(), causing get_running_loop() to raise RuntimeError.

Together these two tests cover both branches of the try/except. The new test follows the file's existing TestCase pattern, uses clear assertions, and includes a docstring.

No E2E test is needed — this is a timing-dependent race condition in process signal handling that cannot be reliably exercised in Playwright.

One reviewer optionally suggested adding a unit assertion for repeated signal delivery (idempotent stop scheduling) to further harden regression coverage. This is a reasonable follow-up but not blocking.

Backwards Compatibility

No breaking changes. Both reviewers agreed:

  • The external behavior is identical: SIGINT/SIGTERM still triggers server.stop() and graceful shutdown.
  • The only difference is when stop() executes — on the next loop callback rather than inline in the signal handler — which is imperceptible to users.
  • The fallback preserves the old behavior when no event loop is running.

Security & Risk

No security concerns identified by either reviewer:

  • No new dependencies, endpoints, or network behavior.
  • No changes to authentication, session handling, or data paths.
  • The change purely affects internal shutdown scheduling, reducing crash risk.

Regression risk is very low. The worst case if call_soon_threadsafe fails to execute the callback is that shutdown doesn't complete cleanly — the same failure mode as the original crash.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Key evidence from changed files:
    • lib/streamlit/web/bootstrap.py: Signal handler scheduling change — purely internal process management, no network/auth/embedding/asset behavior affected.
    • lib/tests/streamlit/web/bootstrap_test.py: Corresponding unit test for the deferral invariant.
  • Suggested external test focus areas: N/A
  • Confidence: High
  • Assumptions and gaps: None. The change is entirely internal to process lifecycle management and does not touch any externally observable behavior. Both reviewers independently reached this conclusion.

Accessibility

No frontend changes. Not applicable.

Recommendations

  1. Optional (non-blocking): Consider adding a unit test for repeated signal delivery while the loop is running to verify idempotent stop scheduling. This could be a follow-up PR.

Verdict

APPROVED: Clean, minimal bugfix that correctly defers shutdown I/O out of the signal handler using the standard asyncio pattern. Both reviewers (claude-4.6-opus-high-thinking and gpt-5.3-codex-high) approved unanimously with no blocking issues. Test coverage adequately validates both code paths.


Consolidated review by claude-4.6-opus-high-thinking. Individual reviews: claude-4.6-opus-high-thinking (APPROVED), gpt-5.3-codex-high (APPROVED).

@lukasmasuch lukasmasuch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM 👍 Thanks for the contribution

@lukasmasuch
lukasmasuch enabled auto-merge (squash) July 13, 2026 11:13
@lukasmasuch
lukasmasuch merged commit c385346 into streamlit:develop Jul 13, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:users PR changes affect end users

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RuntimeError: reentrant call inside _io.BufferedWriter when stopping server via Ctrl+C on Windows

3 participants