Skip to content

feat(insight): add async export scheduling - #702

Open
wangyb-A wants to merge 6 commits into
mainfrom
feat/insight-async-export
Open

feat(insight): add async export scheduling#702
wangyb-A wants to merge 6 commits into
mainfrom
feat/insight-async-export

Conversation

@wangyb-A

@wangyb-A wangyb-A commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • move Workflow Insight rendering, truncation, export, and flush off the checkpoint thread
  • coalesce cumulative snapshots with one lazy daemon lane per exporter
  • drain final records and flush exporters with one shared configurable timeout
  • isolate exporter failures and bound pending state without failing workflows

Tracks #687.

Design

  • one lazy daemon worker per exporter
  • one in-flight record and latest pending record per execution ARN
  • FIFO fairness across execution ARNs
  • export_timeout_seconds defaults to 5 seconds and bounds drain plus flush
  • workers stop when idle; blocked workers are retained and never replaced
  • no core SDK changes

Validation

  • 113 Insight tests passed, including local-runner e2e
  • scheduler and async subset passed 12 consecutive runs
  • mypy passed
  • Ruff lint and format passed
  • wheel and sdist built
  • full repository collection: 3537 tests, no errors
  • Workflow Insight conformance PR feat: support CONTEXT operations in CheckpointedResult.create_from_op… #73: CloudWatch 18/18 and S3 18/18 (unchanged; record content, ordering, and timeout are unaffected by these changes)

Review decisions

Iteration Finding Decision Reason Validation
1 Deepcopy failure could alias records across lanes Fixed Violated exporter isolation Copy-failure and no-alias tests
1 Deepcopy fallback lacked tests Fixed Regression would be silent Focused tests repeated
1 _inflight_arn was dead state Fixed Misleading and unused Full suite and mypy
1 Bounded deque scans are O(n) Declined Queue capped at 1024; rewrite adds risk Cap and fairness tests
1 Warm timeout behavior lacked coverage Fixed Required approved behavior Deterministic A/B FIFO test
2 Global thread-count assertion could flake Fixed Daemon exit could alter baseline Lane-local checks; subset 12x
2 Product concurrency and lifecycle Accepted No product defects found Full reviewer trace
3 Full implementation after fixes Accepted No actionable findings Final reviewer pass
3 Timing assertions under severe host load Accepted risk Wide margins; 12 repeats passed Subset 12x
3 Shared flush can publish another execution buffer early Accepted by design Non-lossy shared lifecycle behavior Warm-container tests
public Cancelled flush barriers could accumulate behind a blocked exporter Fixed A stale barrier per warm invocation grew queue/barrier state without bound Repeated-timeout test + queued/already-popped cancellation-race tests; scheduler subset 12x; CloudWatch 18/18 and S3 18/18
public Lane used the default reentrant RLock Fixed Replaced with an explicit non-reentrant Lock; the lane never re-acquires _cond while holding it, so recursion support is unneeded and misuse now fails loudly Full insight suite; mypy; ruff; scheduler subset 12x
public Same exporter instance could be configured more than once Fixed WorkflowInsightConfig now rejects a duplicate exporter instance (by object identity, not equality/hash) with a clear ValueError; preserves one-thread-per-distinct-instance safety and avoids duplicate, timing-dependent scheduling. Distinct same-class instances and the default exporter are unaffected Tests: same instance twice raises; two distinct instances each get a lane; default exporter unaffected; full insight suite; mypy; ruff
public _ExecutionState.scheduled was read/written without the plugin lock Fixed (defensive) Route the flag through _lock via _mark_scheduled/_was_scheduled, consistent with the other state fields; the lock is released before any scheduler/end_invocation or exporter work, so no new lock ordering or deadlock. SDK serializes hooks, so scope is minimal Scheduled-flag and no-op-invocation tests; scheduler subset 12x

Reviewed three times with commit-code-reviewer, plus two post-public-review passes addressing the rows above. No actionable findings remain.

@wangyb-A
wangyb-A force-pushed the feat/insight-async-export branch from 0bbf504 to 6b73e82 Compare September 2, 2026 18:43
@wangyb-A

