Skip to content

fix: close Janus queues during client shutdown - #1258

Merged
Carter Tinney (cartertinney) merged 1 commit into
mainfrom
agents/janus-2x-lifecycle
Sep 2, 2026
Merged

Carter Tinney (cartertinney) merged 1 commit into
mainfrom
agents/janus-2x-lifecycle

Conversation

@cartertinney

Copy link
Copy Markdown
Member

Summary

  • construct Janus 2 queues without an event-loop round trip
  • close every fixed and dynamically created async inbox after its consumers stop
  • require Janus 2.x explicitly and update the lockfile

Testing

  • uv run --frozen pytest tests/unit/iothub/aio/test_async_inbox.py tests/unit/iothub/aio/test_async_clients.py tests/unit/iothub/test_inbox_manager.py -W error::RuntimeWarning -q (790 passed, 3 skipped)
  • targeted Ruff and Black checks

Construct Janus 2 queues without an event-loop round trip and close every fixed and dynamic inbox after its consumers stop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@cartertinney Carter Tinney (cartertinney) changed the title async: close Janus queues during client shutdown fix: close Janus queues during client shutdown Sep 1, 2026

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.

🟢 Approval recommended

The implementation matches Janus 2 lifecycle semantics and includes focused regression coverage.

Pull request overview

Updates async client shutdown to properly release Janus queue resources.

Changes:

  • Uses Janus 2’s loop-independent queue construction.
  • Closes fixed and dynamic inboxes after consumers stop.
  • Requires Janus 2.x and adds shutdown coverage.
File summaries
File Description
pyproject.toml Requires Janus 2.x.
uv.lock Records the Janus version constraint.
async_inbox.py Simplifies construction and adds queue shutdown.
async_clients.py Closes all inboxes during client shutdown.
inbox_manager.py Exposes all managed inboxes.
test_async_inbox.py Tests construction and queue closure.
test_async_clients.py Tests closure of fixed and dynamic inboxes.
Review details
  • Files reviewed: 6/7 changed files
  • Comments generated: 0
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@cartertinney
Carter Tinney (cartertinney) merged commit 99cae03 into main Sep 2, 2026
42 checks passed
@cartertinney
Carter Tinney (cartertinney) deleted the agents/janus-2x-lifecycle branch September 2, 2026 04:11
Carter Tinney (cartertinney) added a commit that referenced this pull request Sep 2, 2026
## Summary

- serialize first-time creation of the three shared async client event
loops
- preserve lock-free access after a loop has been published
- synchronize test-only loop cleanup with loop creation
- add concurrent initialization and Janus loop-affinity regression
coverage

## Root cause

[Python E2E build
163209](https://dev.azure.com/azure-iot-sdks/f9b79625-2860-4d92-a4ee-57b03fabfd10/_build/results?buildId=163209)
timed out in the first async C2D test on Windows/Python 3.10. Message
delivery succeeded, but the log showed `CLIENT_INTERNAL_LOOP` being
created twice by concurrent first callers.

The loop getters used an unsynchronized check-then-create. After #1258
stopped constructing each Janus queue through the internal loop, the
loop was no longer guaranteed to be initialized before the handler
runner and feature-enablement paths started concurrently. The queue
could bind to the first loop while the second loop replaced the global
reference. Every subsequent queue receive then failed Janus's
loop-affinity check, and the handler manager's immediate restart
behavior turned the failure into a tight restart loop until pytest timed
out.

## Why this shape

The process-wide loops are intentionally shared by clients to isolate
internal work, handler runners, and user coroutine handlers. Moving to
per-client loops would require broader ownership and shutdown changes
while leaving lazy initialization subject to the same race. Eager
creation would unconditionally start three daemon threads. A
centralized, double-checked creation lock restores the existing
singleton invariant at its owner and keeps the established hot path
lock-free.

## Why prior review missed it

#1258's reviews and tests focused on Janus resource closure and removing
an unnecessary event-loop round trip during queue construction. Its unit
tests verified sequential construction, queue operations, and shutdown,
and its complete E2E matrix passed. The older loop manager only asserted
that repeated sequential calls returned the same loop; it had no
concurrent-first-access coverage.

The change therefore exposed a pre-existing race rather than introducing
an obviously incorrect Janus operation. It requires two independent
first consumers to enter a narrow scheduling window, which did not occur
in #1258's runs and appeared later in one Windows job. This PR makes
that concurrency contract explicit and deterministic in tests.

## Validation

- regression demonstrated before the fix: all three loop getters created
two loops under synchronized concurrent first access
- Python 3.10 focused async lifecycle suite: 153 passed
- Python 3.14 related async lifecycle suite: 933 passed, 3 skipped
- full unit suite: 5445 passed, 6 skipped
- concurrency regression repeated 20 times
- package sdist and wheel build succeeded
- Black and Ruff passed
- independent design and code reviews completed

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

4 participants