Skip to main content

ServerStores

Struct ServerStores 

Source
pub struct ServerStores { /* private fields */ }
Expand description

Every store the server needs, opened once.

Implementations§

Source§

impl ServerStores

Source

pub const HASH_MARKER: &'static str = "OBJECT_FORMAT"

The file that says which hash a store’s objects are: sha1 or sha256, one line, written when the store is first opened. Not the engine’s — a store carries no marker of its own, and opening a sha1 archive as sha256 does not fail, it decodes garbage. --adopt reads it; nothing else does.

Source

pub fn open(root: impl AsRef<Path>) -> Result<Self>

Open everything under root, creating whatever is not there yet.

The handle cache is sized to the descriptor limit this process has — after crate::limits::nofile has raised it as far as it may — so the cache can never by itself put the process over RLIMIT_NOFILE.

Source

pub fn open_with_capacity( root: impl AsRef<Path>, capacity: usize, ) -> Result<Self>

ServerStores::open, with an explicit handle-cache bound.

Exists so the eviction path can be tested at a capacity a test can actually exceed. A bound that is only ever exercised at 256 is a bound nothing has ever seen fire.

Source

pub fn open_with_engine( root: impl AsRef<Path>, capacity: usize, engine: Engine, ) -> Result<Self>

ServerStores::open_with_capacity, with the engine named rather than resolved from the environment.

The one seam a head-to-head bench needs: the same data root opened twice, once per engine, from one process — which is what makes an Arrow-vs-gix row a comparison rather than two anecdotes.

Source

pub fn await_enrolment(&self, keys: BTreeMap<String, String>)

Say which fed keys this box will enrol and has not yet — the whole set, replacing the last (crate::fed_principals::note_fed).

Source

pub fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String>

The fed key under fingerprint this box will enrol and has not yet, if any (ServerStores::await_enrolment).

Source

pub fn root(&self) -> &Path

Source

pub fn identity(&self) -> Option<Arc<GunnarIdentity>>

The identity core of this box, once the server built it.

Source

pub fn set_identity(&self, identity: &Arc<GunnarIdentity>)

Hand the box’s identity core to the paths outside a gRPC context (once; a second hand-over is ignored: one core per box). Held weakly: the caller keeps the core alive (the server’s context does).

Source

pub fn slow(&self) -> &SlowRoot

The slow disk this server writes its off-stripe copies to.

Always present. See crate::slow for what goes there and for the three-valued claim over it.

Source

pub fn layout_config(&self) -> &Load

What the installer decided, or the sentence saying there is no config. Read once at open; see the field’s documentation.

Source

pub fn audit_path(&self) -> PathBuf

Where the control-plane audit trail lives: <slow>/audit/ — one of the three copies that must be on another failure domain.

It used to be <control dir>/audit-chain.jsonl, on the stripe with the repositories whose history it records. An existing chain is ADOPTED into the new place once, on the first boot of a build that knows about the slow disk (slow::adopt_file: copy, fsync, unlink — rename answers EXDEV across devices, which is exactly the case that matters). If the move cannot be made the old path is returned unchanged and the server keeps writing the trail where it already is.

On a twin, or on a box mid-restore, it is a DIFFERENT file (audit-chain.local.jsonl, gunnar_core::txlog::AUDIT_LOCAL_REL). Such a box holds the primary’s trail as replicated state and must originate nothing into it: its own gate decisions — who browsed it, who was refused — are its own, and interleaving them into a hash chain that has exactly one writer would break every prev after the splice. The twin’s own trail stays on the twin and is never sent anywhere.

Source

pub fn mirror_identity(&self)

The identity half of the metadata mirror: every principal and every enrolled public key, as one JSON document off the stripe.

Public material only — a fingerprint, an authorized_keys line, a principal’s id and name. No secret in this file, ever: the secrets are sealed in control.redb under a KEK that is a separate file, and a mirror that carried them would make the slow disk a second place to steal them from.

Best effort, and deliberately so at every call site: this runs after a principal or a key has ALREADY been written to the control store, and failing the verb because the mirror could not be refreshed would turn a full slow disk into an outage of the identity system. A failure is one WARN naming the path.

Source