wangyb-A commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/ai review

Comment on lines +303 to +304
if not barrier.wait(remaining):
barrier.canceled = True

This comment was marked as outdated.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@wangyb-A

wangyb-A commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/ai review

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Codex AI review

No actionable findings. Residual risk remains around real Lambda freeze/thaw behavior with exporters that stay blocked beyond the configured timeout.

Reviewed commit 6c723bae2bb1b08ff732a776ba3f1b36bc1b77c5. Workflow run

@github-actions

This comment has been minimized.

@wangyb-A
wangyb-A marked this pull request as ready for review September 3, 2026 18:35
@wangyb-A
wangyb-A temporarily deployed to ai-pr-review-runtime September 3, 2026 18:36 — with GitHub Actions Inactive
@wangyb-A
wangyb-A deployed to ai-pr-review-runtime September 3, 2026 18:52 — with GitHub Actions Active
@wangyb-A
wangyb-A had a problem deploying to ai-pr-review-runtime September 3, 2026 18:52 — with GitHub Actions Failure
Comment on lines +345 to +348
# must omit endTime/durationMs. Passing end_time=None makes _emit
# drop both fields. Output and error likewise belong only to a
# terminal record.
self._emit(
self._schedule_record(

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.

Claude AI review · Finding arf_v1_4qdr5mczjf6yvbzm4jlpoh2z7y

The comment above this call still says "Passing end_time=None makes _emit drop both fields," but _emit was split/renamed into _build_record/_schedule_record earlier in this same PR. A future maintainer grepping for _emit (or trusting this comment while modifying the schedule path) will be misled since that method no longer exists. Update the comment to reference the current method name.

Suggested change
# must omit endTime/durationMs. Passing end_time=None makes _emit
# drop both fields. Output and error likewise belong only to a
# terminal record.
self._emit(
self._schedule_record(
# must omit endTime/durationMs. Passing end_time=None makes
# _schedule_record drop both fields. Output and error likewise
# belong only to a terminal record.
self._schedule_record(

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude AI review

This PR adds an async, coalescing per-exporter export scheduler (_export_scheduler.py) for the experimental aws-durable-execution-sdk-python-insight plugin, moves rendering/truncation/export/flush off the SDK checkpoint thread, adds a shared export_timeout_seconds drain/flush deadline, and validates exporter-instance uniqueness. No core SDK files are touched.

I traced the full lane/queue/worker state machine (coalescing via _pending, FIFO fairness via _move_record_token_to_back, the pending-cap eviction, the flush-barrier cancel race against an in-flight vs. already-popped marker, and the idle-worker exit/lazy-restart cycle) against the new tests/test_export_scheduler.py and tests/test_plugin_async.py, and against prior-findings.json. All three previously-reported issues for this same feature branch — the unlocked scheduled write, the implicit RLock in _ExporterLane's Condition, and duplicate-exporter-instance sharing a lane — are fixed in this diff (_mark_scheduled/_was_scheduled under self._lock, an explicit threading.Condition(threading.Lock()), and WorkflowInsightConfig._validate_exporters rejecting a repeated instance by identity). I found no new correctness, determinism, or thread-safety regressions in the locking/queue design; the invocation-end drain reliably blocks until the flush barrier completes, so existing synchronous-looking assertions in test_plugin.py/test_config.py remain non-flaky.

Only a minor documentation nit remains (see inline comment). Residual, already-documented risk: on_invocation_end can now add up to export_timeout_seconds (default 5s) of latency to a terminal invocation's return path if an exporter is slow/blocked, and a shared flush at one execution's invocation-end can publish another execution's buffered records early on a warm container — both are explicitly called out in the PR description/README as accepted, bounded trade-offs and are covered by dedicated tests.

Reviewed commit 7ba75206bba231b072ac58a65c8b41a80c664e29. Workflow run

Yubo Wang added 6 commits September 3, 2026 21:41
Move all exporter work off the SDK checkpoint thread. A new private
_ExportScheduler owns one lazy daemon worker per exporter lane;
per-exporter copy, render, truncation, export() and flush() now run
there, so a slow exporter never blocks workflow progress.

Per lane: at most one in-flight record and one latest pending record
per execution ARN. Cumulative snapshots for the same ARN coalesce (the
in-flight record is never cancelled); updating a pending ARN moves it
to the back for FIFO fairness across ARNs; pending ARNs are capped with
oldest-eviction. A blocked worker is retained and never replaced, and
idle workers exit after the drain, so threads cannot grow unbounded.

on_operation_change returns immediately unless emit mode is on-change.
Invocation end schedules the final record, then drains and flushes the
touched lanes under one shared deadline; on timeout the workflow
response is returned and delivery degrades to best-effort. Exceptions
in render/export/flush are isolated and logged.

Add WorkflowInsightConfig.export_timeout_seconds (default 5.0),
validated as a finite number greater than zero (rejects bool, NaN,
infinity, and non-positive values).

Add scheduler, plugin-async, and config unit tests plus updated
on-change coalescing coverage; refresh the README note. No core SDK
changes.
Skip a lane record when copy.deepcopy fails instead of aliasing the
shared canonical record. The alias let this lane's truncation mutate
the object other lanes still read, breaking workflow isolation. A copy
failure is now logged through the module logger and the lane keeps
draining, matching render/truncation failure handling.

Also remove the dead _inflight_arn lane field (written, never read).

Tests: deepcopy-failure skips the record, does not call the exporter,
logs the failure, and the lane continues to export a later valid
record; a non-aliasing regression guards in-place mutation; a
warm-container cross-invocation test proves bounded invocation-end
waits, no A/B merge, and FIFO drain + flush after unblock.
Make the two shared-timeout tests wait deterministically for their
released lane workers to stop before returning, so their daemon workers
cannot exit between a later test's baseline capture and its assertion.

Replace the fragile process-global thread-count delta in
test_blocked_worker_is_not_replaced with lane-local worker identity,
aliveness, and a lane-scoped worker count. This proves the blocked lane
never spawns a replacement without depending on global thread state.

Product code is unchanged.
Cancelled flush barriers no longer pile up behind a blocked exporter.
end_invocation now pairs each barrier with its lane and, on timeout,
calls _ExporterLane.cancel_flush(barrier): under the lane lock it marks
the barrier cancelled and pulls its still-queued _FLUSH marker out,
completing it there. If the worker already popped the marker the flush
is left to the worker; an in-flight synchronous flush is not killed.
This keeps queue and barrier state bounded across many warm
invocations while preserving record ordering, normal flush, the shared
deadline, blocked-worker retention, and bounded pending state.

Also switch the lane Condition from the default RLock to an explicit
non-reentrant Lock; the lane never re-acquires _cond while holding it.

Tests: deterministic repeated-timeout test (blocked exporter across
many warm invocations) plus queued-vs-already-popped cancellation
race tests.
Reject the same exporter instance appearing more than once in
WorkflowInsightConfig.exporters with a clear ValueError, compared by
object identity (not equality/hash) during config normalization. Two
distinct instances of the same class stay valid and each keeps its own
lane; the default exporter is unaffected. Preserves the
one-thread-per-distinct-instance safety and avoids duplicate,
timing-dependent scheduling.

Route _ExecutionState.scheduled mutation and read through the plugin
_lock via _mark_scheduled/_was_scheduled, consistent with the other
state fields. The lock is released before any scheduler/end_invocation
or exporter work, so no new lock ordering or deadlock is introduced.

Add tests: same instance twice raises; two distinct same-class
instances each get a lane; default exporter unaffected; scheduled flag
tracks scheduling.
@wangyb-A
wangyb-A force-pushed the feat/insight-async-export branch from 9e0e717 to 2e7049c Compare September 3, 2026 21:41
@wangyb-A
wangyb-A requested a deployment to ai-pr-review-runtime September 3, 2026 22:17 — with GitHub Actions In progress
@wangyb-A
wangyb-A requested a deployment to ai-pr-review-runtime September 3, 2026 22:17 — with GitHub Actions In progress
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.

1 participant