Skip to content

fix(eventsourcing): SQL concurrency, upcaster wiring, exports (v26.06.10)#36

Merged
ancongui merged 1 commit into
mainfrom
fix/eventsourcing-concurrency-upcaster-exports
Jun 5, 2026
Merged

fix(eventsourcing): SQL concurrency, upcaster wiring, exports (v26.06.10)#36
ancongui merged 1 commit into
mainfrom
fix/eventsourcing-concurrency-upcaster-exports

Conversation

@ancongui
Copy link
Copy Markdown
Contributor

@ancongui ancongui commented Jun 5, 2026

Summary

Validating implement-event-sourcing (skill itself faithful — all 5 behaviours verified, zero demo fixes) surfaced framework issues in pyfly.eventsourcing:

  1. SqlAlchemyEventStore optimistic-concurrency TOCTOU + un-translated error (correctness bug). append() read latest_version() on a separate connection before async with self._engine.begin(), so two concurrent writers could both pass the expected_version check; the loser hit UNIQUE(aggregate_id, sequence) and surfaced a raw IntegrityError rather than the documented ConcurrencyError, so retry-on-ConcurrencyError callers silently missed the collision. Fix: the version check now runs inside the write transaction, and a UNIQUE violation is caught and re-raised as ConcurrencyError. (InMemoryEventStore was already correct.)
  2. EventUpcaster was dead code (noop-wiring). EventUpcaster/NoOpUpcaster were exported + documented but never invoked by any read path. Fix: both stores accept upcasters=... and apply them in load()/stream_all() (default identity).
  3. EventHandlerException not exported — reachable only via the private pyfly.eventsourcing.aggregate submodule, unlike its sibling ConcurrencyError. Fix: exported from pyfly.eventsourcing (+ __all__).
  4. Outbox dead-letter surfacing gap. Records exhausting max_attempts were retained but had no accessor. Fix: TransactionalOutbox.dead_letters().

Tests

tests/eventsourcing/test_eventsourcing_fixes.py: concurrent SQL append → exactly one ConcurrencyError (not a raw DB error) + consistent final version; upcasters applied on load and stream_all; outbox dead_letters() surfaces exhausted records; EventHandlerException importable from the package.

Gates

mypy --strict (607 files) ✓ · ruff ✓ · full suite 3635 passed, 1 skipped.

Bumps v26.06.09 → v26.06.10, CHANGELOG, uv.lock synced.

…t EventHandlerException + bump v26.06.10

Surfaced validating implement-event-sourcing (skill faithful; these are framework bugs):
- SqlAlchemyEventStore.append read latest_version() on a separate connection BEFORE
  the write txn (TOCTOU) and never translated the UNIQUE violation, so concurrent
  writers leaked a raw IntegrityError instead of ConcurrencyError. Version check now
  runs inside the txn + IntegrityError -> ConcurrencyError backstop.
- EventUpcaster/NoOpUpcaster were exported+documented but never invoked (dead code).
  Both stores now accept upcasters= and apply them in load()/stream_all().
- EventHandlerException now exported from pyfly.eventsourcing (was submodule-only,
  unlike its sibling ConcurrencyError).
- TransactionalOutbox.dead_letters() surfaces exhausted records.

Tests: tests/eventsourcing/test_eventsourcing_fixes.py (concurrency, upcaster-on-read,
dead-letters, export). Gates: mypy --strict (607), ruff, full suite 3635 passed.
@ancongui ancongui force-pushed the fix/eventsourcing-concurrency-upcaster-exports branch from 628541c to 6854b87 Compare June 5, 2026 14:45
@ancongui ancongui merged commit e9dbd82 into main Jun 5, 2026
5 checks passed
@ancongui ancongui deleted the fix/eventsourcing-concurrency-upcaster-exports branch June 5, 2026 14:47
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