Skip to content

fix: replace deprecated asyncio event loop patterns#877

Open
mangelajo wants to merge 5 commits into
jumpstarter-dev:mainfrom
mangelajo:fix/asyncio-deprecation-warnings
Open

fix: replace deprecated asyncio event loop patterns#877
mangelajo wants to merge 5 commits into
jumpstarter-dev:mainfrom
mangelajo:fix/asyncio-deprecation-warnings

Conversation

@mangelajo

Copy link
Copy Markdown
Member

Summary

Audited the entire codebase for deprecated asyncio event loop patterns that emit DeprecationWarning on Python 3.12/3.13 and will break on Python 3.14. Found and fixed three locations (the SNMP driver had the same issue and was already fixed in #775).

Compatibility matrix

Deprecated pattern 3.12 3.13 3.14 Replacement (since 3.7)
get_event_loop() (no running loop) DeprecationWarning DeprecationWarning RuntimeError get_running_loop()
set_event_loop() DeprecationWarning DeprecationWarning No-op asyncio.run()
asyncio.Event() (no running loop) DeprecationWarning DeprecationWarning RuntimeError Create inside async def
ensure_future() DeprecationWarning DeprecationWarning DeprecationWarning create_task()

Changes

1. TFTP driver (HIGH — will crash on 3.14)

jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py

_start_server() ran in a thread using new_event_loop() + set_event_loop() + run_until_complete(), and constructed TftpServer (which creates asyncio.Event() objects) before the loop was running.

Replaced with asyncio.run() and moved TftpServer construction into an async method so asyncio.Event() objects are created with a running loop.

2. Shell driver (MEDIUM — deprecation warnings on 3.12/3.13)

jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py

Replaced asyncio.get_event_loop().time() with asyncio.get_running_loop().time() (called from async context, so trivial swap).

3. mitmproxy example (LOW — deprecated but functional)

jumpstarter-driver-mitmproxy/examples/addons/data_stream_websocket.py

Replaced asyncio.ensure_future() with asyncio.create_task().

Audit scope

Searched all Python packages for get_event_loop, new_event_loop, set_event_loop, run_until_complete, ensure_future, and asyncio.Event()/Queue()/Lock() created outside async contexts. The rest of the codebase already uses the correct modern APIs.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@mangelajo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3b2fe7ca-501e-4d93-bed6-141527898260

📥 Commits

Reviewing files that changed from the base of the PR and between 00444ee and 27b5a4f.

📒 Files selected for processing (4)
  • python/packages/jumpstarter-driver-mitmproxy/examples/addons/data_stream_websocket.py
  • python/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
  • python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py
  • python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py
📝 Walkthrough

Walkthrough

The changes modernize asyncio usage across the Mitmproxy, shell, and TFTP drivers. TFTP server lifecycle management now uses asyncio.run, with new tests covering startup, cleanup, error handling, and async lifecycle behavior.

Changes

Asyncio driver updates

Layer / File(s) Summary
Telemetry task creation
python/packages/jumpstarter-driver-mitmproxy/examples/addons/data_stream_websocket.py
Both telemetry injection paths now use asyncio.create_task for _push_telemetry.
Running-loop timeout tracking
python/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
Inline shell timeout measurement now uses asyncio.get_running_loop().time().
Async TFTP server lifecycle
python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py, python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py
Server startup uses asyncio.run; lifecycle setup and cleanup move into _run_server_lifecycle, with tests covering startup, cleanup, failures, and async execution.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Poem

A rabbit hops through async streams,
With tidy tasks and loop-time dreams.
The TFTP server starts just right,
Shell clocks tick in running light.
“Hop-hop!” says Bun—the code is bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing deprecated asyncio event loop patterns.
Description check ✅ Passed The description matches the changeset and correctly explains the TFTP, shell, and mitmproxy asyncio updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py`:
- Around line 77-94: Handle setup failures in _run_server_lifecycle by capturing
exceptions from asyncio.get_running_loop() and TftpServer construction in a
startup-error field, then always set _loop_ready so waiting callers are
unblocked. Update start() to check this field after _loop_ready.wait() and raise
the original exception instead of reporting the generic timeout; preserve
cleanup of _loop in the lifecycle finally block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c59dc39b-ba1e-46de-a0b1-7bb3b1ff868d

📥 Commits

Reviewing files that changed from the base of the PR and between de9f4fd and 7b03502.

📒 Files selected for processing (3)
  • python/packages/jumpstarter-driver-mitmproxy/examples/addons/data_stream_websocket.py
  • python/packages/jumpstarter-driver-shell/jumpstarter_driver_shell/driver.py
  • python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py`:
- Line 1: Remove the unused asyncio import from driver_test.py; no other changes
are needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df5109f8-b7a7-4540-883c-2b0225cac98d

📥 Commits

Reviewing files that changed from the base of the PR and between 7b03502 and a111faf.

📒 Files selected for processing (1)
  • python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py

Comment thread python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py Outdated
Comment on lines +80 to +81
This ensures asyncio.Event() objects inside TftpServer are created
with a running event loop, as required by Python 3.14+.

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.

Dumb question maybe, but is this true? The docs say the loop parameter was removed from asyncio.Event in 3.10 and I thought the loop gets picked up lazily on first await since then, so creating it without a running loop should be OK even on 3.14? https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the review, reading the docs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right! I verified from the CPython 3.14 source:

  • Event.__init__() only does self._waiters = collections.deque() and self._value = False — no loop access
  • The _LoopBoundMixin._get_loop() (which calls events._get_running_loop()) is only invoked lazily inside Event.wait() when self._get_loop().create_future() is called
  • The loop parameter was removed in 3.10, and since then Event() does lazy loop binding on first await

So creating asyncio.Event() outside a running loop is fine on 3.14+. The actual motivation for this refactor is replacing the deprecated new_event_loop() + set_event_loop() + run_until_complete() pattern with asyncio.run()get_event_loop() raises RuntimeError on 3.14 when no loop is running, and the old manual lifecycle code is replaced by asyncio.run() which handles shutdown_asyncgens() and loop.close() automatically.

Updated the docstrings to reflect the correct motivation in the latest commit.

…ompatibility

- TFTP driver: replace new_event_loop()/set_event_loop()/run_until_complete()
  with asyncio.run(), and move TftpServer construction into async context
  so asyncio.Event() objects are created with a running loop (required by 3.14)
- Shell driver: replace get_event_loop() with get_running_loop() (called
  from async context, but get_event_loop() is deprecated since 3.10)
- mitmproxy example: replace ensure_future() with create_task()
Tests for _start_server, _run_server_lifecycle, and the error handling
path to cover the new asyncio.run() based server startup code.
Address review comments on PR jumpstarter-dev#877:

1. Surface startup errors (CodeRabbit): If TftpServer construction
   fails inside _run_server_lifecycle, the error is now captured in
   _startup_error and _loop_ready is always set via finally, so
   start() returns promptly with the real cause instead of blocking
   for 5s and raising a generic timeout.

2. Correct docstrings (mmahut): asyncio.Event() does NOT require a
   running loop at construction time on Python 3.14 — the loop is
   bound lazily on first await via _LoopBoundMixin. Updated docstrings
   to accurately state the motivation: replacing the deprecated
   new_event_loop() + set_event_loop() + run_until_complete() pattern
   with asyncio.run().

3. Add test for startup error surfacing path.
@mangelajo
mangelajo force-pushed the fix/asyncio-deprecation-warnings branch from 00444ee to 27b5a4f Compare July 17, 2026 11:27

@mangelajo mangelajo left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed all review comments:

  1. CodeRabbit — startup error surfacing: Setup code in _run_server_lifecycle is now wrapped in try/except/finally so _loop_ready.set() always fires. Errors are captured in _startup_error and start() re-raises them as TftpError with the real cause chained. Added test_tftp_start_surfaces_startup_error to cover this.

  2. mmahut — asyncio.Event() docstring correction: Verified from CPython 3.14 source that Event.__init__() does no loop access (lazy binding via _LoopBoundMixin). Updated docstrings to reflect the correct motivation: replacing deprecated new_event_loop() + set_event_loop() + run_until_complete() patterns.

  3. Rebased onto current main (was 17 commits behind).

All 25 TFTP tests pass, lint clean.

self._loop = asyncio.get_running_loop()
self.server = TftpServer(
host=self.host,
port=self.port,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed CodeRabbit's suggestion: setup code is now wrapped in try/except/finally so _loop_ready.set() always fires. If TftpServer() construction fails, the error is captured in _startup_error and start() re-raises it as a TftpError with the real cause chained — no more waiting 5s for a generic timeout.

Added test_tftp_start_surfaces_startup_error to cover this path.

Comment on lines +80 to +84

Uses asyncio.run() instead of the deprecated new_event_loop() +
set_event_loop() + run_until_complete() pattern, which emits
DeprecationWarning on Python 3.12/3.13 and breaks on 3.14
(get_event_loop() raises RuntimeError when no loop is running).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed mmahut's review: the previous docstring incorrectly claimed asyncio.Event() needs a running loop on Python 3.14+. Verified from CPython 3.14 source that Event.__init__() does no loop access — the loop is bound lazily via _LoopBoundMixin._get_loop() on first await.

Updated the docstring to state the correct motivation: replacing the deprecated new_event_loop() + set_event_loop() + run_until_complete() pattern with asyncio.run().

Add --cov and coverage source config to the TFTP package's
pyproject.toml so that pytest-cov generates coverage.xml with
correct source paths. Without this, diff-cover cannot map the
TFTP driver's coverage data to the git diff, making all changed
lines appear uncovered.
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.

2 participants