pub fn storage_reality(&self) -> StorageReality

The storage reality of this machine, for Admin.StorageInfo.

Three layers that are kept apart because they age differently: what the installer DECIDED (historical, from the record), what is TRUE NOW (the configured filesystems, read here), and what gunnar is CONFIGURED to do (the placement table as built). One artifact behind the first and third — crate::layout_config and crate::slow — so the console and the placement code cannot drift into two views of the same facts.

Nothing here surveys the machine. The only paths measured are the two gunnar was told to use; no mount table is walked, no device is enumerated, and the installer’s decisions are never inferred from what happens to be mounted now. A record that is missing is reported as missing.

Source

pub fn engine(&self) -> Engine

The storage engine this process is running.

Source

pub fn repos_root(&self) -> &Path

Where served repositories live.

Source

pub fn activity(&self) -> &Arc<PrincipalActivity> ⓘ

The identity table (the ACL model). Who signed in and who changed something, and when (2026-10-21) — the stamps Access.ListPrincipals answers beside each principal.

Source

pub fn auth(&self) -> &AuthTable

Source

pub fn catalog(&self) -> &RepoCatalog

The repository catalog.

Source

pub fn policies(&self) -> &PolicyStore

Per-repository policy documents (refs/gunnar/policy).

The one home for branch protection and ref-name policy since Policy.* stopped answering Unimplemented. gunnar-policy.json beside a repository is still read as a fallback, and transport::load_policy says why and for how long.

Source

pub fn mirrors(&self) -> &MirrorStore

Pull-mirror registrations (refs/gunnar/mirrors).

The server’s own registry, reachable over Mirror.*. Not the same thing as gunnar-client’s mirrors.json, which is the pull-mirror client’s state on a host running gunnar mirror.

Source

pub fn account_audit(&self) -> &Arc<AccountAuditLog> ⓘ

The flow ledger: what moved, per namespace, per UTC day.

Handed out as an Arc because its holders outlive any one borrow — the transfer path keeps a clone for the length of a transfer, the flush tick keeps one for the length of the process — and because every holder must share the one set of live atomics. See the field. Every account’s audit log — read only by each account’s admins (Enterprise.AuditLog), written by the control plane’s handlers.

Source

pub fn names(&self) -> &NameFilterStore

The name filter — its lists and the accounts’ review queues.

Source

pub fn encryption_keys(&self) -> &EncryptionKeys

People’s encryption keys (Access.GetEncryptionKey and friends, Repos.RecipientKeys).

Source

pub fn settings(&self) -> &SettingsStore

The generic server settings (Admin.GetSetting / Admin.SetSetting).

Source

pub fn usage_ledger(&self) -> &Arc<UsageLedger> ⓘ

Source

pub fn metrics(&self) -> &Arc<ServerMetrics> ⓘ

The server’s one set of counters (see the field).

Source

pub fn flow(&self) -> &Arc<FlowLedger> ⓘ

Source

pub fn snapshot_pin(&self) -> SnapshotPin<'_>

Pin the stores for a snapshot streamed from a cut (R5.1-A, 2026-10-07): until the guard drops, ServerStores::wait_unpinned holds back a gc, a repository store’s removal and an LFS object’s removal — the objects reachable from the cut’s tips, the stores it names and the LFS objects it lists stay where the stream will read them. Take it BEFORE the log’s freeze; the ones who wait take it before their own mutation guard, so a freeze can never wait on a thread that waits on a pin.

Source

pub fn wait_unpinned(&self, what: &str)

Wait while a snapshot streams from a cut (ServerStores::snapshot_pin) — at most SNAPSHOT_PIN_WAIT, after which the caller goes on and that snapshot fails at the piece that needed what was removed (the twin asks again; it never installs half). what names the caller in the line said while it waits. Called BEFORE any mutation guard.

Source

pub fn txlog(&self) -> &Arc<Log> ⓘ

The catalog and the identity table, as one store (the certificate-identity plan). The transaction log — what the twin fetches and what a restore replays.

Source

pub fn is_twin(&self) -> bool

Is this box a twin right now? Set at open from twin.json, and by ServerStores::mark_twin the moment Bootstrap.BecomeTwin confers the role on a running box. NOT decided once, whatever this comment used to say.

