Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/proxy-design.md

The pull-through proxy cache — design

Class: design · Function: proxy (a registry backend that uses storage for write-back + an upstream). A full-variant capability — it links egress crates, so it is absent from the airgap/static profile. Whole-system view: design.md.

What it is

ProxyBackend (server/lib/src/proxy.rs) is a repository backend that, on a request, tries its primary backend first, then each configured upstream in order, and write-through-caches a hit back into the writable primary. So a local miss transparently fills from upstream and subsequent requests hit the local copy. Because it is just another Arc<dyn RepositoryBackendTrait>, it dispatches through FastRoutes like every other repo — the serve path doesn't know it's a proxy.

Pluggable upstreams — RemoteUpstream

The upstream is a seam, not a hardcoded crates.io shape. RemoteUpstream (traits/src/lib.rs) is a read-oriented backend carrying an upstream base URL + the shared UpstreamAuth enum. Three concrete impls:

impl targets source
RustRegistryUpstream crates.io-shaped sparse registries server/lib/src/upstream/
NexusUpstream Sonatype Nexus server/lib/src/upstream/nexus.rs
ArtifactoryUpstream JFrog Artifactory server/lib/src/upstream/artifactory.rs

So holger can target holger / Nexus / Artifactory interchangeably as a cache backend.

Wiring

Wired through the nexus-remote / artifactory-remote repository types in backend_from_config() (server/lib/src/repository.rs), configured with ron_base_url (the upstream URL) + ron_upstream_repo:

Repository(
    ron_name: "crates-cache",
    ron_repo_type: "nexus-remote",
    ron_base_url: "https://nexus.corp",
    ron_upstream_repo: "cargo-proxy",
    ron_out: Some(RepositoryIO( ron_storage_endpoint: "znippy", ron_exposed_endpoint: "default" )),
)

UpstreamAuth (shared with the connectors' spirit) carries the per-upstream credential; the SSRF guards on the agent connectors apply to the upstream fetch discipline too.

Concurrency note

The write-through fill calls the writable-znippy put, which holds a per-backend write Mutex across the check-then-mutate section — so a cache-miss fill racing a direct put can't corrupt the single .znippy (see storage-design).

Negative cache — remembering a miss (parity §2)

The positive path caches hits; the negative cache caches misses. When a coordinate (or a GET/HEAD path) misses the primary and every upstream, ProxyBackend remembers it as absent for a TTL, so a repeated request for a not-yet-published artifact short-circuits the (potentially slow / network) upstream walk instead of re-paying it every time — the Nexus/Artifactory "negative cache".

Config. Opt-in per repo via ron_negative_cache_secs (seconds). Omitted or 0 ⇒ disabled, and every miss re-queries the upstreams exactly as before (additive, no regression — the constructor default is Duration::ZERO; a builder with_negative_cache_secs turns it on).

Repository(
    ron_name: "crates-cache",
    ron_repo_type: "rust",
    ron_writable_archive: Some("{d}/cache.znippy"),
    ron_upstreams: ["crates-io"],
    ron_negative_cache_secs: Some(30),   //  remember a miss for 30s
    ron_out: Some(RepositoryIO( ron_storage_endpoint: "znippy", ron_exposed_endpoint: "default" )),
)

Invariants.

  1. Primary is always checked first, before the negative cache is even consulted — so a later local publish (a put into the primary) is served immediately and is never shadowed by a stale cached miss. The only bounded staleness is a publish that lands upstream during a live TTL window; that resolves on the next re-walk after expiry.
  2. Reads only. Only GET/HEAD 404s are memoized on the HTTP path (a write's 404 is not a stable "absent" fact); fetch misses are keyed by full coordinate. Fetch keys and HTTP keys live in disjoint namespaces (F\0… / H\0…) so they can never alias.
  3. Bounded memory. Entries carry an expiry Instant; expired ones are evicted lazily on read and pruned on insert, and the map is capped at NEG_CAP (4096) live entries — a coordinate-scanning probe degrades the cache to plain re-querying rather than leaking memory.
  4. An erroring upstream is skipped, never fatal, and never memoized. "Tries each upstream in order, returning the first hit" is the whole contract, so an upstream that returns Err (connection refused, DNS failure, TLS error, a non-404 status) is logged at warn, counted as result="upstream_error" on /metrics, and the walk continues to the next member. One unreachable upstream must not blind the server to the reachable ones — a repo with ron_upstreams: ["crates-io", "local-mirror"] keeps serving everything local-mirror holds while crates.io is down. If no upstream hits and at least one errored, the coordinate is unknown rather than known-absent: the error is surfaced and the miss is not written to the negative cache, because memoizing a transient network fault would keep returning 404 for the whole TTL after the upstream recovered. Only a walk in which every upstream cleanly missed is remembered.

Tier note. Purely in-process (std only, no new deps, no egress); it lives in the always-compiled proxy.rs but is dormant unless a repo declares upstreams — so it changes nothing about the airgap/mini/embedded tiers' zero-egress guarantee.

Observability (L4). A negative-cache short-circuit emits a #[cfg(feature="testmatrix")] functional_status("holger-proxy/negcache", …) cell on both the fetch and HTTP paths. Five unit tests, incl. two RED-when-broken counters (a repeated miss must NOT re-walk a call-counting upstream) + the never-shadow-a-later-primary-hit guard