pub struct ServerStores { /* private fields */ }Expand description
Every store the server needs, opened once.
Implementations§
Source§impl ServerStores
impl ServerStores
Sourcepub const HASH_MARKER: &'static str = "OBJECT_FORMAT"
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.
Sourcepub fn open(root: impl AsRef<Path>) -> Result<Self>
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.
Sourcepub fn open_with_capacity(
root: impl AsRef<Path>,
capacity: usize,
) -> Result<Self>
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.
Sourcepub fn open_with_engine(
root: impl AsRef<Path>,
capacity: usize,
engine: Engine,
) -> Result<Self>
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.
Sourcepub fn await_enrolment(&self, keys: BTreeMap<String, String>)
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).
Sourcepub fn awaiting_enrolment(&self, fingerprint: &str) -> Option<String>
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).
pub fn root(&self) -> &Path
Sourcepub fn slow(&self) -> &SlowRoot
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.
Sourcepub fn layout_config(&self) -> &Load
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.
Sourcepub fn audit_path(&self) -> PathBuf
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.
Sourcepub fn mirror_identity(&self)
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.
Sourcepub fn storage_reality(&self) -> StorageReality
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.
Sourcepub fn repos_root(&self) -> &Path
pub fn repos_root(&self) -> &Path
Where served repositories live.
Sourcepub fn activity(&self) -> &Arc<PrincipalActivity> ⓘ
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.
pub fn auth(&self) -> &AuthTable
Sourcepub fn catalog(&self) -> &RepoCatalog
pub fn catalog(&self) -> &RepoCatalog
The repository catalog.
Sourcepub fn policies(&self) -> &PolicyStore
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.
Sourcepub fn mirrors(&self) -> &MirrorStore
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.
Sourcepub fn account_audit(&self) -> &Arc<AccountAuditLog> ⓘ
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.
Sourcepub fn names(&self) -> &NameFilterStore
pub fn names(&self) -> &NameFilterStore
The name filter — its lists and the accounts’ review queues.
Sourcepub fn encryption_keys(&self) -> &EncryptionKeys
pub fn encryption_keys(&self) -> &EncryptionKeys
People’s encryption keys (Access.GetEncryptionKey and friends, Repos.RecipientKeys).
Sourcepub fn settings(&self) -> &SettingsStore
pub fn settings(&self) -> &SettingsStore
The generic server settings (Admin.GetSetting / Admin.SetSetting).
pub fn usage_ledger(&self) -> &Arc<UsageLedger> ⓘ
Sourcepub fn metrics(&self) -> &Arc<ServerMetrics> ⓘ
pub fn metrics(&self) -> &Arc<ServerMetrics> ⓘ
The server’s one set of counters (see the field).
pub fn flow(&self) -> &Arc<FlowLedger> ⓘ
Sourcepub fn snapshot_pin(&self) -> SnapshotPin<'_>
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.
Sourcepub fn wait_unpinned(&self, what: &str)
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.
Sourcepub fn txlog(&self) -> &Arc<Log> ⓘ
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.
Sourcepub fn is_twin(&self) -> bool
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.
Sourcepub fn twin_flag(&self) -> Arc<AtomicBool> ⓘ
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.
Sourcepub fn secrets_unframed(
&self,
) -> Result<SealedSecretStore<Arc<dyn SecretStore>>>
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).
Sourcepub fn adopt_kek(&self, bytes: &[u8]) -> Result<()>
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.
Sourcepub fn kek_bytes(&self) -> Vec<u8> ⓘ
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).
Sourcepub fn raw_control_store(&self) -> &Arc<dyn ControlStore> ⓘ
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.
Sourcepub fn raw_secrets(&self) -> &Arc<dyn SecretStore> ⓘ
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.
pub fn control_store(&self) -> &Arc<dyn ControlStore> ⓘ
Sourcepub fn control_db(&self) -> Arc<ControlDb> ⓘ
pub fn control_db(&self) -> Arc<ControlDb> ⓘ
The redb handle under the control store — see the field.
Sourcepub fn suspend_repos(&self, why: &str)
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.
Sourcepub fn resume_repos(&self)
pub fn resume_repos(&self)
The stores may open again (lazily, on the next request), after the
growth mounted /data back.
Sourcepub fn control_dir(&self) -> &Path
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.
Sourcepub fn secrets(&self) -> Result<SealedSecretStore<Arc<dyn SecretStore>>>
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.
Sourcepub fn transparency_key(&self) -> Result<PrivateKey>
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.
Sourcepub fn transparency(&self) -> TransparencyLog
pub fn transparency(&self) -> TransparencyLog
The transparency log over this server’s push attestations.
Sourcepub fn build_log(&self) -> TransparencyLog
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.
Sourcepub fn forge_log(&self) -> TransparencyLog
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.
Sourcepub fn membership_log(&self) -> TransparencyLog
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.
Sourcepub fn provenance_key(&self) -> Result<PrivateKey>
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.
Sourcepub fn webhook_key(&self) -> Result<PrivateKey>
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.
Sourcepub fn pki(&self) -> Result<Pki>
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.
Sourcepub fn pki_readonly(&self) -> Result<Pki>
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.
Sourcepub fn install_plan_gates(&self, gates: Vec<Arc<dyn PlanGate>>)
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.
Sourcepub fn install_space_ledger(&self, ledger: Arc<SpaceLedger>)
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.
Sourcepub fn space_ledger(&self) -> Option<Arc<SpaceLedger>>
pub fn space_ledger(&self) -> Option<Arc<SpaceLedger>>
The installed space ledger, if any.
Sourcepub fn plan_gates(&self) -> Vec<Arc<dyn PlanGate>>
pub fn plan_gates(&self) -> Vec<Arc<dyn PlanGate>>
The installed chain, in the order it is asked. Empty: no ceiling.
pub fn ssh_control(&self) -> Control
Sourcepub fn browse(&self) -> Arc<AtomicBool> ⓘ
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).
Sourcepub fn writes_frozen(&self) -> Arc<AtomicBool> ⓘ
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).
Sourcepub fn store_opens(&self) -> u64
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.
Sourcepub fn repo_dir(&self, path: &RepoRef) -> Result<PathBuf>
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.
Sourcepub fn open_repo_capacity(&self) -> usize
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.
Sourcepub fn set_open_repo_capacity(&self, capacity: usize) -> usize
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.
Sourcepub fn repo_store(&self, repo: &Repo) -> Result<Arc<OpenRepo>>
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.
Sourcepub fn apply_explode_quota(&self, namespace: &str, dir: &Path)
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.
Sourcepub fn point_head(store: &OpenRepo, repo: &Repo) -> Result<()>
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.
Sourcepub fn repoint_head(&self, repo: &Repo) -> Result<()>
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.
Sourcepub fn adopt_default_branch_if_unborn(
&self,
path: &RepoRef,
pushed: &[String],
) -> Result<Option<String>>
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
HEADresolves 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,HEADresolves 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 ofHEAD.
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.
Sourcepub fn create_repo(&self, spec: NewRepo, adopt: bool) -> Result<Repo>
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:
- 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 deletedropped the catalog row and left the store, and the nextrepo createof the same name opened it —ls-remoteright after the create listed all 59 refs of the deleted skade, and recreated--hash sha256the name served sha1 refs (fatal: protocol error).adoptis 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. - The catalog row.
- 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 getto find.
Returns the catalogued repository; the handle is in the cache.
Sourcepub fn refuse_stale_store(
&self,
path: &RepoRef,
adopt: bool,
hash_kind: HashKind,
) -> Result<bool>
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 sha1The 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
--hashis 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.
Sourcepub fn tombstone_path(dir: &Path) -> PathBuf
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.
Sourcepub fn is_tombstoned(&self, path: &RepoRef) -> Result<bool>
pub fn is_tombstoned(&self, path: &RepoRef) -> Result<bool>
Is path tombstoned — deleted from the catalog, store not reclaimed?
Sourcepub fn open_handles(&self) -> usize
pub fn open_handles(&self) -> usize
How many repository handles are open. For tests and Admin.Stats.
Sourcepub fn close_repo(&self, path: &RepoRef)
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.
Sourcepub fn purge_repo(&self, path: &RepoRef) -> Result<(u64, u64)>
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:
- 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);
- 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);
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.
Sourcepub fn reclaim_repo(&self, path: &RepoRef) -> Result<(u64, u64)>
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.
Sourcepub fn soft_delete(
&self,
path: &RepoRef,
repo: Option<&Repo>,
principal_id: &str,
retention: Duration,
) -> Result<Tombstone>
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.
Sourcepub fn undelete_spec(&self, path: &RepoRef) -> Result<NewRepo>
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.
Sourcepub fn clear_tombstone(&self, path: &RepoRef) -> Result<()>
pub fn clear_tombstone(&self, path: &RepoRef) -> Result<()>
Take the tombstone off, once the catalog row is back.
Sourcepub fn read_tombstone(marker: &Path) -> Option<Tombstone>
pub fn read_tombstone(marker: &Path) -> Option<Tombstone>
Read a tombstone file, or None when there is not one.
Sourcepub fn tombstone_of(&self, path: &RepoRef) -> Result<Option<Tombstone>>
pub fn tombstone_of(&self, path: &RepoRef) -> Result<Option<Tombstone>>
The tombstone beside path, if any.
Sourcepub fn tombstones(&self) -> Result<Vec<DeletedRepo>>
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.
Sourcepub fn orphans(&self) -> Result<Vec<OrphanStore>>
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
Deletedtombstone — one of those isrepo list-deleted’s, and it hasundeleteandpurgeas its exits; - a
ReclaimFailedtombstone IS an orphan.tombstones()filters that reason out oflist-deleteddeliberately (bytes owed, not bytes held), which left it in no listing whatsoever; the row carries the note so an operator sees why.
Sourcepub fn is_orphan(&self, path: &RepoRef) -> Result<bool>
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.
Sourcepub fn orphan_soft_delete_refusal(path: &RepoRef) -> String
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.
Sourcepub fn reclaim_orphan(&self, path: &RepoRef) -> Result<u64>
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 undeleteorrepo 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.
Sourcepub fn sweep_expired_tombstones(&self, now_unix_ms: i64) -> Vec<(RepoRef, u64)>
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.
Sourcepub fn repo_of_dir(&self, dir: &Path) -> Option<(String, String)>
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§
Auto Trait Implementations§
impl !Freeze for ServerStores
impl !RefUnwindSafe for ServerStores
impl !UnwindSafe for ServerStores
impl Send for ServerStores
impl Sync for ServerStores
impl Unpin for ServerStores
impl UnsafeUnpin for ServerStores
Blanket Implementations§
Source§impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedExplicit<'a, E> for Twhere
T: 'a,
Source§impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
impl<'a, T, E> AsTaggedImplicit<'a, E> for Twhere
T: 'a,
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoRequest<T> for T
impl<T> IntoRequest<T> for T
Source§fn into_request(self) -> Request<T>
fn into_request(self) -> Request<T>
T in a tonic::Request