Source

pub fn twin_flag(&self) -> Arc<AtomicBool> ⓘ

The shared twin flag, for the front door — the same Arc ServerStores::mark_twin writes, so a box conferred the twin role over the wire refuses git from that instant instead of from the next restart. See gunnar_ssh::Control::with_twin_flag.

Source

pub fn mark_twin(&self)

From now on this box is a twin (crate::twin::become_twin).

Source

pub fn secrets_unframed( &self, ) -> Result<SealedSecretStore<Arc<dyn SecretStore>>>

The sealed secret store over the UNLOGGED half: what a twin reads its replicated host key through (a twin frames nothing).

Source

pub fn adopt_kek(&self, bytes: &[u8]) -> Result<()>

Hold bytes as this process’s KEK (S9): what a follower does with a Kek frame once the key file is durable, so the sealed rows it holds open from now on.

Source

pub fn kek_bytes(&self) -> Vec<u8> ⓘ

The KEK this process holds, as bytes — a snapshot’s Kek item when no key file exists (the key was handed in by the environment).

Source

pub fn raw_control_store(&self) -> &Arc<dyn ControlStore> ⓘ

The control store with no log in front of it — the twin applies the primary’s ControlBlob / ControlDoc frames here, and nothing else writes here.

Source

pub fn raw_secrets(&self) -> &Arc<dyn SecretStore> ⓘ

The secret store with no log in front of it — the twin’s Secret frames land here as the sealed bytes they are.

Source

pub fn control_store(&self) -> &Arc<dyn ControlStore> ⓘ

Source

pub fn control_db(&self) -> Arc<ControlDb> ⓘ

The redb handle under the control store — see the field.

Source

pub fn suspended(&self) -> bool

Are the repository stores closed for a growth?

Source

pub fn suspend_repos(&self, why: &str)

Close every repository store for the runtime growth (crate::grow, DATA-SET-GROWTH-FLOW.md G6): from here until Self::resume_repos repo_store refuses with why, the handle cache is emptied and the engines release what they cached — the packs’ mmaps, the redb tails, the descriptors that would otherwise pin /data. A store still held by an in-flight request (there are none: the drain finished first) drops when that request returns.

Source

pub fn resume_repos(&self)

The stores may open again (lazily, on the next request), after the growth mounted /data back.

Source

pub fn control_dir(&self) -> &Path

Where the KEK and the legacy Arrow secret log live. See CONTROL_DIR.

control() used to be here — the one accessor on this type that opened a repository, and the reason the server needed a storage engine to boot. There is no control repository any more, so there is no accessor for one.

Source

pub fn secrets(&self) -> Result<SealedSecretStore<Arc<dyn SecretStore>>>

The server’s secret store, sealed.

Everything on the boot path must go through this rather than through the raw handle: the raw handle writes plaintext, and the SSH host key is the one secret whose whole purpose is to be persisted. The decorator is the only thing standing between a private key and a file designed to be copied off the machine — and now that the store is a table in control.redb, “the file designed to be copied off the machine” is the same file the catalog is in.

Still Result, and deliberately: the signature is what every caller on the boot path is written against, and the store behind it may yet be one that can fail to open (VaultSecretStore is still noted-not-built). It no longer opens anything.

§And what it does with a value that is not sealed

It refuses it, unless Self::legacy_secrets says a pre-sealing secrets.arrow is on this disk. That is not belt-and-braces: reading an unsealed value through is a way past the seal that needs no KEK at all, so anything that can write control.redb — a restored backup, a tampered cold-tier archive, a replica’s copied file — could put its own SSH host key in the slot and gunnar would serve it. Only the machine that has the old log has a legitimate unsealed value to read, and only that machine gets the policy that reads one.

Source

pub fn transparency_key(&self) -> Result<PrivateKey>

The transparency log’s signing key, minting one on first use.

Its own key and not the issuer’s: the issuer says who somebody is, and this says what the log held. One key for both would mean compromising the log’s signer also mints administrators, and would make revoking one job impossible without revoking the other.

Atomic across racing callers, exactly as the issuer key and the host key are: two heads issued at once produce one key, not two.

Source

pub fn transparency(&self) -> TransparencyLog

