Table of Contents

Release 2026-08-19

This page is part of the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at 2026-08-19.md, and llms.txt lists every page.

Part of the changelog.

A per-package patch advances Orleans.Lattice to 9.0.6 (see Changed and Fixed); all other packages remain at their 9.0.x/9.0.0 versions.

Changed

  • The GSet, MvRegister, and OrMap CRDT clone/merge hot paths shed avoidable rehashing and allocation. Three independent primitive optimisations, each behaviour-, ordering-, and wire-format-neutral: (1) GSet.Merge now builds its result as a union pre-sized to the combined operand count and fills it with two UnionWith folds, instead of cloning the left operand and growing it element by element - roughly 42% less allocation on the merge (5,016 B to 2,908 B for two 64-element sets, MemoryDiagnoser, deterministic). (2) MvRegister.Clone and (3) OrMap.Clone now copy their per-replica dot Context dictionary with the source's own comparer rather than re-specifying StringComparer.Ordinal, which restores the Dictionary bulk-copy fast path (a comparer mismatch had forced a full element-by-element rehash) - allocation-neutral and about 8-10% less clone CPU at 4,096 entries, and because Merge folds through Clone the saving carries into merge. This mirrors the existing comparer-preserving copy in Rga.Clone. New/extended MemoryDiagnoser microbenchmarks (Crdt gset merge, and a new OrMap clone (multi-replica context)) cover the change. (Orleans.Lattice 9.0.6, #1544)

Fixed

  • A checkpointed leaf partition with a momentarily empty cache no longer releases its block pin and licenses a WAL trim of its un-snapshotted prefix. BPlusLeafGrain.ResolveDurablePinForPartition decided a partition was empty - and therefore safe to release its HybridLogicalClock.Zero block pin up to the checkpoint - purely from the per-activation in-memory cache, which does not reflect a leaf's durable data in the window between activation and cache hydration: a cold reactivation whose snapshot rehydrate finds nothing (yet the persisted projection checkpoint says the prefix [0, checkpoint] was durably applied), or tombstone reaping / compaction emptying a checkpointed partition's cache while its WAL prefix still must replay. Such a partition took the empty branch and reported (clock, checkpoint), releasing the block before the coverage gate (#1492) could run and licensing the shared-shard WAL GC to trim a checkpointed, un-snapshotted prefix; the next cold rebuild then replayed from a WAL whose prefix was gone and came up with its checkpoint below the trim floor, failing to reactivate (LeafProjectionStaleException) and silently degrading the affected semantic index to keyword mode. The empty-partition release is now narrowed to a partition that has never durably checkpointed (checkpoint < 0, nothing committed to lose); a partition with a persisted checkpoint and a transiently empty cache falls through to the same coverage gate as a cache-populated partition, retaining the (Zero, -1) block pin until a covering snapshot exists. Liveness is preserved - snapshot capture stamps every partition's checkpoint into SnapshotOffsetsByPartition, dataless or not, so a checkpointed-but-empty partition's block lifts as soon as any snapshot is captured - and the genuinely-empty (checkpoint < 0) release path is byte-for-byte unchanged with no new allocations. (Orleans.Lattice 9.0.6, #1535)
  • The core resilient range scan no longer silently truncates a credential-scoped full-tree read across a cursor reopen. ILattice.ScanEntriesAsync/ScanKeysAsync and the DeleteRangeAsync drain re-open their underlying cursor when a segment is evicted mid-scan, and re-asserted only the caller's system-origin scope around each reopen - not the ambient credential scope (LatticeCredentialContext), which is backed by the same Orleans RequestContext and is likewise reset in the iterator's execution flow after the first segment. A caller scanning under a credential but no system-origin therefore lost its identity on the first reopen, and every later segment resolved anonymous, was denied a reject-all key-filter by the fail-closed access gate, and completed with zero rows. The ambient credential is now captured once and re-asserted around every reopened segment and cursor step, symmetric to the existing system-origin handling. This fixes the repository-context background reconcile reading back zero stored nodes and re-bootstrapping the whole index on every pass. (Orleans.Lattice 9.0.6, #1539)
  • A short-lived leaf now captures a durable snapshot on graceful deactivation, so its WAL stops being retained forever. A data-bearing leaf holds a HybridLogicalClock.Zero "block" pin - which correctly forbids the WAL GC from trimming its checkpointed-but-un-snapshotted prefix (the no-loss gate, #1535) - until a durable snapshot covers that prefix. Snapshot capture only ever had two triggers, both requiring the leaf to stay activated: the activation-time fall-off-log advisory and a periodic recheck every LeafSnapshotReClassifyEveryNCheckpoints checkpoints (default 64). A short-lived bursty activation (activate, take a few writes and checkpoints, then deactivate before the cadence threshold) fired neither, so the leaf checkpointed, went dormant, and left its block pin held with no snapshot ever taken - the shared per-partition WAL was then retained without bound even though every consumer had moved on. The leaf now captures a snapshot on graceful deactivation, gated on a genuine this-activation checkpoint advance so a cold reactivation never captures a partial cache and falsely claims coverage; the capture advances durable coverage, the materialiser pin resolves to the covered frontier, and the WAL GC trims the now-covered prefix. The capture is best-effort: if the snapshot store is unavailable the coverage does not advance, the block pin stays in place, and the WAL is retained rather than trimmed ahead of durable coverage, so the no-loss invariant is preserved. Crash deactivations bypass the hook by design; the persisted checkpoint still bounds the next activation's replay. (Orleans.Lattice 9.0.6, #1537)
  • An already-converged, snapshot-less leaf now lifts its block pin on its next cold reactivation instead of retaining its WAL forever. The graceful-deactivation capture (#1537) is gated on a genuine this-activation checkpoint advance, which correctly excludes a cold reactivation whose cache was restored without a forward apply. But a leaf that already checkpointed its whole prefix (checkpoint at the WAL head) and never captured a covering snapshot holds a HybridLogicalClock.Zero block pin, and on a cold reactivation it fully rebuilds its cache from the start of the readable WAL yet advances no checkpoint - so the deactivation capture stayed suppressed and none of the three capture triggers fired, leaving the block pin (and the shared-shard WAL it retains) held indefinitely. The graceful-deactivation capture now also proceeds when the cache was cold-rebuilt from the WAL start this activation over a pre-existing durable checkpoint: that rebuild leaves the cache holding the entire readable window, a faithful superset of the checkpointed prefix, so stamping coverage is truthful. A brand-new leaf (no pre-existing checkpoint) never qualifies, so its foreground writes are never auto-covered. The one shape that could make such a rebuild unfaithful - a trimmed WAL prefix with no covering snapshot - already throws LeafProjectionStaleException at activation (the #945 fall-off guard) before the capture is reached, so the no-loss invariant (#1535) is preserved. (Orleans.Lattice 9.0.6, #1542)