The plugin registry — reasoning
Class:
reasoning. Why the registry is one trait behind an O(1) table, and the shapes we rejected. The descriptive design isregistry-design.md.
Why one trait, not a blanket generic or an enum of backends
Two tidier-looking shapes were rejected:
- An
enum Backend { Rust(..), Maven(..), … }. Every new ecosystem would then edit the enum and everymatchover it — including on the hot request path — so the engine grows an ecosystemmatchit must never have.Arc<dyn RepositoryBackendTrait>keeps the hot path a single vtable call and confines all type knowledge to the onebackend_from_config()arm. - A
impl<T> RepositoryBackendTrait for Tblanket. There is no shared behaviour to factor — each backend'sfetch/handle_http2_requestis entirely its own ecosystem's framing — so a blanket buys nothing and blocks per-backend specialization.
The trait draws the seam exactly where the variation is (path mapping + protocol framing) and nowhere else, which is why "add an ecosystem" is one crate + one registry arm with zero engine change.
Why FastRoutes is a first-byte bucket, not a HashMap
Dispatch is the hot path and must be lock-free. A HashMap<String, _> behind a Mutex/RwLock would put a lock on every request. FastRoutes buckets by the first byte of the repo name into two [usize; 256] arrays — O(1), allocation-free, lock-free — built once at wire_holger time and shared immutably by the gRPC service and LocalHolger. Adding the writable backends and the auth gate deliberately did not touch this path: authorize_write was added only to put_artifact, so reads never run auth, and file-rust byte download is a plain std::fs::read with no lock and no hash.
Why the config is a graph, and why znippy owns the format numbers
The graph rationale is in reasoning.md (typed enums + named refs vs a flat stringly-typed list). One registry-specific choice: ArtifactFormat::znippy_type_id() does not define its own discriminants — it defers to znippy's skeletons::*Plugin.type_id() and a test asserts they match. The alternative (holger picking its own numbers) would let holger and znippy drift into two disagreeing sources of truth for the same DenseUnion tag; deferring makes drift a compile-time test failure instead of a silent wire-format bug.
Why writable and read-only share one trait (rather than two traits)
is_writable() + a put() that errors on read-only backends keeps one dispatch type, so FastRoutes and the serve engine never branch on "is this writable". A separate WritableBackend trait would fork the routing table and the serve path. The write guard lives above the trait (gRPC put_artifact rejects !is_writable() with PermissionDenied, and RBAC gates it) — the trait stays uniform, the policy sits in one place.