The transparency log over this server’s push attestations.

Source

pub fn build_log(&self) -> TransparencyLog

The transparency log over this server’s build records.

The same machinery over a different file: a build record and a push attestation are different shapes with different readers, and mixing them into one file would break every reader of the other.

Source

pub fn forge_log(&self) -> TransparencyLog

The transparency log over this server’s forge events.

Issues, reviews and merges, each a row signed by the principal that did it — so an approval is evidence in the same sense a signed commit is, and not the server’s word for it.

Source

pub fn membership_log(&self) -> TransparencyLog

The transparency log over this server’s group-membership changes.

The same machinery over a different file: each line is an add or remove signed by the acting admin’s own key, so who was in a group at any time is evidence a third party can check and not the server’s word. The live membership table (gunnar_core::AuthTable) stays the authority the SSH listener resolves grants through; this is the durable, signed record of how it came to say what it says.

Source

pub fn provenance_key(&self) -> Result<PrivateKey>

The builder’s provenance signing key, minting one on first use.

A third key beside the issuer’s and the log’s, because it is a third job — and because a runner on its own host needs a key of its own that revoking the log’s signer does not take with it.

Source

pub fn webhook_key(&self) -> Result<PrivateKey>

The outbound webhook signing key, minting one on first use.

A fourth key beside the issuer’s, the log’s and the builder’s, because a delivery signature is a fourth job: it says “this event body came from this server”. Its PUBLIC half is what a webhook receiver pins and checks each delivery against with ssh-keygen -Y verify — no shared secret, so a leaked delivery URL is not a leaked signing key. Sealed under the KEK and atomic across racing callers, exactly as the keys above are.

Source

pub fn forge_attest_key(&self) -> Result<PrivateKey>

The forge-attestation key, minting one on first use (GHUNT-19): what gunnar signs a person’s forge event with when it came from their stepped-up session (gunnar_store::FORGE_ATTEST_KEY). Sealed under the KEK and atomic across racing callers, exactly as the keys above are.

Source

pub fn forge_attest_public(&self) -> Result<Option<PublicKey>>

The forge-attestation key’s PUBLIC half, if one was ever minted — read without minting, so a reader (a twin included) never writes a secret.

Source

pub fn pki(&self) -> Result<Pki>

The server’s certificate issuer (the certificate-identity plan).

Built from the two halves that must never be one: the control store, which holds the issuer certificate and the trusted-CA list, and the sealed secret store, which holds the private key. It is Self::secrets that is handed over and never the raw handle — the raw handle writes plaintext, and this is the one key on the machine whose loss means an attacker issues themselves an administrator rather than having to break anything.

The two halves are now two tables rather than two files, and the boundary that protects the key is the seal rather than the file: the KEK is in secrets.kek beside the database and not in it, so a copied control.redb carries a certificate it cannot sign with.

It no longer needs a storage engine. Neither does Self::pki_readonly, which is why both are here and only the difference between them is about what they may write.

Source

pub fn pki_readonly(&self) -> Result<Pki>

The issuer certificate and the trusted-CA list, and nothing that can sign.

gunnar issuer show is a control-plane read. The returned handle refuses every secret-store call by construction, so it cannot mint, import or issue — see gunnar_core::Pki::read_only. That is now the only thing separating it from Self::pki: since the secrets moved into control.redb, “without an engine” describes both, and the distinction this method carries is a capability one rather than a dependency one.

Source

pub fn install_plan_gates(&self, gates: Vec<Arc<dyn PlanGate>>)

The identity table and catalog together, as gunnar-ssh wants them.

Handed the shared write-gate so the front door refuses a client push on a standby. Every Control this returns clones the SAME gate, so SSH and smart-HTTP freeze together the instant a role change flips it. Install the plan-gate chain a mirror sync is held to — the front doors’ own, in their order. Replaces whatever was installed: the chain is built once per serve, and the newest build is the one in force.

Source

pub fn install_space_ledger(&self, ledger: Arc<SpaceLedger>)

Install the data set’s space ledger — the disk gate’s (gunnar_ssh::DiskHeadroomGate::ledger). Replaces any earlier one.

Source

