Skip to content

feat(journaling): route Garnet journals across partitions with a MetaStore - #11501

Draft
ReubenBond wants to merge 15 commits into
dotnet:mainfrom
ReubenBond:rb-feat-journaling-garnet-phase2
Draft

ReubenBond wants to merge 15 commits into
dotnet:mainfrom
ReubenBond:rb-feat-journaling-garnet-phase2

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Spike — not for merge. This stack explores range-partitioned journaling on embedded Garnet; all PRs stay drafts.

Depends on #11498 (stacked); review only the commits after it.

Problem

Layer 1 runs one Garnet partition that owns every bucket. The range-partitioned design needs many partitions, a MetaStore that is the authority for which partition owns each bucket and where each partition runs, and clients that route by a cached view of that map. That routing has to stay correct when the view is stale, so later phases can move ranges online.

Solution

Phase 2 of src/Garnet/DESIGN.md. The range map is static: it can express migrations and hosting, but ranges don't move yet.

  • MetaStore (Orleans.Journaling.Ranges, with no Garnet or Orleans types). IRangeMetaStore performs a compare-and-swap from version V to V+1 on a RangeMap: map identity, bucket count, ranges, migrations, and partition hosting with a published endpoint. Two implementations:

    • InMemoryRangeMetaStore, for tests.
    • AzureBlobRangeMetaStore: one JSON document, written conditionally on its ETag. Each document carries a write id, so after an ambiguous retried upload the store reports the true outcome or "unknown", never a wrong one.

    RangeMap.Validate enforces the structural invariants.

  • Hosting. Each silo hosts the partitions the map assigns to its HostId. Each partition gets its own data directory and endpoint, opens its initial buckets, records a descriptor (map identity, partition id, bucket count), and then publishes its endpoint by CAS. If HostId is not set, it is generated once and stored in the data directory.

  • Routing. GarnetJournalRouter maps JournalId → bucket → owner using the cached view, which it updates in version order, with concurrent refreshes sharing one MetaStore read. It handles failures as follows:

    • WrongPartition with a newer bucket version: refresh the view and retry at once.
    • A version no newer than the view, an unpublished or unreachable partition, or a lost reply: back off and retry within OperationTimeout.

    Retries are idempotent through the existing recent-ETag mechanism.

  • Catalog. Each bucket's index is read at its owner with a new OJINDEX procedure. It returns NeedPull for buckets whose migration is not done, rather than returning a partial listing.

Rationale and refinements (recorded in DESIGN.md)

  • D17: requests don't carry the client's view version. By D6 an open bucket is the unique acceptor, so the partition's decision doesn't depend on the client's view. The bucket version reported in WrongPartition tells the client whether to refresh or back off.
  • D18: hosts publish endpoints in the map. A stale endpoint is safe because whatever listens there rejects buckets it doesn't hold open.
  • D23: the map's identity is part of every bucket key. A partition belonging to another cluster, reached through a reused port, therefore rejects the operation too.
  • D24: unknown MetaStore outcomes throw. Callers re-read the map instead of acting on a guess.
  • D19–D22 cover the PartitionId format, keeping the Azure store in this package (Garnet already depends on Azure.Storage.Blobs), the persisted default HostId, and the optional Garnet MetaStore (Undecided).

Tests

  • Routing and view-refresh tests across three embedded partitions: a stale view after a bucket moves, out-of-order views, an owner that hasn't opened its bucket yet, unpublished and unreachable endpoints, and a foreign map's partition.
  • MetaStore CAS tests with concurrent writers, run against both the in-memory and Azure Blob stores. The Azure Blob ones are in the AzureStorage category and pass against Azurite.
  • Validation and serialization tests for RangeMap.
  • A three-silo, four-partition test cluster that runs DurableGrainTests and checks that journals spread over both hosting silos and are all listed by the catalog.
Microsoft Reviewers: Open in CodeFlow

Copilot AI balanced review requested due to automatic review settings October 14, 2026 17:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved deadline enforcement, wildcard startup, shutdown connection ownership, and retry-test correctness issues block approval.

4 open findings
What changed in this PR

Extends the experimental Garnet journal provider from #11498 with static multi-partition hosting and MetaStore-backed routing.

Changes:

  • Adds validated range maps and in-memory/Azure Blob MetaStores.
  • Routes operations and catalog reads by bucket ownership, with endpoint publication and retries.
  • Adds routing, metadata, and multi-silo integration coverage.

Automated coverage results were unavailable.

