Artifact 776731b888bd39d05f3fd7117bb2b7d3cc66a0efad398754af70c9cd83fc70bf:
- File
internal/api/repos.go
— part of check-in
[4a479e6ce2]
at
2026-08-18 01:15:43
on branch trunk
— api: make the repo set a concurrent registry; serve rejects positionals
Deps.Repos was a bare map, read by every op on the request path and written only at startup. Adding repositories at runtime — the point of the registry ticket — makes that a data race with no symptom until it corrupts one. api.Repos owns the lock; a *Repo is published complete or not at all, and Delete hands back the entry so the caller can close the index and stop the pipeline.
serve now refuses leftover arguments. Go's flag package stops at the first non-flag and returns the rest, so one unquoted shell variable silently dropped twelve --repo flags and quarry served the remaining eleven without a word. (user: dmestas, size: 2325) [annotate] [blame] [check-ins using]
package api import ( "sort" "sync" ) // Repos is the set of stores quarry serves. // // It is a type rather than the bare map it replaced because repositories // can be added and removed while the server runs. Every op reads this on // the request path, so a plain map mutated by an admin call would be a // data race with no symptom until it corrupted one — the failure mode // Go's map implementation deliberately makes loud, but only sometimes. // // The lock is held for the lookup and nothing else: an op takes a *Repo // out and works with it unlocked, so a slow read never blocks a // registration. That is safe because a *Repo is immutable once // published — removal drops it from the map, and a request already // holding one finishes against a store that is still open. type Repos struct { mu sync.RWMutex m map[string]*Repo } // NewRepos builds a registry, optionally seeded — the shape tests and // the composition root both want. func NewRepos(seed map[string]*Repo) *Repos { m := make(map[string]*Repo, len(seed)) for k, v := range seed { m[k] = v } return &Repos{m: m} } func (r *Repos) Get(name string) (*Repo, bool) { if r == nil { return nil, false } r.mu.RLock() defer r.mu.RUnlock() repo, ok := r.m[name] return repo, ok } // Put registers a store, replacing any existing entry of that name. func (r *Repos) Put(name string, repo *Repo) { r.mu.Lock() defer r.mu.Unlock() if r.m == nil { r.m = map[string]*Repo{} } r.m[name] = repo } // Delete unregisters a store and returns what was removed, so the // caller can release what it owns — an index handle and a pipeline // outlive the map entry unless somebody closes them. func (r *Repos) Delete(name string) (*Repo, bool) { r.mu.Lock() defer r.mu.Unlock() repo, ok := r.m[name] delete(r.m, name) return repo, ok } // Names lists registered stores in sorted order. Sorted because callers // render it: map order would reshuffle the repository list on every // page load. func (r *Repos) Names() []string { if r == nil { return nil } r.mu.RLock() defer r.mu.RUnlock() out := make([]string, 0, len(r.m)) for name := range r.m { out = append(out, name) } sort.Strings(out) return out } func (r *Repos) Len() int { if r == nil { return 0 } r.mu.RLock() defer r.mu.RUnlock() return len(r.m) }