pub fn space_ledger(&self) -> Option<Arc<SpaceLedger>>

The installed space ledger, if any.

Source

pub fn plan_gates(&self) -> Vec<Arc<dyn PlanGate>>

The installed chain, in the order it is asked. Empty: no ceiling.

Source

pub fn ssh_control(&self) -> Control

Source

pub fn browse(&self) -> Arc<AtomicBool> ⓘ

The shared browse flag. true = this box is a browse replica: a frozen standby that serves anonymous reads and refuses every control-plane mutator and its own promotion. Same one value as Self::writes_frozen’s sibling (one implementation).

Source

pub fn writes_frozen(&self) -> Arc<AtomicBool> ⓘ

The shared write-gate. true = this box is a frozen standby.

The replication slot writes it on a role change; the SSH/HTTP front doors read it on every push. One value (one implementation).

Source

pub fn store_opens(&self) -> u64

How many repository stores this process has opened so far — see the store_opens field. Monotonic, so a caller measures a delta across the operation it is pricing. The measurement behind the red: a plain Mirror.ReplicationStatus must leave this unchanged.

Source

pub fn repo_dir(&self, path: &RepoRef) -> Result<PathBuf>

Where a repository’s git directory is, checked against symlink escapes.

The string sandbox in gunnar_core::resolve_repo_path cannot see a symlink; that check lives in RepoRef::path_under, and this is the one place gunnar calls it.

Source

pub fn open_repo_capacity(&self) -> usize

The bound the handle cache is currently holding itself to.

The companion to Self::open_handles, which answers how many are held but not what against. reclaim_under_pressure MOVES this number, so a caller — or a test — that reads only the count cannot tell a cache that evicted from a cache that was cut.

Source

pub fn set_open_repo_capacity(&self, capacity: usize) -> usize

Lower (or raise) the handle bound and evict down to it now. Returns how many handles were dropped.

The same call Self::reclaim_under_pressure makes, exposed because that one is reachable only from a /proc/meminfo reading below RECLAIM_FLOOR_PERCENT — a state a test cannot put a shared build box into, and the reason the reclaim path is the one half of the bound that gunnar-cli/tests/handle_cache.rs could not reach. Eviction on insert is covered there and is not duplicated here (one implementation); eviction on pressure had never run outside the appliance it was written for.

Source

pub fn repo_store(&self, repo: &Repo) -> Result<Arc<OpenRepo>>

The stores for one catalogued repository, opening it if this is the first request for it.

The returned Arc is what keeps the handle alive: while a caller holds one, the evictor will not drop that repository from the cache, so an in-flight clone cannot have its store lock pulled out from under it.

Source

pub fn apply_explode_quota(&self, namespace: &str, dir: &Path)

Move every open store under dir onto the namespace’s quota.

Called when a store opens and when an admin changes the plan. A failure to read the catalog is logged and the default stands: a quota that cannot be read must not stop a repository serving, because the worst it costs is a cache that is the wrong size and the best it costs is an outage.

Source

pub fn point_head(store: &OpenRepo, repo: &Repo) -> Result<()>

Make an open repository’s HEAD name the default branch its catalog entry carries.

The only place the server projects the catalog onto a repository’s HEAD (one implementation), and it is called from exactly two: Self::repo_store on the open path, so create and every transfer after a restart carry it, and ControlPlane::set_default_branch, which has to re-run it because the handle it needs is already in the cache and repo_store returns early for those.

A move is logged at info. It is a change to what a clone checks out and it happens without anyone asking on the open path, so it must be visible in the log of the repository that had it done to it.

Source

pub fn repoint_head(&self, repo: &Repo) -> Result<()>

Self::point_head for a repository this server has catalogued, opening its store first. Used after the catalog’s default branch moves.

Source

pub fn adopt_default_branch_if_unborn( &self, path: &RepoRef, pushed: &[String], ) -> Result<Option<String>>

Adopt a pushed branch as the default branch of a repository whose HEAD still resolves to nothing, and report the name if one was adopted.

§Why a push is allowed to change this

