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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bcf6a843-65d7-4f47-a0e4-784dbdf2aa23

📥 Commits

Reviewing files that changed from the base of the PR and between 00444ee and 8daf76f.

📒 Files selected for processing (5)
  • 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
  • python/packages/jumpstarter-driver-tftp/pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/packages/jumpstarter-driver-mitmproxy/examples/addons/data_stream_websocket.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, propagates startup errors, clears loop state, and adds tests plus coverage configuration.

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 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 and validation
python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py, python/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver_test.py, python/packages/jumpstarter-driver-tftp/pyproject.toml
TFTP startup uses asyncio.run, records and surfaces startup failures, signals readiness, clears loop state, and adds lifecycle tests with coverage reporting.

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

Sequence Diagram(s)

sequenceDiagram
  participant TftpStart
  participant ServerThread
  participant TftpLifecycle
  participant TftpServer
  TftpStart->>ServerThread: start server thread
  ServerThread->>TftpLifecycle: asyncio.run lifecycle
  TftpLifecycle->>TftpServer: construct server
  TftpLifecycle-->>TftpStart: signal readiness
  TftpLifecycle->>TftpServer: await server lifecycle
  TftpStart->>TftpStart: raise TftpError on startup failure
Loading

Possibly related PRs

Poem

A bunny hops through running loops,
While tidy tasks replace old groups.
TFTP starts, reports, and ends,
Shell clocks tick as timeout bends.
Async carrots for all my friends!

🚥 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 clearly matches the changeset and describes the same asyncio compatibility fixes.
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.

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.

fixed ':D

…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().

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.

Thank you for confirming, this was just a nitpick comment really!

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.
@mangelajo
mangelajo requested a review from mmahut July 20, 2026 13:25
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