Skip to content

feat: Accept synchronous hooks on the async client - #548

Merged
jsonbailey merged 3 commits into
mainfrom
jb/sdk-3077/sync-hooks-async-client
Nov 2, 2026
Merged

jsonbailey merged 3 commits into
mainfrom
jb/sdk-3077/sync-hooks-async-client

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

feat: Remove the experimental AsyncHook class

Warning

Breaking change to the experimental async API. Code that subclasses AsyncHook or passes AsyncHook instances to AsyncLDClient must change to use ldclient.hook.Hook. The async API is experimental, so this is not marked with ! and does not need a major version. Edit the release notes to call out the break for async users.

Summary

AsyncLDClient now uses the same ldclient.hook.Hook as LDClient, so existing integrations such as the OpenTelemetry tracing hook work with the async client. Before this change, the async client accepted only AsyncHook and ignored sync hooks.

  • The async client runs the hook stages synchronously in the caller's task. A span or context variable set in the before stage stays current until the after stage.
  • AsyncHook is removed. The async API is experimental.
  • If an evaluation is cancelled or raises, the after stages still run, with an error detail and the default value. The exception is then re-raised. A tracing hook can then end its span.
  • AsyncPlugin keeps its async register(). Its get_hooks() now returns Hook instances.
  • AsyncConfig and add_hook() ignore items of the wrong type, the same as the sync client.
  • The async contract-test service uses the sync PostingHook.

Note

Overview
Breaking change for the experimental async client: AsyncHook is removed. AsyncLDClient and AsyncConfig now take the same Hook type as LDClient, so shared integrations (e.g. OpenTelemetry) work without a separate async hook API.

Hook before/after stages run synchronously in the caller’s task (not awaited). Context set in before_evaluation stays visible through the evaluation and is cleared in after_evaluation, which matters for contextvars and tracing under concurrent variation() calls.

When evaluation raises or is cancelled, after hooks still run with an error EvaluationDetail (default value + EXCEPTION reason) before the exception propagates, so hooks can tear down spans/context.

add_hook() and config hooks filtering match the sync client (ignore non-Hook types instead of raising on sync hooks). AsyncPlugin.get_hooks() returns Hook instances; contract tests drop AsyncPostingHook and register PostingHook via config.

Reviewed by Cursor Bugbot for commit 17e8352. Bugbot is set up for automated code reviews on this repo. Configure here.

AsyncLDClient now takes the same ldclient.hook.Hook as LDClient, so
integrations such as the OpenTelemetry tracing hook work with it. The
hook stages run synchronously in the caller's task, so a span made
current in the before stage stays current until the after stage.

AsyncHook is removed. AsyncPlugin keeps its async register(), and its
get_hooks() returns Hook instances. AsyncConfig now raises TypeError
for a hook that is not a Hook or a plugin that is not an AsyncPlugin,
which matches add_hook().
Match the sync client: AsyncConfig and add_hook() ignore items of the wrong type. The async contract-test service uses the sync PostingHook. Remove redundant docstring notes.
The after stages get an error detail with the default value, and the exception is re-raised. A hook such as the OpenTelemetry tracing hook can then detach its context and end its span.
@jsonbailey
jsonbailey force-pushed the jb/sdk-3077/sync-hooks-async-client branch from 915b8d6 to 17e8352 Compare November 2, 2026 14:24
@jsonbailey
jsonbailey marked this pull request as ready for review November 2, 2026 15:06
@jsonbailey
jsonbailey requested a review from a team as a code owner November 2, 2026 15:06
@jsonbailey
jsonbailey merged commit 2457311 into main Nov 2, 2026
18 checks passed
@jsonbailey
jsonbailey deleted the jb/sdk-3077/sync-hooks-async-client branch November 2, 2026 16:33
jsonbailey added a commit that referenced this pull request Nov 2, 2026
🤖 I have created a release *beep* *boop*
---


##
[9.19.0](9.18.4...9.19.0)
(2026-11-02)


### Breaking changes to the experimental async client

The async client is experimental, so these changes do not cause a major
version bump. If you use `AsyncLDClient` with hooks or plugins, update
your code:

* Remove the experimental `AsyncHook` class. The async client now uses
the same synchronous `Hook` class as the sync client. Change each
`AsyncHook` subclass to subclass `Hook`, and change its `async def`
stages to `def`. The stages run on the event loop, so they must not
block.
([#548](#548))
([2457311](2457311))
* `AsyncPlugin.get_hooks` now returns a list of `Hook` instances.
`AsyncPlugin.register` stays async.
([#548](#548))
([2457311](2457311))


### Features

* Accept synchronous hooks on the async client
([#548](#548))
([2457311](2457311))


### Bug Fixes

* Run after-evaluation hooks when an async evaluation is cancelled or
raises
([2457311](2457311))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> **Release 9.19.0** — bumps the package version from **9.18.4** to
**9.19.0** in `pyproject.toml`, `ldclient/version.py`,
`.release-please-manifest.json`, and the provenance verification example
in `PROVENANCE.md`, and adds the **9.19.0** section to `CHANGELOG.md`.
> 
> What ships in this release (documented in the new changelog entry, not
in this diff’s code):
> 
> The **experimental async client** unifies on the sync **`Hook`** API:
**`AsyncHook` is removed**, **`AsyncPlugin.get_hooks`** returns
**`Hook`** instances ( **`AsyncPlugin.register`** stays async), and the
async client **accepts synchronous hooks**. Hook stage methods should be
plain **`def`** (non-blocking on the event loop). A bug fix ensures
**after-evaluation hooks run** when an async evaluation is **cancelled
or raises**.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
54a6f6a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: jsonbailey <jbailey@launchdarkly.com>
kinyoklion added a commit that referenced this pull request Nov 12, 2026
Conflicts in the capability lists of the contract test services keep both sides: flag-overrides from feat/overrides, and the retry conformance, service endpoints, wrapper, proxy and TLS capabilities from main. The async override test that captured evaluation details used AsyncHook, which #548 removed from main; it now uses Hook, which the async client accepts.
kinyoklion added a commit that referenced this pull request Nov 12, 2026
<!-- TERMAG_PR_BODY_BEGIN -->
BEGIN_COMMIT_OVERRIDE
feat: Add experimental support for flag overrides, read from local JSON
or YAML files or supplied by a custom override source
feat: Mark evaluations that a flag override affected with
`overrideAffected` in the evaluation reason, and count them under
separate summary counters
fix: Stop the components that already started when the client fails to
start
END_COMMIT_OVERRIDE

Every change in this PR was already reviewed and merged through its own
PR against `feat/overrides`: #525, #526, #527, #528, #529, #530, #572,
#574, #576, #577. This PR merges the branch into `main`.

Flag overrides let an operator force flags and segments to known
definitions on a running client, whether or not the client can reach
LaunchDarkly, and remove the override when the incident is over. They
are an option of the FDv2 data system, on both `LDClient` and
`AsyncLDClient`, with a file-based source in
`ldclient.integrations.overrides.FileOverrideSourceBuilder`, and are
experimental.

The one change outside the feature: #527 stops the components that had
already started when the data system fails to start, so a failed client
construction no longer leaves threads behind.

`main` was merged into `feat/overrides` in 46e5f0e, so the branch has
every `main` commit. That merge resolved two conflicts and made one
follow-up change:
- The capability lists of both contract test services keep both sides:
`flag-overrides` from this branch, and the retry conformance, service
endpoints, wrapper, proxy and TLS capabilities from `main`.
- One async override test used `AsyncHook`, which #548 removed from
`main`. It now uses `Hook`, which the async client accepts.

Before the push:
- the unit test suite passed (2,357 tests, without the Redis, DynamoDB
and Consul store tests, which need servers);
- `mypy`, `isort` and `pycodestyle` were clean;
- the contract test harness's `flag overrides` suite passed against both
the sync and the async test service (88 tests each).
<!-- TERMAG_PR_BODY_END -->

<!-- termag: box=i-0575d1c54853058d7 project=python-server-sdk -->
<sub>Opened from termag</sub>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Overview**
> Adds **experimental flag overrides** so operators can force known
flag/segment definitions at runtime (via local JSON/YAML files through
`FileOverrideSourceBuilder`, or a custom `OverrideSource`), integrated
into the FDv2 data system for sync and async clients through
`datasystem_config.override_source` / `ConfigBuilder.overrides()`.
> 
> The data system now maintains an **override layer** merged over the
normal store, serves override-only flags when LaunchDarkly data is not
ready, and fans out **dependency-aware flag-change notifications** when
LD data affects overridden prerequisites/segments. Evaluations propagate
an **`overrideAffected`** reason marker; analytics treat those
evaluations differently (summary counters only, `all_flags_state`
disables per-flag tracking for them).
> 
> Contract test services advertise **`flag-overrides`** and build
override sources from harness config; docs add
`ldclient.integrations.overrides`.
> 
> Also **fixes client startup**: if the data system fails to start,
already-started components are stopped so failed construction does not
leak threads.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46e5f0e. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
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