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

nordisk/holger

.nornir/serve-design.md

Serving — design

Class: design · Function: serve (uses registry for dispatch, storage for extract; gated by auth on writes). The whole-system view is design.md.

Two front doors, one backend

Both bottom out in backend.fetch(id) → ZnippyReader::extract_file(path): random-access, blake3-verified, no full-archive decompress.

The gRPC contract (RepositoryService)

The identity threading every RPC is ArtifactId { namespace, name, version }.

RPC request → response notes
FetchArtifact {repository, id} → {data, content_type, size_bytes} whole blob in one message; NOT_FOUND if absent
ListArtifacts {repository, name_filter, limit, page_token} → {artifacts[], next_page_token} name_filter optional substring; token pagination
PutArtifact {repository, id, data} → {success, message} PERMISSION_DENIED if !is_writable() (and RBAC gate)
StreamArtifact {repository, id} → stream ArtifactChunk { data } same request as FetchArtifact; 64 KB chunks for a progress bar

ArtifactEntry { id, size_bytes, content_type, published_unix_ms?, sha256? } is one ListArtifacts row — the shape a tree/list view consumes.

The last two are proto3 optional (explicit field presence), and the word is load-bearing. Absent means the serving backend does not know, which is a different thing from 0 and from "" — a zero timestamp sorts as 1970 and an empty digest compares equal to another empty digest, so a proto3 scalar default would be a fabricated answer from a backend that has none. Presence makes "unknown" a state rather than a value, which is what lets an old peer and a new peer talk in either direction without either of them inventing one. (Same reasoning as StartMigrationRequest.skip_digest_verification, which is inverted on the wire so the proto3 zero is the safe one; here the safe state says its own name.)

Backends differ in what they can answer without reading artifact bytes, and that is a cost, not a preference — a listing that reads and hashes the store is a worse defect than a missing column. holger_traits::ListingProvenance, derived from ArtifactFormat::home() so no backend can be left out of the answer, records which is which:

backend time digest why
every znippy-* – – ZnippyReader is list_files/file_size/contains/extract_file: no mtime, no per-entry digest
raw yes – mtime is free from the walk's own stat; a digest would mean reading every blob
file-rust yes when memoized the sparse-index path caches (len, mtime) → sha256; the listing peeks and never hashes
file-oci yes yes the listed artifact IS the manifest, OCI is content-addressed, and the read is capped

Two further services complete the proto: ArchiveService (seal / verify / manifest — see the promotion reasoning) and AdminService. FetchArtifact / PutArtifact / StreamArtifact are wired end-to-end through backend.fetch/backend.put; the reference tonic client is RemoteHolger (server/lib/src/object.rs).

Proto codegen — no protoc, no shell-out

The proto Rust is generated by pure-Rust protox via cargo xtask gen-proto (xtask/src/gen_proto.rs): protox::compile → FileDescriptorSet → tonic_build … .skip_protoc_run().compile_fds(fds) → committed server/lib/src/generated/holger.v1.rs, which src/grpc.rs include!s. build.rs is only a rerun-if-changed hint — a normal cargo build runs no subprocess and needs no protoc. The public module path stays holger_proto::*. Guarded by server/lib/tests/unit_tests.rs::generated_proto_types_roundtrip.

Endpoints & health

ExposedEndpoint (RON) names a listen surface and carries ron_max_body_bytes (the shared body/ingest cap, default 1 GiB). The deployment exposes /healthz (liveness) and /readyz (readiness). One exposed endpoint fronts many repos via its FastRoutes table; a repo is reachable at both doors simultaneously.