repo create gives every repository refs/heads/main, and a donor whose branch is master therefore lands in a repository whose HEAD names a branch that does not exist and never will. That repository cannot be cloned: HEAD has no object, so it is not advertised, and stock git clone writes an empty worktree, prints “remote HEAD refers to nonexistent ref” and exits 0. Nothing between the successful push and the empty checkout says a word about the default branch, which is what cost the roundtrip harness a day on 2026-08-06.

So the first branch to arrive in a repository with an unborn HEAD is adopted, which is what every hosted forge does and what makes push alone produce something clonable. It is deliberately narrow:

  • it fires only when HEAD resolves to nothing after the push, so the only repositories it can touch are the ones that are unclonable as they stand — if the configured default branch was among the ones just pushed, HEAD resolves and this does nothing;
  • it considers only refs/heads/… that the push created or moved, never a delete and never a tag; and
  • the choice goes through the catalog and then through Self::point_head, so the authority and its projection still move together and there is still one writer of HEAD.

It is logged at warn, not info: the server chose a default branch nobody asked for, and the operator has to be able to find that decision later. set-default-branch overrides it.

Source

pub fn create_repo(&self, spec: NewRepo, adopt: bool) -> Result<Repo>

repo create is transactional, and it never adopts a store it did not make. The one verb every create routes through: the gRPC Repos.Create, the local gunnar repo create, and the first-repository door once a push has passed the hash check.

Order, and why each step is where it is:

  1. Look at the disk first. If a directory already stands at the name — or a tombstone says a delete could not reclaim it — the create is refused by name, before the catalog is touched. This is the 2026-08-30 resurrection: on v1.1.3-166 repo delete dropped the catalog row and left the store, and the next repo create of the same name opened it — ls-remote right after the create listed all 59 refs of the deleted skade, and recreated --hash sha256 the name served sha1 refs (fatal: protocol error). adopt is the opt-in for the one legitimate case (a catalog restored from backup over a surviving data disk), and only for a store whose hash marker matches.
  2. The catalog row.
  3. The store. If the engine cannot make it, step 2 is rolled back and whatever the engine left is removed: a create that failed leaves no directory, no row, nothing for repo get to find.

Returns the catalogued repository; the handle is in the cache.

Source

pub fn refuse_stale_store( &self, path: &RepoRef, adopt: bool, hash_kind: HashKind, ) -> Result<bool>

Step 1 of Self::create_repo: nothing may stand at path on disk.

Returns true when an existing store was ADOPTED — the one case in which the directory was not made by this create, and therefore the one case in which Self::create_repo’s rollback must not remove it.

§--adopt means “take it as it is”, including a store from before

the marker existed

MEASURED on the appliance 2026-08-30, v1.1.3-227-g6a2367a, request 175daccc0000000d:

repo create alice/linux --hash sha1 --public --adopt
  -> failed precondition: cannot adopt the store at
     /data/repos/alice/linux.git: it carries unmarked objects, the
     create asked for sha1

The marker is a 2026-08-30 file. Every store written before that day has none, so an --adopt that requires one refuses precisely the stores adoption exists for — a catalog restored over a surviving data disk, or the orphan an old catalog-only delete left. Composed with a create that refuses the name and a delete that cannot authorise it, that is a store with no way out at all.

The honest rule, and the one implemented here:

  • unmarked — there is no second opinion to check, so the caller’s --hash is taken at their word AND WRITTEN into the marker. The store is marked from then on, so the next adopt is checked; the caller’s word is recorded rather than merely trusted.
  • marked and equal — adopted, as before.
  • marked and different — refused, as before. This is the arm that stops a sha256 row over a sha1 store from serving refs no client can read, and it is not weakened: a store that carries a marker is checked against it.
Source

pub fn tombstone_path(dir: &Path) -> PathBuf

Where a name’s tombstone lives: beside its directory, so a store that could not be removed and its marker are removed together by the retry.

Source

pub fn is_tombstoned(&self, path: &RepoRef) -> Result<bool>

Is path tombstoned — deleted from the catalog, store not reclaimed?

Source

pub fn open_handles(&self) -> usize

How many repository handles are open. For tests and Admin.Stats.

Source

pub fn close_repo(&self, path: &RepoRef)

Forget a repository’s handle, releasing its store lock. Used by repo delete --purge before the directory is removed.

Source

