Skip to content

Conversation

@achimnol
Copy link

@achimnol achimnol commented Jan 5, 2026

Summary

This PR adds graceful shutdown support for the tokio runtime, addressing #40.

Motivation

When Python extensions built with pyo3-async-runtimes are used in subprocesses or short-lived contexts, tokio tasks may still be running when Python interpreter finalization begins. This causes fatal errors like:

Fatal Python error: PyGILState_Release: thread state...must be current when releasing

Real-world Use Case: etcd-client-py

This implementation enables proper shutdown coordination as demonstrated in lablup/etcd-client-py#17, which uses the new APIs to implement automatic runtime cleanup:

  1. Reference counting via ACTIVE_CONTEXTS atomic counter tracks client contexts
  2. Async shutdown sequence in __aexit__:
    • Awaits tokio cleanup task (returns flag for last context)
    • If final context, calls _trigger_shutdown() (wraps request_shutdown_background)
    • Awaits asyncio.to_thread() to block-join the runtime (wraps join_pending_shutdown)

This ensures shutdown completes within the async context, avoiding deadlocks and race conditions where the runtime shuts down mid-task.

Implementation

The tokio runtime now lives in a dedicated thread (inspired by valkey-glide):

struct RuntimeWrapper {
    handle: Handle,                           // Thread-safe handle for spawning
    runtime_thread: Mutex<Option<JoinHandle<()>>>,
    shutdown_notifier: Arc<Notify>,
    timeout_sender: Mutex<Option<SyncSender<u64>>>,
}

When request_shutdown(timeout_ms) is called:

  1. The shutdown notifier signals the runtime thread
  2. The timeout value is sent via channel
  3. The runtime thread calls Runtime::shutdown_timeout()
  4. request_shutdown blocks on thread.join() until complete

New APIs

Tokio

Function Description
get_handle() -> Handle Returns cloneable Handle (recommended)
spawn(fut) Convenience function for spawning futures
spawn_blocking(f) Convenience function for blocking tasks
request_shutdown(timeout_ms) -> bool Blocking shutdown
request_shutdown_background(timeout_ms) -> bool Non-blocking shutdown
join_pending_shutdown(py) -> bool Join pending background shutdown

async-std (API consistency)

Function Description
spawn(fut) Wrapper for async_std::task::spawn
spawn_blocking(f) Wrapper for async_std::task::spawn_blocking
request_shutdown(timeout_ms) -> bool Sets flag only (cannot actually shut down)

Deprecated APIs

  • tokio::get_runtime() - Cannot be gracefully shut down. Use get_handle() instead.

Usage Example

import asyncio
from my_rust_module import cleanup_runtime

async def main():
    await do_work()
    cleanup_runtime()  # Wraps request_shutdown()

asyncio.run(main())

Dependency Changes

  • Replaced futures with futures-channel + futures-util for reduced dependency tree
  • Added parking_lot for RwLock
  • Added tokio sync feature for Notify

Backward Compatibility

  • Existing code using future_into_py works unchanged
  • get_runtime() is deprecated but still functional (uses a separate leaked runtime)
  • No breaking changes to the public API

Testing

Tested in etcd-client-py across:

  • Python 3.11, 3.12, 3.13, 3.14, 3.14t (free-threaded)
  • Linux x86_64 and ARM64
  • macOS

Related

achimnol added a commit to lablup/etcd-client-py that referenced this pull request Jan 5, 2026
Refactored code for upstream PR review:
- Add section comments for better code organization
- Remove debug eprintln! statements
- Add get_handle() as the recommended public API
- Improve documentation for all public functions
- Mark get_runtime() as deprecated with migration guidance

PR: PyO3/pyo3-async-runtimes#71
@achimnol
Copy link
Author

achimnol commented Jan 5, 2026

I have worked with Claude Code to resolve the runtime cleanup issues in our library (etcd-client-py) as mentioned above. Since this PR is an AI-generated code, it may have some clutters that need to be refined. I tried my best to make it human-readable, but please feel free to give me feedbacks.
(e.g., suggestions for test scenarios, desired changes to the code structure, etc.)

Also refer to the test cases in etcd-client-py for stress tests iterating over 20 times per run:

