1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! **One store handle per store root, per process — the thing both engines need
//! and only one of them had.**
//!
//! # The defect this exists to make unrepresentable
//!
//! A storage engine's reading path and its ingest path are two *call sites* and
//! must not be two *handles*. Both engines keep a view of what is on disk —
//! znippy an archive tail, gix a `PackSet` and an odb pool whose handles are
//! `refresh_never()` — and each engine refreshes that view **on the instance
//! that did the write**. So a `put_pack` through a second, throwaway handle
//! refreshes the throwaway and drops it, and the handle the server is actually
//! serving from never learns that a pack arrived.
//!
//! znippy routed both paths through one cached handle from the start. gix did
//! not: [`open`](super::gix_engine::open) and
//! [`ingest`](super::gix_engine::ingest) each called `GixStore::open_or_init`.
//! MEASURED on the reference desktop 2026-08-11, one `gunnar-server` process on `engine-gix`:
//!
//! ```text
//! push → succeeds
//! clone → remote: the repository is unavailable; try again
//! server log: <oid> is a LOOSE object: it is in this repository but
//! has no pack entry, so there are no stored bytes to copy
//! ```
//!
//! …while on disk that repository held **0 loose objects and 1 pack**, and
//! `git verify-pack -v` found the very oid in it and called the pack `ok`.
//! Restarting the server — which is nothing but a fresh handle — served the
//! same bytes perfectly. It cost eleven arms of a two-engine sweep, across two
//! independent transports, and it is invisible to any test that pushes in one
//! process and reads in another.
//!
//! # Why a shared helper rather than a second copy of znippy's
//!
//! One implementation. The lookup-or-construct-under-the-lock dance is ~12 lines and getting
//! it subtly different in two engines is exactly how the two views drift apart
//! again. This is the *shape*; each engine keeps its own map, its own store type
//! and its own constructor, because those are genuinely different.
//!
//! **Opened under the lock, deliberately.** An ordinary read-through cache
//! constructs outside the lock and discards the loser of a race. Here the loser
//! is a second live handle over one directory — the very thing this module
//! exists to prevent — so a race must not be able to create one even
//! transiently.
use HashMap;
use PathBuf;
use ;
use Result;
/// The map one engine caches its handles in.
///
/// **`Weak`, not `Arc`, and that is the whole memory story.** See
/// [`shared`] — a strong map here is immortal, and it made the bound
/// `ServerStores` already enforces do nothing.
pub type Cache<S> = ;
/// Lock a cache, treating poisoning as ordinary contention.
///
/// A poisoned lock here means some other thread panicked while holding it; the
/// map itself is a `HashMap<PathBuf, Arc<_>>` and cannot be left torn by that,
/// so recovering the guard is correct and refusing every subsequent request
/// would turn one panic into a dead server.
/// **The handle for `root`, shared with every other live user of it.**
///
/// `make` runs on a miss, and it runs **while the lock is held** — see the
/// module docs for why that is the point rather than an oversight.
///
/// # Why the map holds `Weak` — MEASURED 2026-08-11
///
/// A strong map is an immortal one: nothing ever removes an entry, so every
/// repository a process has *ever* opened stays fully resident until exit. That
/// is not free. znippy's store carries a redb page cache of
/// `DEFAULT_REDB_CACHE_BYTES` = **64 MiB, one database per repository**
/// (corrected 2026-08-31: this said "two databases per repository" and it was
/// stale — `git_ops::open_with_arms` opens exactly one `ObjectReadStack` and
/// `znippy-plugin-git`'s `tests/fd_per_store.rs` counts exactly one
/// `objects.tail` descriptor. The 2026-08-31 reading took the doubled figure
/// from here; the 382 B/object measurement refuted it). On the reference desktop the forge
/// suite seeds
/// ~20 repositories and gunnar-on-znippy reported **4815–4842 MB of anonymous
/// RSS — flat**, whether the clone it then served was 3 257 objects or 352 396.
/// A figure that does not move with the workload is not the workload. gix, over
/// the same twenty repositories, sat at 176 MB.
///
/// The bound already exists one layer up and this was defeating it:
/// [`ServerStores`](crate::stores::ServerStores) caches `Arc<OpenRepo>` in a
/// [`DEFAULT_OPEN_REPOS`](crate::stores::DEFAULT_OPEN_REPOS)-entry LRU and
/// evicts, but evicting the `OpenRepo` freed nothing while this map still held
/// the store underneath it. `Weak` hands the lifetime back to that LRU: a store
/// lives exactly as long as some `OpenRepo` holds it, and the eviction that was
/// already happening now actually reclaims.
///
/// # It does NOT weaken the correctness fix this module exists for
///
/// The defect was a *reader opened before a write* not seeing it, and a reader
/// that predates a write is by definition still alive — so it holds an `Arc`,
/// the `Weak` upgrades to that same instance, and `ingest` writes through the
/// handle the reader is serving from. Unchanged.
///
/// When nothing holds it, the upgrade fails and `make` builds a fresh store —
/// which loads a fresh view off disk and is therefore *also* correct, just not
/// shared. There is no window in which two live handles disagree, which is the
/// only thing the strong map was buying.
pub
/// The live store for `root`, if some handle keeps it alive. Never opens one:
/// a store nobody holds has no worker to cancel and no progress to report.
pub
/// Drop every handle this cache holds.
///
/// Called from [`super::shutdown`] on the way out. What it buys differs per
/// engine — znippy needs it so `redb`'s clean-shutdown `Drop` runs at all, gix
/// only releases mmaps — but the *shape* is one line either way, which is why
/// it is here and not written twice.
pub
/// **Every store some handle keeps alive**, as `(root, handle)`. Never opens
/// one: what this answers is "what is this process paying RAM for right now",
/// and a store nobody holds costs nothing and must not be reported as if it
/// did. The `Weak` map is swept on the way, like [`shared`] does.
pub