Skip to content

Commit a5a62cd

Browse files
committed
feat: update run() function to use asyncio.Runner with uvloop for improved performance and cleanup
1 parent db52905 commit a5a62cd

File tree

1 file changed

+41
-35
lines changed

1 file changed

+41
-35
lines changed

src/asynctasq/utils/loop.py

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
33
Provides a single `run()` helper that automatically detects and uses the
44
running event loop, or creates a new uvloop-based loop if none is running.
5+
6+
Best Practices Applied (2025):
7+
- Uses asyncio.Runner with uvloop for Python 3.11+
8+
- Proper shutdown sequence: asyncgens -> executor -> cleanup
9+
- Graceful error handling during cleanup phases
10+
- Uses asyncio.run() semantics with uvloop optimization
511
"""
612

713
from __future__ import annotations
@@ -44,11 +50,11 @@ async def _cleanup_asynctasq():
4450

4551

4652
def run(coro: Any):
47-
"""Run coroutine using a new event loop.
53+
"""Run coroutine using a new event loop with uvloop optimization.
4854
4955
Creates a new event loop (uvloop if available, asyncio otherwise) and runs
50-
the coroutine to completion. This is similar to asyncio.run() but with
51-
automatic uvloop support and AsyncTasQ cleanup.
56+
the coroutine to completion. This follows asyncio.run() semantics with
57+
automatic uvloop support and proper cleanup sequence.
5258
5359
Args:
5460
coro: The coroutine to run
@@ -59,13 +65,20 @@ def run(coro: Any):
5965
Raises:
6066
RuntimeError: If called from within a running event loop
6167
68+
Best Practices (2025):
69+
- Uses asyncio.Runner with uvloop (Python 3.12+ min requirement)
70+
- Proper shutdown order: asyncgens → executor → cleanup → close
71+
- Graceful error handling at each cleanup phase
72+
- Matches asyncio.run() behavior with performance optimization
73+
6274
Note:
6375
- This function creates a NEW event loop and cannot be called from
6476
within an existing event loop
6577
- If you're already in an async context (FastAPI, Jupyter, etc.),
6678
use 'await' directly instead
6779
- For running event loops, use asynctasq.init() to register cleanup
6880
hooks automatically
81+
- uvloop provides 2-4x performance improvement over standard asyncio
6982
7083
Example:
7184
>>> from asynctasq.utils.loop import run
@@ -88,39 +101,32 @@ def run(coro: Any):
88101
raise
89102
# No event loop is running, proceed to create one
90103

91-
# Create a new event loop
92-
# Try to use uvloop for best performance, fall back to asyncio if unavailable
104+
# Use asyncio.Runner with uvloop for best performance
105+
# This is the modern recommended approach per 2025 best practices (Python 3.11+)
106+
# Since min supported version is 3.12, we always use Runner
93107
try:
94108
import uvloop
95109

96-
loop = uvloop.new_event_loop()
97-
logger.debug("Using uvloop event loop")
110+
# Use Runner with uvloop factory - combines best of both worlds
111+
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
112+
logger.debug("Using asyncio.Runner with uvloop")
113+
try:
114+
return runner.run(coro)
115+
finally:
116+
# Runner handles asyncgens and executor shutdown automatically
117+
# We only need to cleanup AsyncTasQ resources
118+
try:
119+
runner.run(_cleanup_asynctasq())
120+
except Exception as e:
121+
logger.debug(f"AsyncTasQ cleanup completed with warnings: {e}")
98122
except ImportError:
99-
loop = asyncio.new_event_loop()
100-
logger.debug("Using asyncio event loop (uvloop not available)")
101-
102-
asyncio.set_event_loop(loop)
103-
104-
try:
105-
return loop.run_until_complete(coro)
106-
finally:
107-
try:
108-
# Cleanup AsyncTasQ resources before shutting down asyncgens
109-
loop.run_until_complete(_cleanup_asynctasq())
110-
except Exception:
111-
pass
112-
try:
113-
# Shutdown async generators
114-
loop.run_until_complete(loop.shutdown_asyncgens())
115-
except Exception:
116-
pass
117-
try:
118-
# Shutdown default executor
119-
loop.run_until_complete(loop.shutdown_default_executor())
120-
except Exception:
121-
pass
122-
finally:
123-
# Close the loop
124-
loop.close()
125-
# Reset the event loop to None
126-
asyncio.set_event_loop(None)
123+
# uvloop not available, use standard Runner
124+
with asyncio.Runner() as runner:
125+
logger.debug("Using asyncio.Runner (uvloop not available)")
126+
try:
127+
return runner.run(coro)
128+
finally:
129+
try:
130+
runner.run(_cleanup_asynctasq())
131+
except Exception as e:
132+
logger.debug(f"AsyncTasQ cleanup completed with warnings: {e}")

0 commit comments

Comments
 (0)