Skip to content

JasperFx 2.75.1

Choose a tag to compare

@jeremydmiller jeremydmiller released this 27 Sep 16:07

A patch release. One runtime fix, and one correction to a compliance fact that 2.75.0 shipped in a state no store could pass — read the first section if you enrolled the new document tenancy suite.


⚠️ If you enrolled DocumentConjoinedTenancyCompliance on 2.75.0, one of its ten facts could not pass

optimistic_concurrency_is_scoped_to_the_tenant_for_a_shared_id asserted a refusal no correct store can produce. It advanced tenant A's row by storing a loaded instance, then re-stored that same instance and expected a ConcurrencyException — but a committed Store writes the landed version back onto the instance, so the guard matched the current version and the write was admitted.

That write-back is not incidental, it is required: GuidOptimisticConcurrencyCompliance.a_successful_write_moves_the_instances_own_version_on mandates it, so that a long-lived instance stays usable. The two facts contradicted each other, and no store could be green on both. The only way to pass the tenancy fact was to fail the concurrency suite it is a tenanted special case of.

This was our bug, not yours. A red fact there needed no change on your side, and no store behaviour was wrong. Because JasperFx.Events.ComplianceTests ships as source, the fix arrives by bumping to 2.75.1 — nothing else to do.

Fixed by making "stale" mean a separately loaded instance, read before the winning write, which is what stale has to mean on a store with write-back. The arrangement that gives the fact its teeth — one document id shared across two tenants — is unchanged.

Why it escaped, which is the part worth fixing

The fact skipped in this repository, because the in-memory reference store left SupportsOptimisticConcurrency false. A fact that skips everywhere it could run is a fact nobody has checked, and this one reached three stores that way.

So the reference store now implements the IVersioned guard and enrolls GuidOptimisticConcurrencyCompliance alongside the tenancy suite. It had to be enrolled in both, because the contradiction is between two suites — nothing enrolled in only one can see it. Reverting the suite to its 2.75.0 text now fails in this repo with the same symptom reported against Fisher.

That work also exposed a second defect in the reference store: LoadAsync handed back the stored object reference, so two independent reads were one object and could never disagree about a version. Documents now round-trip through JSON on read and write, as a real store does.

#903.


Block<T> workers no longer inherit the activity current when the block was built

Block<T> starts its workers with Task.Run in the constructor, and Task.Run flows the ExecutionContext — so every worker captured whatever Activity.Current was set at construction and kept it for the block's whole lifetime, long after that activity had ended. Every span the block's action started became a child of it.

The reported consequence: a Marten async daemon whose projection agents were rebuilt from inside a Wolverine HTTP handler parented every subsequent projection page span under that one request — 3,943 spans in ten minutes, on a trace already reported as finished. Anyone restarting projection agents from a request-scoped context was affected.

The ambient activity is now cleared per item, so each posted item's work is a trace root. Per item rather than once per worker because an action that starts an activity and does not dispose it would otherwise leak into the next item's parentage.

ExecutionContext.SuppressFlow() would have fixed the symptom too and was deliberately not used: it stops every AsyncLocal from reaching the workers, logging scopes and the current culture included, and a caller relying on those would lose them silently. Clearing one ambient value changes nothing else a worker inherits, and nothing at all for the caller.

Reported with a complete repro, the correct diagnosis and the fix taken here by @smoqmilus. #900.


Compliance facts that now discriminate

Two facts were passing regardless of whether the behaviour they name was correct. Both are replaced or joined by versions that fail on a broken runtime — verified by reverting the relevant fix and re-running, not by inspection.

  • creating_and_deleting_within_one_batch_reports_no_deletion (#893) asserts what a commit reported rather than what it stored. Its predecessor passed either way, because inline the phantom delete targets a row that does not exist and is a no-op in SQL — but the deletion still appears in IDocumentChangeSet.Deleted, which is how #886's reporter found it. Confirmed to fail with #889's runtime fix reverted, while the old fact still passed.
  • a_project_with_no_aggregates_builds_clean_under_the_double_load (#887) pins JasperFxSourceGeneratorAppliedAttribute's AllowMultiple = true in the one topology where the marker is the only thing emitted twice. The existing fact asserted "no CS0579" inside a build already broken by CS0433, so it would have passed if the rule regressed.

New seam for consumers

ComplianceStoreConfig.AddCommitListener(...) — the event-store twin of the document config's listener slot. Each store's event fixture needs to replay it (options.AddCommitListener(listener) on Marten, one call identical to what each document fixture already makes). Until a store does, the new fact fails rather than skips, by the #672 rule.


Downstream enrollment for 2.75.0's document tenancy suite

Still open, and unaffected by this patch: JasperFx/marten#5517, JasperFx/polecat#682, JasperFx/fisher#336. The Polecat one is not just enrollment — document tenancy there is taken from the event store's, store-wide, with no per-type opt-in.