Repository navigation
Open
Description
Problem
Part 1 (#165) replaces a writer within one store. A store's loss, or a region's, needs copies of the log elsewhere, and a way to make one of them authoritative.
Solution
Part 2 of #97, under the plan approved on #97 (#97 (comment)) with the maintainer's answers. It takes #117 with it, because a log may be shipped only to a verified store. The release is the next free 0.1.0-preview.N after part 1. Its descriptor closes this issue, #117 and #97.
Deliverables
- ADR 0183,
varve storage verifyand the compatibility page (Object store verification: varve storage verify, provider result records and a generated compatibility page #117), first.- The contract suite moves into shipped code, with both race properties, run in a scratch prefix.
- Result records go under
testdata/object-stores/results/. - The page,
docs/operator/object-stores.md, is generated with four states and checked byeng/status.cs:- unsupported means known to break the contract (Garage);
- MinIO CE is untested, noting that the community edition is no longer distributed as binaries.
- Verify writes a verification record at the target endpoint, carrying its suite version.
- The gate's broken stores are
IgnoresConditionsandLosesRaces.LosesRacespasses the probe and fails the race property.
- ADR 0184, tier-3 log shipping.
- A per-dataset
varve:ShippingTargetdeclaration. - The shipper is a follower role, by default on the epoch holder.
- What's shipped, in this order: chunks, then sealed segments, byte-identical by create-if-absent; then extent sets; then the catalog last. Not the writer record.
- Resume is by the target's tail, verified through the header chain.
- The shipper is a reader of the source under ADR 0160, and the target's maintenance writer.
- A target qualifies only with a verification record at that exact endpoint (suite version at or above the build's minimum), and with its provider verified on the compiled page. Otherwise
409 store-unverified, naming the page. Directory targets are exempt.
- A per-dataset
- ADR 0185, promotion.
varve promoteclaims a fresh epoch on the target's writer record (triggerpromotion), above every shipped chunk's epoch.- The
lineagerecord (replica→promoted) records the source and the last position shipped. - An open for write of a
replicais refused (409 dataset-is-replica). - The returning old primary is refused by
replica-promoted, and by the fence slot withshipping-diverged.
- ADR 0186, tier 2:
- a follower over the RA-GRS secondary, reading unannounced;
- lag from the account's
LastSyncTime, with a custom role forblobServices/read; - promotion after an account failover, discarding beyond the first gap with the operator's confirmation;
- Bicep for two regions behind Front Door.
- Active-active across copies: a dataset shipped to a second store and followed there.
- The close-out:
- README Status,
docs/roadmap.md(Replication complete), AGENTS.md State,eng/status.cs, the changelog; - the operator guide: shipping, promotion, RPO stated plainly, the compatibility page, the geo layout;
docs/problems/, and the traceability record.
- README Status,
Gate
- Tier 3 under failure injection: a follower of the target equals the reference model at its reported position.
- Byte identity: the target's log is byte-identical to the source's up to the shipped position.
- The promotion rehearsal in tiers 2 and 3: promote, write, then the old primary returns and is refused.
- Tier 2 on a real account in the optional
main-restricted leg, by workload identity federation, with no keys. varve storage verifypasses against Azurite and RustFS, and fails with the counterexample against the two broken stores.
AI assistance
Any AI-assisted session on this issue files a record in docs/traceability/ naming the tool and model (ADR 0033).
Metadata
Metadata
Assignees
Labels
No labels
Activity
chore(release): v0.1.0-preview.16, Replication within a store
Hafeok commentedon Nov 11, 2026
Starting part 2. This is a cloud session on branch
claude/zen-hopper-5e32m5, againstmainat2cad18d(v0.1.0-preview.20). The session was developed with AI assistance under human review.The first deliverable is a delta plan on this issue: what in the approved plan (#97, sections 5–8) no longer matches
main, and what I would change. I won't write any code until the maintainer has answered it.Generated by Claude Code
Hafeok commentedon Nov 11, 2026
Delta plan: Replication part 2 against
mainat2cad18d(v0.1.0-preview.20)This was developed with AI assistance under human review. It covers the plan approved on #97 (sections 5–8), checked against
maintoday. Each claim about the code carries a path. Nothing below re-opens a ruling. Wheremainforces a change, I say what it is and what I would do. Where I need you to decide, it is marked D1–D9, and they are collected at the end. No code until you answer.Found and filed on the way: #283 (two passages of the storage format spec that disagree with ADRs 0217 and 0161) and #284 (ADR 0241 is filed by both #259 and #264).
1. What in sections 5–8 no longer matches
main§5, tier 3
maintodayExtentSet.BlobsOf(src/Varve.Store/Extents.cs:87-117) namesorder0–order5,terms,graphsandcommits, butcommitsis absent in pre-0161 sets andgraphsmay be absent. Format 5's seek columns live inside eachorderK(storage format §7.5). There is no separate seek-directory object.BlobsOfnames for the set, not a fixed nine.derived/index/runs/, which are local cache and never in the store (ObjectDerivedStore.cs:111,Shared: extents and catalog only).BlobsOfand nothing else.readers/<id>-shipperReaderRecordFormat.cs, storage format §8.1) hascatalogOldest,catalogNewest,stateandmaxDerived. A follower uses<ReaderId>-follower(ADR 0164, amendment 2026-10-20).<ReaderId>-ship-<target-id>, where target-id is a short digest ofvarve:storeandvarve:prefix. Two targets of one dataset must not share an interval. The record still protects what it copies: the shipper announces the catalog version it copies before its first read, and moves the interval forward only after the target's catalog is replaced.termsandcommitsare written as version 4 while any reader record saysmaxDerived< 6. The source writer rewrites old sets oldest first, and an older build treats a version-6 blob as a miss.shipping: waiting-on-damaged-set. A target reader of an older build is answered by replay, never by a wrong result.Dataset.ShipAsync(src/Varve.Store/Replicas.cs:29-94) re-creates segments through Append/Seal and writes a fresh catalog, so its output is not byte-identical to the source.ShipAsynccopy is not a shipping target: resuming against one isshipping-divergedat slot 0, orshipping-target-not-empty.ShipAsyncstays as the one-shot bootstrap of ADR 0083, and the guide says which to use when.Dataset.AppendFailedandDataset.SpillBound(ADR 0179 point 6).varve applyrefuses unverified providers; runtimestore-unverifiedvarve applycan check only the page, so it does. D1: where the verification record lives (below).D1. I propose that the verification record is the object
varve-verificationat the root of the store URL given tovarve storage verify, the container or the bucket. The shipper reads it at the target'svarve:store. It falls under ADR 0217: a format this build doesn't read isstore-unverified, never "verified". The alternative is per prefix, which is stricter but makes every new target prefix need its own run.§6, promotion and lineage
promotionis reserved in no code.ClaimTrigger(src/Varve.Store.ObjectStorage/Model/ClaimTrigger.cs:8-26) hasRestart,Operator,Abdication,ProbeandUnknown. ADR 0179 point 1 rules only that promotion is an operator's act (Replication part 2: verified stores, log shipping, promotion, geo-replication #166). ADR 0185 adds the trigger, and ADR 0136 gets the amendment. Older builds read it asUnknownand never act on it (ADR 0217 point 2), so the record staysformat: 1.TheLineageRecordFollowsTheSameRule) and in ADR 0216's inventory as "not onmain". ADR 0185 cites both, and the format inventory gains its fixture.promote" has changed under part 1. The plan hadvarve promoteclaim on the target's record directly. In a fleet, part 1's start rule (MemberDecision.cs:64-77) would then see a holder that is not in the writer set and claim again at once. That is two epochs for one promotion, and the warmth is lost. D2 below proposes promotion as a request to an instance, like the takeover.ObjectStorage.AdoptedEpoch(ObjectStorage.cs:243) already exposes it on a follower, and a target with no writer record gets one created at that epoch.§7, tier 2
Nothing exists yet: no secondary endpoint, no
LastSyncTime, nocomp=statsanywhere insrc/ortests/. The Bicep templates offerStandard_GZRSbut not RA-GRS or RA-GZRS (deploy/azure/container-apps/main.bicep:52-59), and there is no geo template.A custom role has a precedent:
container-apps-handover/main.bicep:653(jobStarter). The stats role follows its shape.Azurite and the secondary. I don't know whether Azurite 3.37.0 serves
-secondaryreads andGet Blob Service Stats, so I won't assume it. The first slice of pull request 3 is a spike that answers it. If it does, the geo double runs over Azurite as well as memory. If it doesn't, memory only, and the adapter's secondary path is proved only on the real-account leg, which says so.The real-account leg.
azure-objects.ymlexists (dispatch only, environmentazure-objects,AZURE_TENANT_ID,AZURE_CLIENT_IDandVARVE_AZURE_CONTAINER), but it gates on one variable (ServiceLegs.Require,AzureAccountTests.cs:64-76), not on ADR 0223's rule. The tier-2 leg follows ADR 0223 decision 5, as the provider legs do (tests/Varve.Server.Tests/ProviderTests.cs:271-328):VARVE_GEO_ACCOUNT,VARVE_GEO_CONTAINERorVARVE_GEO_TENANT_ID(plusAZURE_CLIENT_ID) set: it skips, naming each;It is a separate workflow,
azure-geo.yml, in an environmentazure-geo. Its jobs: follow the secondary, report the last-sync time, and promote after a failover. The failover is dispatch only, because a customer-initiated failover takes hours and converts the account. A Bicep file anddocs/azure-geo-setup.md, written step by step in the shape ofdocs/auth-providers.md, are for you to set it up. Until you do, the close-out says tier 2's real-account gate has not run.§8, RustFS
Varve:ObjectStorage:StsEndpoint(ADR 0137, amendment 2026-10-27), RustFS behind mock-oauth2 in.github/storage/compose.yaml, andRustFsFleetTests(tests/Varve.Server.Tests/FleetTests.cs:1844). ADR 0182 is unused (docs/adr/README.md:382). Tier 3's S3 leg uses web identity, with no key.2. The promotion claim, the hold-off and the writer set (change 1)
Facts:
ClaimsHoldOffAfterAStoreFailure(ADR 0163, amendment 2026-11-10, Fleet: a writer whose first open fails never opens again until restart (Started set whatever the outcome) #251) andClaimAfterAReleaseByItsReasonrestated (ADR 0180, amendment 2026-11-10, Replication part 1 follow-ups: the releaser stands out of its own release's race, and a forward answers by what happened #216). It is per member, per dataset, in memory, and elapsed-time with jitter (src/Varve.Server/ClaimHoldOff.cs:22-79). Its round isVarve:Fleet:FollowInterval, with a cap of 30 s.placementrelease, and claims in place of a release. A takeover request "is answered whatever the hold-off" (0163:427), andTakeoverAsync(WriterSet.cs:313-349) never readsHeld.D2. Promotion is a request to an instance, as the takeover is (recommended).
PUT /manage/datasets/{d}/state {"promote":{"source":"<address>","position":p}}on the target fleet, with the operator role. The receiving instance answers it and never forwards it.promoted;promotion(ObjectStorage.ClaimInPlaceAsync, extended to create the record when it is absent);varve promoteis the client of that route (ManagementClient.RequestPromotionAsync), asvarve manage takeoveris of the takeover.varve promote --embedded <target>acts on the storage directly and then releases with the new reasonpromotion. The release lets the target's placed member start by the start rule ("the record is released") rather than by claiming over a live holder.409 takeover-refused's reasons where they apply (not-in-writer-set,cold), plusnot-a-replicaandsource-mismatch.How promotion sits with the hold-off:
promotion, notstorage-error. Astorage-errorrelease would start a standby race (0180's restatement), and a standby would then finish a promotion on its own, which would break "nothing in Varve promotes a replica on its own".dataset-is-replica. An open for write of a dataset whose lineage ispromoted, but whose record has nopromotionclaim that fenced, is refused with409 dataset-is-replica, reasonpromotion-incomplete. Only re-running the promotion completes it, and that is idempotent on the same lineage.dataset-is-replicais not a failure for the hold-off. A member placed on a replica opens it as a follower (it is not a failed open), so it never enters the hold-off, and it logs nothing at warning. Without this, every placed member of a target fleet would back off to 30 s and log a failure for as long as the replica exists.Does a shipper's failed write to the target start a hold-off of its own?
ClaimHoldOffkeyed by target, not as a ruling about claims.shipping: failingwith the target and the last error, and never turns unready, asretentiondoesn't (ADR 0162).The writer set on the target:
placedOnandstandbyOn.replica, every member opens it as a follower: no member claims, and the start rule never runs.503today. On a replica it becomes409 dataset-is-replica(ADR 0168 amendment), because retrying won't help.3. What governs a read of a shipped copy (change 2)
The target fleet's own declarations. Nothing that governs access is shipped.
log/, extent sets and the catalog. The log carries:boundGraphis a graph of the same dataset (ADR 0234 point 1,docs/operator/inference.md:44);Settingscommits ("every copy of the dataset agrees on them", spec §2);"p"^^varve:managementPosition,src/Varve.Protocol/Endpoints/Writes.cs:278,309-312).varve:ClassifierBinding,varve:TBoxBinding, shapes bindings and moved event declarations. All of these live in the source fleet's_fleet/(ADR 0166). Also not shipped: the classification and validation projections, which are local cache only (ADR 0233 point 6, ADR 0174 point 7). A target follower rebuilds them from its own fleet's bindings.A defect the plan didn't see: the recorded management position belongs to another fleet. A dataset on another store is necessarily served by another fleet, since a fleet's system dataset is
_fleet/on its own store. The integer recorded in each commit is a position in the source fleet's system dataset, and nothing records which fleet. On the target today:_order.Observe(src/Varve.Server/OpenDatasets.cs:692-694) and the write-path advance (ServerHost.cs:987-996,Fleet.Recorded) would seed the target's watermark from a source position. Writes are refused503 management-behinduntil the target's system head passes that number, or accepted against a meaningless one.EventCatalogues.AtCommitAsync(src/Varve.Protocol/Endpoints/EventCatalogues.cs:184-212) would resolve moved declarations (ADR 0167) at a source position in the target's system dataset. That gives the wrong catalogue, silently, ormanagement-behindfor ever.D3. The fix (in ADR 0185, amending ADR 0165).
_fleet/log/MANIFESTdataset id, read by the shipper. Every commit at or below the lineage's promoted position, and every commit of a replica, carries a foreign position.Recordedread only positions above the promoted one.varve:ShippingTarget, on_fleet, intofleets/<source-fleet-id>/on the target store. It is followed read-only. The same copy is what an operator needs after losing the source store, because without it the source's grants and placement are gone with it. Without the copy, an event of a foreign commit whose declarations had moved answers a new503 events-source-fleet-unavailable, and the guide says why._fleetat all, and the problem above is the only answer. That is simpler, but a region's loss then loses its declarations.4. The erasure precondition (change 3), as ADR 0184 would state it
One tension to hand to #203, not to resolve here. Spec §9 T4 has each replica destroy its own copy of a key. The roadmap's milestone 9 part 2 text has one key store outside every dataset, "so destroying a key erases on every replica at once". (a) is the roadmap's. ADR 0184 names the tension, and I'll comment it on #203.
5. The split, and each pull request's gate
The three-pull-request split holds. I found no better seam. Two notes:
eng/release.cs --sbom), and the problem pages need Problems that say what to do (#194, ADR 0242 filed unaccepted) #267 (ADR 0242's "What to do"). I'll write every new page with## What to dofrom the start, so it passes either way.Pull request 1, #117 (ADR 0183).
Completes: #117,Closes #117on its last commit.src/Varve.Store.ObjectStorage/Verification/(layer 5), with:RacersandRoundsas parameters;LosesRaces, a test double of a store that passes ADR 0134's probe (ObjectStorage.cs:838-901) and loses the race.tests/…/ObjectStoreContractCases.csbecomes a call into it, so there is one suite.varve storage verify <url> [--rounds n] [--record <file>]writes the result record and the endpoint's verification record.testdata/object-stores/results/gets the Azurite and RustFS records.eng/object-stores.csgeneratesdocs/operator/object-stores.md, andeng/status.csgains check 6.store-unverifiedgets its problem page.storage (services);IgnoresConditionsandLosesRaces(memory, every OS);Varve.Serveralready referencesVarve.Store.ObjectStorage,.Azureand.S3(src/Varve.Server/Varve.Server.csproj), so the CLI's closure gains code, not dependencies. ThetoolSBOM's digest changes, and the register doesn't. I rundotnet run eng/lockfiles.csand expect no diff, and I'll show it.Pull request 2, tier 3 and promotion (ADRs 0184, 0185).
Completes: none.ShippingTargetvocabulary and shapes;promotiontrigger and release reason;varve promote;dataset-is-replica,replica-promoted,shipping-divergedandshipping-target-not-empty;replica-promoted, and byshipping-divergedwhen it raced);shipping (services).Pull request 3, tier 2, active-active across copies and the close-out (ADR 0186).
Completes: #166, #97(#117 is closed by pull request 1). The descriptor commit carriesCloses #166andCloses #97.shipping (services);6. CI jobs, durations, nightly depth
The durations below are estimates from the existing jobs. I'll replace them with measured per-OS figures (
eng/test-durations.cs --label) on each pull request.storage (services)test-store(ineng/ci.cs)shipping (services)geo (account)inazure-geo.ymlmainonly, ADR 0223 ruleA failure in
shipping (services)that I can't reproduce goes on its own issue with the job id. I won't retry it until it's green.Nightly (
.github/workflows/nightly.yml, jobstore), each with its seed printed (docs/testing.md§8):VARVE_VERIFY_ROUNDSVARVE_SHIPPING_SCHEDULESVARVE_PROMOTION_SCHEDULESVARVE_GEO_SCHEDULESEach comes with its budget check at 80%.
7. ADRs and dated amendments
New ADRs, filed unaccepted. I checked every open pull request today, and none uses 0183–0186. I'll check again before filing.
I propose no ADR beyond these. No new package needs one, and the Bicep and the leg sit under ADR 0223 and ADR 0142's amendments.
Dated amendments (ADR 0068):
0134: the suite is shipped code;
LosesRaces.0135: the target's consolidation is done by its shipper.
0136:
promotionand the release reasonpromotion;replicalineage, and onpromotedwithout a fenced promotion claim;0137:
blobServices/readrole.0142: the geo layout beside the default and the handover layouts.
0160:
0163:
dataset-is-replicais not a failure for the hold-off;0164:
(Memory bounds for writing, checkpointing and reopening: a dataset written under a limit reopens under it (#227) #264 amends 0164 as well, and whichever lands second merges.)
0165: a recorded management position belongs to the fleet that wrote it (D3).
0166:
varve:shipsToandvarve:ShippingTarget, and their shapes.0167: events of foreign commits (D3).
0168: a write to a replica is
409 dataset-is-replica.0213: the shipper copies sets as they are, verifies checks before copying, and never rewrites.
0216: the lineage record, the verification record and the result record.
0217: the verification record follows its rule (point 4 already covers lineage).
0113 and 0119: readiness gains
shipping, and the problem catalogue gains its entries.Problem types, each with a
docs/problems/page with "What to do":store-unverified;shipping-diverged;shipping-target-not-empty;dataset-is-replica(with reasonsreplicaandpromotion-incomplete);replica-promoted;events-source-fleet-unavailable, only under D3's fallback.Decisions for you
varve promote --embeddedplus apromotionrelease where no fleet serves the target? Or should the CLI claim directly, as the plan had it?_fleet(proposed), or answer503 events-source-fleet-unavailable?azure-geo.ymlin a new environmentazure-geo(proposed), or a job inazure-objects.yml's environment?409 dataset-is-replicarather than today's503. Agreed?On your answer I file ADR 0183 and start pull request 1.
Generated by Claude Code