Skip to content

Compliance wave 8: conjoined event tenancy and subscriptions (2.45.0) - #642

Merged
jeremydmiller merged 1 commit into
mainfrom
compliance/wave-8
Aug 8, 2026
Merged

Compliance wave 8: conjoined event tenancy and subscriptions (2.45.0)#642
jeremydmiller merged 1 commit into
mainfrom
compliance/wave-8

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

The last two suites in the event sourcing backlog — marten#5148 and marten#5151. Library 26 → 28 suites, 216 → 230 tests. With these, the ES compliance backlog is empty.

Suite Tests Seam cost
ConjoinedEventTenancyCompliance 8 one config member
SubscriptionCompliance 6 one partial + one registrar member

ConjoinedEventTenancyCompliance — misfiled, like the last two

#5148 was filed as needing a tenant-scoped session seam. It doesn't:
IEventStore<TOperations, TQuerySession>.OpenSession(IEventDatabase, string tenantId) is already on the shared generic interface and implemented by both products, and TenancyStyle is already a shared JasperFx enum (JasperFx/MultiTenancy/TenancyStyle.cs) that both spell identically as Events.TenancyStyle = TenancyStyle.Conjoined. Reached with the same cast trick wave 7 introduced. The only cost is ComplianceStoreConfig.ConjoinedEventTenancy.

The property under test is isolation, and it's worth pinning across stores because the failure mode is silent and asymmetric: a store that leaks across tenants still answers correctly for the tenant that owns the data, and misbehaves only for the other one. So every test checks both directions — what a tenant can see, and what it must not.

The suite deliberately reuses one stream id across two tenants. Under conjoined tenancy a stream's identity is (tenant, id), not id alone, and reusing the id is the sharpest way to show it: a store keying on id alone either collides on append or hands one tenant's events to the other.

SubscriptionCompliance — the cost estimate held exactly

Unlike #5148, #5151's filed cost was right. Both products declare ISubscription with an identical member, but IChangeListener is a per-product type, so the signature cannot be written once — the same shape as ComplianceFlatTableProjection. Neither exposes a public Subscribe overload taking the shared ISubscriptionSource<,> even though both prefer it internally, and registerSubscription is private in both.

What's pinned is ordering and completeness. Page boundaries are deliberately never asserted — only that the union of the pages is right, monotonic, and free of redelivery.

A design flaw the suite caught in itself

The tests originally waited on WaitForNonStaleProjectionDataAsync. That tracks projections, and this suite's store registers none — so it could report non-stale before the subscription had been handed anything at all, and one test failed for exactly that reason.

Replaced with ComplianceSubscription.WaitForStreamEventCountAsync, scoped to a single stream because the subscription instance is shared across the suite's tests and a store-wide total would be satisfied by the wrong events.

Recording is under a lock: the daemon delivers pages from its own threads, and an unsynchronized List.Add here would be the marten#5085 class of bug — a test-side data race presenting as an impossible assertion failure.

Verification

Both stores 14/14 on the new suites — Polecat green on the first run, no capability gates and no product gaps found. Full compliance 230/230 on each. Marten full EventSourcingTests 1796 / 0 / 7; Polecat full suite running.

Consumer adoptions will be verified against locally packed 2.45.0-local.N artifacts before publish, not just source-linked.

Closes JasperFx/marten#5148
Closes JasperFx/marten#5151

🤖 Generated with Claude Code

https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde

The last two suites in the event sourcing backlog (marten#5148, marten#5151).
Library 26 -> 28 suites, 216 -> 230 tests. With these the ES compliance backlog
is empty.

ConjoinedEventTenancyCompliance (8) -- one database sliced by tenant. The
property under test is ISOLATION, and it is worth pinning across stores because
the failure mode is silent and asymmetric: a store that leaks across tenants
still answers correctly for the tenant that owns the data and misbehaves only for
the other one. So every test checks both directions.

The suite deliberately reuses ONE stream id across two tenants. Under conjoined
tenancy a stream's identity is (tenant, id), not id alone, and reusing the id is
the sharpest way to show it -- a store keying on id alone either collides on
append or hands one tenant's events to the other.

Cost: ONE config member, ConjoinedEventTenancy. Filed as needing a tenant-scoped
session seam; it does not. IEventStore<TOperations,TQuerySession>.OpenSession(
IEventDatabase, string tenantId) is already shared and implemented by both
products, and TenancyStyle is already a shared JasperFx.MultiTenancy enum that
both spell identically. Reached with the same cast trick wave 7 introduced.

SubscriptionCompliance (6) -- ordering and completeness for the "do something with
every event" surface that is not a projection. Page boundaries are deliberately
never asserted, only that the union of pages is right, monotonic and free of
redelivery.

Cost: a per-consumer partial plus one registrar member, and that IS the honest
price this time. Both products declare ISubscription with an identical member,
but IChangeListener is per-product, so the signature cannot be written once --
the same shape as ComplianceFlatTableProjection. Neither exposes a public
Subscribe overload taking the shared ISubscriptionSource<,> even though both
prefer it internally, and registerSubscription is private in both.

DESIGN CORRECTION FOUND WHILE WRITING IT: the subscription tests originally waited
on WaitForNonStaleProjectionDataAsync. That tracks PROJECTIONS, and this suite's
store has none -- so it could report non-stale before the subscription had been
handed anything, and one test failed for exactly that reason. Replaced with
ComplianceSubscription.WaitForStreamEventCountAsync, scoped to one stream because
the subscription instance is shared across the suite's tests and a store-wide
total would be satisfied by the wrong events.

Recording is under a lock: the daemon delivers pages from its own threads, and an
unsynchronized List.Add here would be the marten#5085 class of bug -- a test-side
data race presenting as an impossible assertion failure.

Both stores 14/14 on the new suites, first run on Polecat, no capability gates and
no product gaps found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde
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.

Compliance suite: subscriptions Compliance suite: conjoined event tenancy — needs a tenant-scoped session seam

1 participant