Skip to content

chore(aot-smoke): cover handler exceptions, unsubscribe lifecycle, cancellation#78

Merged
MarcelRoozekrans merged 6 commits into
mainfrom
chore/aot-smoke-cover-asyncevents-paths
May 28, 2026
Merged

chore(aot-smoke): cover handler exceptions, unsubscribe lifecycle, cancellation#78
MarcelRoozekrans merged 6 commits into
mainfrom
chore/aot-smoke-cover-asyncevents-paths

Conversation

@MarcelRoozekrans

Copy link
Copy Markdown
Contributor

Summary

Closes backlog item B1. The existing aot-smoke project covered only the single-handler happy path for both Sequential + Parallel modes; this PR adds three independent assertion blocks exercising the three previously-uncovered behavioral paths under PublishAot=true:

  • Sequential exception propagation — handler1 throws → InvokeAsync rethrows → handler2 never runs. Asserts caught exception type and handler2 count = 0.
  • Parallel unsubscribe/resubscribe lifecycle — subscribe → invoke (count=1) → unsubscribe → invoke (count still 1) → resubscribe → invoke (count=2). Validates Register/Unregister's lockless CAS path.
  • CancellationToken propagation — pre-cancelled token → OperationCanceledException propagates before any handler runs. Deterministic (no TCS / timing fragility).

Why now

Surfaced 2026-05-27 during the org-wide aot-smoke coverage survey done after ZeroAlloc.Serialisation shipped 2.3.1 + 2.3.2 reactively. Same "smoke exists but partial" pattern applied to ZA.AsyncEvents. This PR closes it. Already-shipped siblings in the same workstream: ZeroAlloc.Validation B4 (#51) and ZeroAlloc.Inject B1 (#68).

What changed

  • 3 new assertion blocks in Program.cs (~80 LOC total) — each block creates a fresh OrderService() for isolation
  • 1 added using ZeroAlloc.AsyncEvents; directive (Phase 2's explicit AsyncEvent<int> local needed it)
  • docs/backlog.md — B1 entry struck shipped with durable findings preserved

No new fixture files. No library changes.

Findings worth flagging

  • Explicit delegate type requires using ZeroAlloc.AsyncEvents; — Phase 2's AsyncEvent<int> local needed the namespace imported; Phase 1's inline lambdas didn't.
  • Sequential mode propagates throw type unwrappedInvalidOperationException surfaces from InvokeAsync directly, not wrapped.
  • ct.ThrowIfCancellationRequested() fires BEFORE the handler loop — confirmed by the cancel-before-invoke assertion (handler count = 0).

Decisions (design doc)

  • Reuse existing OrderService via fresh instances rather than adding three new fixture services
  • Cancel-before-invoke for cancellation block rather than mid-flight TCS choreography — deterministic regression net
  • Sequential + exception / Parallel + lifecycle pairings match the backlog's bullets

SemVer

No package version bump — CI-only chore: changes. release-please will treat as chore: and skip the release manifest.

Test plan

  • Local build clean
  • Local JIT run prints AOT smoke: PASS (Phases 0+1+2+3 all green)
  • CI build clean
  • CI aot-smoke job passes (the real AOT publish on ubuntu-latest)

🤖 Generated with Claude Code

MarcelRoozekrans and others added 6 commits May 28, 2026 13:51
Three new assertion blocks in samples/ZeroAlloc.AsyncEvents.AotSmoke/Program.cs
to cover Sequential exception propagation, Parallel unsubscribe/resubscribe
lifecycle, and CancellationToken pre-cancellation propagation. Reuses the
existing OrderService via fresh instances per block; no new fixture files.

CI-only chore — no library changes, no NuGet release. Strikes B1 shipped
on merge. Same pattern as ZA.Validation B4 (#51) and ZA.Inject B1 (#68).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4-phase plan mirroring ZA.Validation B4 (#51) and ZA.Inject B1 (#68):
three new assertion blocks in Program.cs covering Sequential exception
propagation, Parallel unsubscribe/resubscribe lifecycle, and
CancellationToken pre-cancellation propagation. No new fixture files;
each block reuses OrderService via fresh instances. Phase 4 strikes B1
shipped and opens the PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sequential mode bails on the first handler throw — handler2 never runs.
Asserts the caught exception is InvalidOperationException (not wrapped)
and that handler2's invocation count remains 0. Validates the foreach
loop's first-throw-bails behavior under PublishAot.
Subscribe-invoke-unsubscribe-invoke-resubscribe-invoke flow tests the
lockless CAS register/unregister path under PublishAot. Asserts count
progression 1 → 1 → 2 across the three phases; catches regressions in
Unregister's removal or Register's post-unregister re-add.
Pre-cancelled token surfaces OperationCanceledException via the library's
ct.ThrowIfCancellationRequested() before the first handler runs. Asserts
OCE is caught (not swallowed) and handler count remains 0 (ct check
fires before the handler invocation, not after). Deterministic — no TCS
or timing dependency.
@MarcelRoozekrans MarcelRoozekrans merged commit 2e3b35d into main May 28, 2026
3 checks passed
@MarcelRoozekrans MarcelRoozekrans deleted the chore/aot-smoke-cover-asyncevents-paths branch May 28, 2026 12:10
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