feat: Add async event processor - #472
Conversation
e7d9e77 to
0c83486
Compare
2230024 to
5ed9484
Compare
| """A fixed-size pool of concurrent tasks that rejects jobs when its limit | ||
| is reached. Matches the contract of | ||
| ``ldclient.impl.fixed_thread_pool.FixedThreadPool``.""" | ||
| class BoundedTaskSet: |
There was a problem hiding this comment.
The AsyncWorkerPool was previously created to mirror the FixedThreadPool but some of the naming and params didn't seem to align with the async code.
| self._diagnostic_flush_workers.stop() | ||
| await self._diagnostic_flush_workers.wait() | ||
|
|
||
| await self._http.close() |
There was a problem hiding this comment.
Stop can drop pending events
Medium Severity
stop promises to deliver all pending events, but _do_shutdown never flushes the outbox. The pre-stop flush() can be dropped when the inbox is full, or leave events buffered when flush workers are saturated—the same case flush_and_wait already retries—so those events are abandoned on shutdown.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 016e434. Configure here.
| log.debug('Sending events payload: ' + json_body) | ||
| payload_id = str(uuid.uuid4()) |
There was a problem hiding this comment.
would this leak redacted properties?
| elif message.type == 'stop': | ||
| await self._do_shutdown() | ||
| message.param.set() | ||
| return |
There was a problem hiding this comment.
If _doShutdown throws then it will go into the catch block, but that would not break out of the loop.
|
Shutdown event-delivery parity (sync vs. async) — decision needed While addressing the The proposed async fix is to have The catch: this gap exists identically in the sync Question: should we fix the sync side too, and if so —
(Leaning toward keeping this PR async-scoped and handling sync separately, but flagging for a call before applying.) |
016e434 to
8d0005a
Compare
Implement the new AsyncEventProcessor interface with a concrete DefaultAsyncEventProcessor, matching the sync DefaultEventProcessor naming convention.
Triggers a flush and awaits delivery via a new inbox message, returning whether it completed within the timeout.
The async event delivery concurrency limiter no longer mirrors the sync FixedThreadPool's shape. BoundedTaskSet drops the unused name parameter and the thread vocabulary, reserves a slot synchronously at spawn (so a full set rejects rather than queues), and uses a done-callback plus asyncio.gather for cleanup and draining.
Remove the banner-style section headings; where they carried a useful note, capture it as a short class docstring instead.
…ction comments Rename drain() back to wait() (it awaits the in-flight tasks; it does not halt intake), simplify the job param to Callable[[], Coroutine], and remove banner-style section-heading comments from the async event/concurrency tests.
…cessors Move the event schema version constant into event_processor_common so both processors advertise the same X-LaunchDarkly-Event-Schema and can't drift.
_trigger_flush now returns whether it handed the batch to a worker; the flush_and_wait handler retries (waiting for a free worker) until the batch is handed off, so a saturated worker pool no longer causes a false success.
2a10924 to
623faa0
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 623faa0. Configure here.
| message.param.set() | ||
| return | ||
| except Exception: | ||
| log.error('Unhandled exception in event processor', exc_info=True) |
There was a problem hiding this comment.
Shutdown exception hangs stop forever
High Severity
If _do_shutdown raises, the outer except logs it and continues the main loop without setting the stop reply or returning. stop then waits forever on that reply, so shutdown never completes.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 623faa0. Configure here.


Adds the async analytics event delivery component for the async SDK client, extracted from the async SDK implementation branch (SDK-60).
Stacked on #471 (AsyncConfig) — review that first. Until #471 merges this PR shows its commits too; after merge a rebase drops them out.
What's here
AsyncEventProcessorinterface +DefaultAsyncEventProcessorconcrete implementation, mirroring the syncEventProcessor/DefaultEventProcessorsplit.send_event/flushare sync;stopis a coroutine.event_processor_common.pyso the sync and async processors share buffering, output formatting, and dispatch logic.DefaultEventProcessorsuite over an injected mock aiohttp session.SDK-2769
feat: Add async event processor
Note
Medium Risk
New async path delivers analytics to LaunchDarkly and refactors shared event-handling used by sync; behavior is intended to match sync and is heavily tested, but delivery and disable-on-error semantics remain business-critical.
Overview
Introduces async analytics event delivery for the async SDK:
DefaultAsyncEventProcessorand an asyncio-basedEventDispatcher(inbox loop, bounded concurrent flushes, diagnostics, gzip/retry HTTP) mirror the sync processor while exposing syncsend_event/flushand asyncstop/flush_and_wait.Shared dispatch logic moves into
event_processor_commonasEventDispatcherBase(_process_event, index dedup, debug sampling,_handle_responsedisable-on-unrecoverable-error) plus a singleCURRENT_EVENT_SCHEMA; the syncEventDispatchernow subclasses that base instead of duplicating ~100 lines.Async concurrency for flush workers switches from
AsyncWorkerPooltoBoundedTaskSet(try_runrejects when full); asyncflush_and_waitretries flush until a worker accepts the batch. A large test suite parallels the syncDefaultEventProcessortests over a mock aiohttp session.Reviewed by Cursor Bugbot for commit 623faa0. Bugbot is set up for automated code reviews on this repo. Configure here.