pub fn purge_repo(&self, path: &RepoRef) -> Result<(u64, u64)>

repo delete reclaims the disk. Stop the repository’s index work, release its handle, remove its directory — pack, journal, tail, exploded tier and index tables — and report the bytes that went.

MEASURED 2026-08-29 on the appliance: a repo delete after the linux push left 25 GB of gunnar-git.znippy/ behind, because the catalog row was removed and the directory was not. Order here is the point:

  1. the engine cancels the index worker (an in-flight pack is aborted at its next checkpoint, milliseconds, never finished — its rows are keyed by oid and the directory is about to go);
  2. the handle cache forgets the repository, so the store drops as soon as the last request holding it returns (the worker is cancelled, so the drop does not join hours of work);
  3. remove_dir_all. A store still held by an in-flight request keeps its open fds on unlinked files until it returns; the bytes are reclaimed when it does, and the directory entry is gone now.

Returns (bytes_before, bytes_after) measured by walking the directory, so the log line and the caller report applied output.

A reclaim that fails leaves the name TOMBSTONED (2026-08-30): a file beside the directory saying when and why, so Self::create_repo refuses the name until repo delete is run again and succeeds. Without it a failed remove_dir_all (a store held open on a read-only mount, a permissions slip) left a directory the next create would refuse by name anyway — but with no sentence saying that a delete had been asked for and had not finished.

Source

pub fn reclaim_repo(&self, path: &RepoRef) -> Result<(u64, u64)>

The removal half of Self::purge_repo, unframed: what the twin’s apply of a RepoStore purge frame runs.

Source

pub fn soft_delete( &self, path: &RepoRef, repo: Option<&Repo>, principal_id: &str, retention: Duration, ) -> Result<Tombstone>

repo delete without --purge: a DATED TOMBSTONE, not a reclaim.

The catalog row is the caller’s to remove; this is everything else. The bytes stay exactly where they are, the name becomes unusable (Self::refuse_stale_store refuses a create over a tombstone), and a deadline is written into the tombstone at this instant.

Why this exists at all, in one measured line from MEASURED: repo delete --purge returns the bytes in ~2 seconds — 181 GiB to 33 MiB — and a live mirror does not help because the delete replicates. The failure a first paying customer is most likely to cause is deleting their own repository, and the only thing that answers it is a window.

What it does NOT do, and deliberately:

  • it does not touch the bytes, so it cannot fail on a full disk;
  • it cancels the index worker and drops the handle, because a repository nobody may reach should not be spending CPU or file descriptors on being resolved;
  • it does not delete the tombstone of a failed reclaim — that record is owed to an operator. Soft-deleting a name already tombstoned that way is refused by name.

retention of zero is legal and means purge at the next sweep: an operator reclaiming space said so, and a zero that silently became thirty days would be the server overruling them.

Source

pub fn undelete_spec(&self, path: &RepoRef) -> Result<NewRepo>

repo undelete: put a soft-deleted repository back.

Returns the NewRepo the caller must re-insert into the catalog, and removes the tombstone. The two halves are split because the catalog is the caller’s — the gRPC path and the local CLI reach it differently — and because the row must go in before the tombstone comes off: a crash between them leaves a repository that is still fenced off, which is recoverable, rather than a name a create would adopt, which is the resurrection the reclaim rules closed.

The recorded hash kind is the one that comes back, never a default. A tombstone that does not carry a row (one written before this existed) is refused BY NAME rather than restored as sha256 over a sha1 store.

Source

pub fn clear_tombstone(&self, path: &RepoRef) -> Result<()>

Take the tombstone off, once the catalog row is back.

Source

pub fn read_tombstone(marker: &Path) -> Option<Tombstone>

Read a tombstone file, or None when there is not one.

Source

pub fn tombstone_of(&self, path: &RepoRef) -> Result<Option<Tombstone>>

The tombstone beside path, if any.

Source

pub fn tombstones(&self) -> Result<Vec<DeletedRepo>>

Every soft-deleted repository, with the bytes it is still holding.

This is what makes a tombstone visible on a bill: the bytes are real, the account is still using them, and a customer who deleted a repository and saw no change in their usage would rightly call that a bug. Admin.StorageInfo reports the total and repo list-deleted prints the rows.

