Skip to content

Defer async client channel creation until first use - #226

Merged
andystaples merged 3 commits into
mainfrom
andystaples-fix-ci-flake
Jul 29, 2026
Merged

Defer async client channel creation until first use#226
andystaples merged 3 commits into
mainfrom
andystaples-fix-ci-flake

Conversation

@andystaples

Copy link
Copy Markdown
Contributor

Summary

Fixes a Python 3.13 failure where constructing AsyncTaskHubGrpcClient or DurableFunctionsClient could raise:

RuntimeError: There is no current event loop in thread 'MainThread'.

This surfaced in the Azure Functions CI for #220 after an async test ran between two synchronous durable-client construction tests.

Root cause

AsyncTaskHubGrpcClient eagerly created its grpc.aio channel in __init__. On Python 3.13, pytest-asyncio clears the current event loop after an async test. The next synchronous client construction therefore asked gRPC to bind a channel when no current loop existed.

The same sequence is possible outside tests when an async client is constructed after asyncio.run() or otherwise outside the event loop that will perform its RPCs. Creating a replacement default loop would only hide the exception and could bind the channel to the wrong loop.

Fix

  • Defer SDK-owned grpc.aio channel and stub creation until the first RPC.
  • Create them synchronously within that async operation, binding the channel to the loop that actually uses it.
  • Keep caller-provided channels eager and unchanged.
  • Make closing a never-used client safe and preserve channel recreation behavior.
  • Add regression coverage for construction with no current event loop followed by use under asyncio.run().
  • Document the user-visible fix in the core and Azure Functions changelogs.

Validation

  • Core unit suite: 1027 passed, 1 skipped.
  • Azure Functions unit suite: 211 passed.
  • Core/Azure Managed and Azure Functions pyright checks passed.
  • Flake8 passed for the changed core source and tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7bc5f597-1596-460d-bfe6-738676344ef0
Copilot AI review requested due to automatic review settings July 29, 2026 04:24

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.

Pull request overview

This PR fixes a Python 3.13 compatibility issue where constructing AsyncTaskHubGrpcClient (and dependents like DurableFunctionsClient) could fail with RuntimeError: There is no current event loop in thread 'MainThread' by deferring grpc.aio channel + stub creation until the first async RPC.

Changes:

  • Deferred SDK-owned async gRPC channel/stub creation in AsyncTaskHubGrpcClient until first RPC via a new lazy _get_stub() helper, and made close() safe when the client was never used.
  • Updated async-client construction tests to create the channel under an active event loop and added a regression test covering “construct with no current loop, then use under asyncio.run()”.
  • Documented the user-visible fix in the core SDK and Azure Functions provider changelogs.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
durabletask/client.py Lazily creates the SDK-owned grpc.aio channel/stub on first RPC and guards close/recreate paths when the channel was never created.
tests/durabletask/test_client.py Adjusts construction tests for lazy initialization and adds a regression test for “no current event loop” construction.
CHANGELOG.md Adds an Unreleased FIXED entry for the core SDK describing the construction/runtime loop fix.
azure-functions-durable/CHANGELOG.md Adds an Unreleased FIXED entry for the Functions durable client construction failure after a loop was closed/cleared.

@berndverst berndverst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One low-severity documentation requirement remains; the lazy async-channel implementation itself looks sound.

Comment thread CHANGELOG.md
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7bc5f597-1596-460d-bfe6-738676344ef0
Copilot AI review requested due to automatic review settings July 29, 2026 05:07

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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7bc5f597-1596-460d-bfe6-738676344ef0
Copilot AI review requested due to automatic review settings July 29, 2026 05:11

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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

tests/durabletask/test_client.py:229

  • install_resilient_test_stubs() now forces async clients to create a stub by calling client._get_stub(). If a recreate task runs after client.close() has set _closing=True (e.g., in close/recreate race tests), _get_stub() raises RuntimeError("The client is closed"), which then gets logged as a recreate failure. Guarding this helper against _closing avoids spurious exceptions/log noise during shutdown while still ensuring the stub exists for normal resiliency tests.
    def wrap_if_needed():
        if is_async:
            client._get_stub()
        if not isinstance(client._stub, wrapper_cls):
            client._stub = wrapper_cls(client._stub, client._resiliency_interceptor)

@berndverst berndverst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The Azure Managed changelog requirement is addressed. I rechecked the follow-up commit, resolved thread, effective full diff, and unchanged lazy-channel implementation; no actionable issues remain.

@andystaples
andystaples merged commit eec8c55 into main Jul 29, 2026
25 checks passed
@andystaples
andystaples deleted the andystaples-fix-ci-flake branch July 29, 2026 15:00
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.

3 participants