File Description
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​RangeMetaStoreTests.cs Tests MetaStore write contracts.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​RangeMapTests.cs Tests validation and serialization.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​Orleans.Journaling.Garnet.Tests.csproj Configures provider tests.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetTestRouting.cs Provides routing helpers.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetPartitionFixture.cs Manages test partition lifecycle.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalSuiteTests.cs Adapts shared journaling suites.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalStorageTests.cs Covers storage behavior.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalStorageOptionsTests.cs Tests configuration validation.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalRouterTests.cs Tests ownership and routing retries.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalProtocolTests.cs Covers protocol and recovery behavior.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalLatencyTests.cs Measures append performance.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetJournalDurabilityTests.cs Exercises process-kill recovery.
test/​Extensions/​Orleans.Journaling.Garnet.Tests/​GarnetDurableGrainTests.cs Tests multi-silo journal distribution.
test/​Extensions/​Orleans.Journaling.Garnet.CrashHost/​Program.cs Hosts crash-test partitions.
test/​Extensions/​Orleans.Journaling.Garnet.CrashHost/​Orleans.Journaling.Garnet.CrashHost.csproj Configures crash-test executable.
src/​Orleans.Journaling/​Orleans.Journaling.csproj Grants provider internal access.
src/​Garnet/​Orleans.Journaling.Garnet/​README.md Documents setup and behavior.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​RangeMapJsonSerializer.cs Serializes versioned maps.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​RangeMap.cs Defines maps and invariants.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​PartitionId.cs Defines validated partition identities.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​IRangeMetaStore.cs Defines metadata storage contracts.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​InMemoryRangeMetaStore.cs Implements in-memory conditional writes.
src/​Garnet/​Orleans.Journaling.Garnet/​Ranges/​AzureBlobRangeMetaStore.cs Implements durable conditional writes.
src/​Garnet/​Orleans.Journaling.Garnet/​Procedures/​GarnetJournalProtocol.cs Adds catalog procedure identification.
src/​Garnet/​Orleans.Journaling.Garnet/​Orleans.Journaling.Garnet.csproj Configures provider packaging and dependencies.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalStorageProvider.cs Coordinates hosting and catalog access.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalStorageOptionsValidator.cs Validates provider options.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalStorageOptions.cs Exposes hosting and routing configuration.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalStorageHostingExtensions.cs Registers the provider.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalStorage.cs Sends journal operations through routing.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalRouter.cs Manages views, connections, and retries.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalPartitionManager.cs Initializes partitions and publishes endpoints.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalPartitionHost.cs Hosts durable embedded partitions.
src/​Garnet/​Orleans.Journaling.Garnet/​GarnetJournalKeys.cs Scopes bucket keys by map identity.
src/​Garnet/​DESIGN.md Records phase-two architecture and decisions.
src/​api/​Garnet/​Orleans.Journaling.Garnet/​Orleans.Journaling.Garnet.cs Records generated public APIs.
Orleans.slnx Includes Garnet projects.
Directory.Packages.props Pins the Garnet dependency.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Garnet/Orleans.Journaling.Garnet/GarnetJournalPartitionManager.cs Outdated
Comment thread src/Garnet/Orleans.Journaling.Garnet/GarnetJournalRouter.cs Outdated
Comment thread src/Garnet/Orleans.Journaling.Garnet/GarnetJournalRouter.cs
Comment thread test/Extensions/Orleans.Journaling.Garnet.Tests/RangeMetaStoreTests.cs Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 03:49
@ReubenBond
ReubenBond force-pushed the rb-feat-journaling-garnet-phase2 branch from 004530c to 954df8b Compare October 15, 2026 03:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/Garnet/Orleans.Journaling.Garnet/GarnetJournalPartitionManager.cs Outdated
Comment thread test/Extensions/Orleans.Journaling.Garnet.Tests/RangeMetaStoreTests.cs Outdated
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Code coverage

Metric Pull request Current main Variance
Lines 83.73% (121,197 / 144,750) 83.58% (119,037 / 142,429) +0.1521 pp
Branches 73.38% (36,237 / 49,381) 73.21% (35,432 / 48,398) +0.1728 pp

Report-only conclusion: improved.

The current-main baseline is commit fbe240c34e and uses the same reviewed coverage matrix.

Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities.

The comparison remains report-only while normal line and branch variance is calibrated.

Coverage details

Copilot AI balanced review requested due to automatic review settings October 15, 2026 13:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread test/Extensions/Orleans.Journaling.Garnet.Tests/GarnetJournalRouterTests.cs Outdated
Copilot AI balanced review requested due to automatic review settings October 15, 2026 15:40
@ReubenBond
ReubenBond force-pushed the rb-feat-journaling-garnet-phase2 branch from 9524a01 to f7741eb Compare October 15, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

This non-mergeable distributed-storage spike needs maintainer review, including deterministic validation of ambiguous MetaStore write outcomes.

2 open findings

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 15, 2026 17:09
@ReubenBond
ReubenBond force-pushed the rb-feat-journaling-garnet-phase2 branch from f7741eb to b9bc405 Compare October 15, 2026 17:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The explicitly non-mergeable distributed-storage spike requires human validation of routing and durability guarantees.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 15, 2026 21:53
@ReubenBond
ReubenBond force-pushed the rb-feat-journaling-garnet-phase2 branch from b9bc405 to 132f2f8 Compare October 15, 2026 21:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

