Compliance wave 8: conjoined event tenancy and subscriptions (2.45.0) - #642
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
ConjoinedEventTenancyComplianceSubscriptionComplianceConjoinedEventTenancyCompliance— 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, andTenancyStyleis already a shared JasperFx enum (JasperFx/MultiTenancy/TenancyStyle.cs) that both spell identically asEvents.TenancyStyle = TenancyStyle.Conjoined. Reached with the same cast trick wave 7 introduced. The only cost isComplianceStoreConfig.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 exactlyUnlike #5148, #5151's filed cost was right. Both products declare
ISubscriptionwith an identical member, butIChangeListeneris a per-product type, so the signature cannot be written once — the same shape asComplianceFlatTableProjection. Neither exposes a publicSubscribeoverload taking the sharedISubscriptionSource<,>even though both prefer it internally, andregisterSubscriptionis 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.Addhere 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
EventSourcingTests1796 / 0 / 7; Polecat full suite running.Consumer adoptions will be verified against locally packed
2.45.0-local.Nartifacts before publish, not just source-linked.Closes JasperFx/marten#5148
Closes JasperFx/marten#5151
🤖 Generated with Claude Code
https://claude.ai/code/session_01VpDCvJcBDZerieJB4JEHde