@achimnol achimnol changed the title feat(tokio): Add graceful shutdown support for tokio runtime feat: Add graceful shutdown support for tokio runtime Jan 5, 2026
@achimnol achimnol force-pushed the add-explicit-shutdown-api branch from 1b266e5 to da42ef5 Compare January 9, 2026 09:36
achimnol added a commit to lablup/etcd-client-py that referenced this pull request Jan 9, 2026
Refines PR PyO3/pyo3-async-runtimes#71 for review:
- Squashed 17 commits into single clean commit
- Updated macros to use new spawn_blocking() API
- Added #[allow(deprecated)] where get_runtime() is still needed
- All tests pass
Prepare dependencies for graceful shutdown support:
- Replace `futures` with `futures-channel` + `futures-util` for reduced dependency tree
- Add `parking_lot` for RwLock (needed for runtime wrapper)
- Add tokio `sync` feature for Notify (shutdown signaling)
- Add test entry for shutdown tests
Implement dedicated runtime thread pattern (inspired by valkey-glide):

RuntimeWrapper:
- Manages tokio runtime in dedicated "pyo3-tokio-runtime" thread
- Exposes Handle for thread-safe task spawning
- Supports graceful shutdown via Notify signaling

New public APIs:
- get_handle() -> Handle: Returns cloneable handle (recommended)
- spawn(fut) / spawn_blocking(f): Convenience spawning functions
- request_shutdown(timeout_ms): Blocking shutdown, waits for completion
- request_shutdown_background(timeout_ms): Non-blocking for async contexts
- join_pending_shutdown(py): Join pending background shutdown

Deprecated:
- get_runtime(): Cannot be gracefully shut down, use get_handle()

Implementation details:
- Runtime lives in dedicated thread, accessed only via Handle
- Shutdown signals thread via Notify, sends timeout via channel
- Thread calls Runtime::shutdown_timeout() then terminates
- Runtime slot cleared after shutdown, allowing re-initialization

Fixes PyO3#40
…nsistency

Add matching APIs to async-std module for consistency with tokio:

New public APIs:
- spawn(fut): Wrapper for async_std::task::spawn
- spawn_blocking(f): Wrapper for async_std::task::spawn_blocking
- request_shutdown(timeout_ms): Sets internal flag only

Note: async-std uses a global runtime model without explicit lifecycle
management. Unlike tokio, there's no way to actually shut down the
runtime. These APIs provide a consistent interface but request_shutdown
only sets a flag for API compatibility.

Internal changes:
- Add RuntimeWrapper struct for API consistency
- Add section comments matching tokio module organization
Update proc macros to work with new shutdown-aware APIs:

tokio_test macro:
- Replace get_runtime().spawn_blocking() with spawn_blocking()
- Uses the new module-level function that works with RuntimeWrapper

tokio_main macro:
- Add #[allow(deprecated)] for get_runtime().block_on() usage
- block_on() requires Runtime reference, not available via Handle
- This is intentional: main macro keeps runtime alive for current_thread
Test updates:
- Add test_tokio_shutdown.rs: Tests shutdown and re-initialization
- Add tests for spawn(), spawn_blocking(), get_handle() in tokio_asyncio
- Update tokio_run_forever to use spawn() instead of get_runtime().spawn()

Deprecation handling in tests:
- Add #[allow(deprecated)] for tests that intentionally use get_runtime()
- Tests using LocalSet::block_on() need Runtime reference (not Handle)
- Use local variable binding pattern for clean allow annotations

Other changes:
- Update CHANGELOG.md with new APIs and deprecations
- Update futures import to futures_util in generic.rs and lib.rs
- Update testing.rs futures import
@achimnol achimnol force-pushed the add-explicit-shutdown-api branch from da42ef5 to 97414ae Compare January 9, 2026 09:44
achimnol added a commit to lablup/etcd-client-py that referenced this pull request Jan 9, 2026
Updates vendored pyo3-async-runtimes (PyO3/pyo3-async-runtimes#71) with
cleaner commit history:

1. deps: Replace futures with futures-channel/futures-util, add parking_lot
2. feat(tokio): Add RuntimeWrapper with graceful shutdown support
3. feat(async-std): Add spawn/spawn_blocking/request_shutdown for API consistency
4. refactor(macros): Update to use new spawn_blocking API
5. test: Add shutdown tests and update existing tests for deprecated API
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.

Do we need some kind of shutdown method?

1 participant