This explicitly non-mergeable spike changes distributed routing and durability-sensitive contracts that require final human review.

1 open finding

🧠 Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

This explicitly not-for-merge spike changes distributed routing and persistence contracts that require final human review.

0 open findings

1 resolved since last review

🧠 Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 18, 2026 17:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

This explicitly non-mergeable spike requires maintainer validation of distributed routing, durability, and lifecycle guarantees.

No open issues 🎉

Review effort: Balanced

Adds Orleans.Journaling.Garnet, which stores journals in an embedded Garnet partition with synchronous AOF durability (phases 0 and 1 of the range-partitioned design in src/Garnet/DESIGN.md).

Each journal operation is one Garnet transaction procedure that checks its bucket's state (the in-band seal used by range migration), applies conditional writes, and recognizes retried mutations by their client-generated ETags. Tests cover the provider contract, the Orleans.Journaling suites, a grain cluster, and process-kill recovery.
… and bound each attempt

Every mutation is now conditioned on a state that never recurs: a content or metadata ETag, or an absence named by its tombstone's ETag. A retry whose ETag has left the eight-entry history can therefore not resurrect a deleted journal or overwrite a later metadata update; it throws GarnetJournalAmbiguousConflictException, an InconsistentStateException. Unconditional metadata updates are conditioned on the ETag the client reads, no-op updates record their ETag, OperationTimeout bounds each attempt's wait, and the bucket count is claimed atomically before any bucket opens.
The API-diff workflow built every source project for net8.0, so net10.0-only projects such as Orleans.Journaling.Garnet failed with NETSDK1005 and kept a stale surface. It now uses net8.0 when targeted and the project's first target framework otherwise.
… in the Garnet durability test

SAVE checkpoints while writers append and exceeded the 5 s default async timeout on a Windows CI runner. Each cycle now also waits, bounded, until every writer has an acknowledged append before the random delay and kill.
…Store

Add a pluggable range MetaStore (in-memory and Azure Blob, compare-and-swap from V to V+1), host the partitions the range map assigns to each silo, publish their endpoints in the map, and route each journal operation to its bucket's owner, refreshing the view on WrongPartition and retrying unavailable partitions within OperationTimeout. The catalog lists each bucket's index at its owner.
…host ids

Bucket keys carry the range map's identity, so a partition of another map reached through a stale endpoint rejects operations. The default HostId is generated once and stored in the data directory instead of using the silo name, which Orleans randomizes. The Azure Blob MetaStore stamps each document with a write id and reports an unknown outcome instead of a wrong one when a later write supersedes an ambiguous upload.
…ialization

Address review feedback on the Garnet routing layer: OperationTimeout now bounds connection, command, refresh, and backoff waits without cancelling the shared view refresh; router disposal stops retries and never reopens connections; partitions record their identity before opening buckets and mark initialization complete separately, so interrupted initializations resume only for the same partition and map; silos initialize wildcard-bound partitions through loopback. Compare recovered MetaStore writes by value in tests.
…o routing

Port layer 1's retry and initialization changes to the multi-partition router: the router reports attempts whose outcome is unknown through the ambiguous flag while WrongPartition rejections leave it unset, storage passes the flag so conflicts after unknown outcomes surface as GarnetJournalAmbiguousConflictException, and partitions claim their whole identity in one set-if-absent field so concurrent initializations with different maps or bucket counts cannot both succeed. Port layer 1's client tests to the router and partition manager.
…router operations

Cover each outcome of a conditional upload which fails its condition with deterministic fault injection: this write's document at the next version reports success, another writer's identical map under a different write id reports failure, and a later version reports an unknown outcome. Router tests apply their watchdog outside the exception assertion, so only an operation's own exception satisfies it.
…eguard

A bucket which is open with an incomplete migration and a nonempty local index makes OJINDEX return NeedPull and fails a full catalog enumeration with InvalidOperationException; once the migration completes, the bucket lists its journals.
Disposal marks the router stopped before closing connections, and closing a connection fails commands in flight with cancellation or pipe errors. Operations now report ObjectDisposedException for any failure observed after disposal begins, keeping the original as the inner exception, while cancellation from a connection retired by a view change is retried as an ambiguous connection failure.
Copilot AI balanced review requested due to automatic review settings October 19, 2026 21:06
@ReubenBond
ReubenBond force-pushed the rb-feat-journaling-garnet-phase2 branch from 5dc87e7 to 1d51141 Compare October 19, 2026 21:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

This explicitly non-mergeable spike changes durability-sensitive routing and partition lifecycle contracts that require final human review.

No open issues 🎉

Review effort: Balanced

This branch has not been deployed

No deployments
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.

2 participants