Every given test compiled against a panic-only interface skeleton (go vet clean); nothing was implemented. modernc.org/sqlite v1.59.0 vetted in a scratch module (WAL works); go.sum given. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
4.3 KiB
v1 task 03: the per-(host, model) limiter and the chooser
Branch: v1 (run git switch v1; git status --short must be empty, otherwise stop)
Commit subject: Add the per-host-model limiter and the host chooser
Goal
Two pure packages. limiter hands out at most parallel concurrent slots per (host, model) and
lets at most queue_max requests wait in line, first come first served. choose picks the host
for a new lease: most free slots × weight, ties to the shortest queue, then list order.
This is PLAN.md §4 (choose) and §6 (concurrency).
Context
A llama-server router with parallel = 4 and unified KV falls over when a fifth request
arrives ("Context size has been exceeded"). The limiter is where that is prevented: the fifth
request waits, the ninth (with queue_max = 4) is refused at once so the client can retry
elsewhere. Waiting is cancellable (the client may go away) and must leak neither a slot nor a
queue place.
Files
- Copy:
internal/limiter/limiter_test.go,internal/choose/choose_test.go - Create:
internal/limiter/limiter.go,internal/choose/choose.go - Modify:
docs/implementer-log.md
Interfaces
internal/limiter, package limiter:
var ErrQueueFull = errors.New("queue full")
type Limiter struct { /* private: mutex, map[(host, model)]*pair */ }
func New() *Limiter
func (l *Limiter) Configure(host, model string, parallel, queueMax int)
// Acquire returns when a slot is held. release gives it back (idempotent: a second call is a
// no-op). waited is how long the caller sat in the queue. Errors: ErrQueueFull immediately
// when queueMax waiters are already queued; ctx.Err() if ctx ends while waiting.
func (l *Limiter) Acquire(ctx context.Context, host, model string) (release func(), waited time.Duration, err error)
func (l *Limiter) InFlight(host, model string) int
func (l *Limiter) Queued(host, model string) int
func (l *Limiter) FreeSlots(host string) int // sum over the host's configured models of parallel - inflight (never below 0); 0 for an unknown host
Rules the tests check:
- An unconfigured (host, model) behaves as
parallel = 1, queueMax = 0. - FIFO: waiters get slots in arrival order. Suggested shape: a mutex,
inflight, and a slice of waiter channels;releasepops the head waiter (if any) and hands the slot over without ever decrementinginflight, else decrements. A waiter whose ctx ends removes itself from the queue under the mutex; if the slot was handed to it in the same instant, it releases it. releaseidempotent viasync.Once.- Never panic; all methods safe for concurrent use.
internal/choose, package choose:
type Info struct {
Healthy, Draining, Loaded, CanServe bool // Loaded: model is resident; CanServe: config lists the model
Free, Queued int
Weight float64
}
// Best returns the candidate with the highest Free*Weight among those that are healthy, not
// draining and known (info ok) — preferring hosts with Loaded over merely CanServe; ties go to
// the lowest Queued, then to candidate order. A host with Free == 0 is still eligible (it will
// queue). ok is false when nothing is eligible.
func Best(candidates []string, info func(host string) (Info, bool)) (string, bool)
Rules: two passes — first over eligible candidates with Loaded, then, if none, over eligible
candidates with CanServe. Score float64(Free) * Weight. Compare with >; on equality prefer
lower Queued; on equality keep the earlier candidate.
Steps
- 1. Copy.
git switch v1;mkdir -p internal/limiter internal/choose; copy both tests fromdocs/plans/v1/_files/internal/…. - 2. See them fail (compile). 3. Write both packages.
gofmt -w internal/. - 4. See them pass.
go test -race -count=1 ./internal/limiter/ ./internal/choose/. The limiter tests are timing-based with generous margins; run them three times:-count=3. - 5. Run the gate.
make gate. 6. Log and commit. Rowv1/03-limiter-choose.
git add internal/limiter internal/choose docs/implementer-log.md
git commit
Done when
- Both packages pass
-race -count=3;make gateprintsgate: ok; copied files byte-identical.
Stop and report if
- A limiter test fails only sometimes: report which and how often; do not loosen it.