It walks repos/ rather than asking the catalog, because the whole point of a tombstone is that the catalog does not know about it.

Source

pub fn orphans(&self) -> Result<Vec<OrphanStore>>

Every store directory with no catalog row and no soft-delete tombstone.

MEASURED on the appliance 2026-08-30 (v1.1.3-227-g6a2367a), the wreckage of Friday’s kernel-mirror incident: /data/repos/alice/linux.git stood on the disk with ~25-39 GB in it, no catalog row and no tombstone. Three guards, each right on its own, composed into a state with no exit — create refused the name because the directory was there (175daccc0000000c), create --adopt refused it because the store predated the OBJECT_FORMAT marker (…0d), and delete --purge refused it because the access check was Access::repo on a repository not in the catalog, which no principal can ever hold a grant on (…0b/…0e). The bytes were stranded and the NAME was permanently unusable.

This is the door that was missing. It walks repos/ rather than asking the catalog, exactly as Self::tombstones does and for the same reason: the whole point of an orphan is that no record of it exists.

What counts as an orphan, and why it is drawn here:

  • no catalog row — a catalogued directory is repo list’s;
  • no Deleted tombstone — one of those is repo list-deleted’s, and it has undelete and purge as its exits;
  • a ReclaimFailed tombstone IS an orphan. tombstones() filters that reason out of list-deleted deliberately (bytes owed, not bytes held), which left it in no listing whatsoever; the row carries the note so an operator sees why.
Source

pub fn is_orphan(&self, path: &RepoRef) -> Result<bool>

Is there a store at path that no record accounts for?

The one question both delete doors ask, so they cannot answer it differently (one implementation): a directory on disk, no catalog row, and no Deleted tombstone. A ReclaimFailed tombstone still counts as an orphan — those bytes are owed to an operator and no listing but Self::orphans shows them.

Source

pub fn orphan_soft_delete_refusal(path: &RepoRef) -> String

What a delete of an orphan may and may not do, as one sentence both doors print.

--purge on an orphan is legitimate and is exactly what repo reclaim does: the operator said “the bytes go”, the row is already gone, the bytes are there. A soft delete of one is not, and the reason is the soft delete’s own contract — it promises repo undelete puts the repository back as it was, and for an orphan the server does not know what it was: no hash kind, no default branch, no visibility. Lane AC already refuses to restore a tombstone that carries no row; refusing to WRITE one is that same rule, one step earlier, before a deadline has been put on bytes nobody can bring back.

Source

pub fn reclaim_orphan(&self, path: &RepoRef) -> Result<u64>

Reclaim an orphan’s bytes. The exit that did not exist.

It refuses anything that is not an orphan BY NAME, because both of the other two states have their own verb and reaching them through this one would be a second way to destroy a repository:

  • catalogued -> repo delete --purge;
  • soft-deleted -> repo undelete or repo purge.

The reclaim itself is Self::purge_repo — the same verb --purge and the sweep use, so an orphan’s bytes go the way every other repository’s bytes go (one implementation), including the index worker being cancelled first and a ReclaimFailed tombstone if it cannot.

Source

pub fn sweep_expired_tombstones(&self, now_unix_ms: i64) -> Vec<(RepoRef, u64)>

The sweep: reclaim every tombstone whose window has closed.

Returns what it reclaimed, (path, bytes). A failure on one repository is logged and the sweep continues — one undeletable directory must not stop the other nineteen from being reclaimed, and purge_repo leaves its own ReclaimFailed tombstone behind so the failure is not lost.

Source

pub fn repo_of_dir(&self, dir: &Path) -> Option<(String, String)>

Which catalogued repository the git directory dir belongs to, as namespace/name — the inverse of Self::repo_dir, for a progress row that knows only a store root.

Trait Implementations§

Source§

impl Debug for ServerStores

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoRequest<T> for T

Source§

fn into_request(self) -> Request<T>

Wrap the input message T in a tonic::Request
Source§

impl<L> LayerExt<L> for L

Source§

fn named_layer<S>(&self, service: S) -> Layered<<L as Layer<S>>::Service, S>
where L: Layer<S>,

Applies the layer to a service and wraps it in Layered.
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more