Routes may share one lease (affinity = "route") and skip crossbar's queue (queue = false)

route.go gains Route.Affinity/Queue with PerRoute(), Queues() and affinity
validation (checkRoutes moved here; config.go calls it once). limiter.Track
counts a request without holding or refusing it; a release hands the slot to a
waiter only while in flight <= parallel. The proxy leases a PerRoute() route
under an empty fingerprint (the row keeps the real one) and uses Track when
Queues() is false.

Implemented by Ornith (OpenCode); owner review removed a release-on-first-flush
workaround for a race in the owner's given test (see implementer log).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 19:33:46 -07:00
co-authored by Claude Opus 5.5
parent 33fa61bedb
commit aea2eeae2c
8 changed files with 486 additions and 85 deletions
+36 -21
View File
@@ -217,6 +217,12 @@ func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
model = resolveModel(model, r, routeCfg)
fp := fingerprint.Of(body)
// A route-affinity route puts every request (chat or control) on one lease per model, so the
// lease key's fingerprint is "" for all of them; the real fingerprint is kept for the row below.
leaseFP := fp
if routeCfg.PerRoute() {
leaseFP = ""
}
started := time.Now()
// v0 compatibility path: no lease table, no limiter, no recording.
@@ -231,14 +237,14 @@ func (p *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Lease. The route's ordered host list is the candidate set.
host, reused, err := p.leases.Acquire(lease.Key{Route: route, FP: fp, Model: model}, routeCfg.Hosts, time.Now())
host, reused, err := p.leases.Acquire(lease.Key{Route: route, FP: leaseFP, Model: model}, routeCfg.Hosts, time.Now())
if err != nil {
switch {
case errors.Is(err, lease.ErrNoHost):
// No host healthy. Ask a waker to rouse a sleeping one; it answers
// (served or 503) when it has had a turn, else falls through to the
// plain 503.
if p.waker != nil && p.wakeOnErrNoHost(w, r, route, routeCfg, rest, model, fp, started, lease.Key{Route: route, FP: fp, Model: model}) {
if p.waker != nil && p.wakeOnErrNoHost(w, r, route, routeCfg, rest, model, fp, started, lease.Key{Route: route, FP: leaseFP, Model: model}) {
return
}
p.writeError(w, http.StatusServiceUnavailable, "no healthy host")
@@ -265,10 +271,30 @@ func (p *Handler) serveLeased(w http.ResponseWriter, r *http.Request, route stri
p.forward(w, r, route, host, leaseState(reused), rest, fp, model, started, 0, 0, "", true)
return
}
// Slot. A full queue is a 503; a context done while waiting means the client left.
release, waited, err := p.lim.Acquire(r.Context(), host, model)
if err != nil {
if errors.Is(err, limiter.ErrQueueFull) {
// Slot or track. A queue = false route leaves queueing to the client's own
// llama-server slot: crossbar only counts the request on the host, never
// holding it or refusing it.
var waited time.Duration
var release func()
if routeCfg.Queues() {
var err error
release, waited, err = p.lim.Acquire(r.Context(), host, model)
if err != nil {
if errors.Is(err, limiter.ErrQueueFull) {
p.writeRecord(store.Request{
Route: route,
FP: fp,
Model: model,
Host: host,
Started: started,
TotalMs: time.Since(started).Milliseconds(),
Status: http.StatusServiceUnavailable,
Err: "queue full",
})
p.writeError(w, http.StatusServiceUnavailable, "queue full")
return
}
p.log.Warn("request", "route", route, "host", host, "method", r.Method, "path", rest, "status", 499)
p.writeRecord(store.Request{
Route: route,
FP: fp,
@@ -276,24 +302,13 @@ func (p *Handler) serveLeased(w http.ResponseWriter, r *http.Request, route stri
Host: host,
Started: started,
TotalMs: time.Since(started).Milliseconds(),
Status: http.StatusServiceUnavailable,
Err: "queue full",
Status: 499,
Err: "client cancelled while queued",
})
p.writeError(w, http.StatusServiceUnavailable, "queue full")
return
}
p.log.Warn("request", "route", route, "host", host, "method", r.Method, "path", rest, "status", 499)
p.writeRecord(store.Request{
Route: route,
FP: fp,
Model: model,
Host: host,
Started: started,
TotalMs: time.Since(started).Milliseconds(),
Status: 499,
Err: "client cancelled while queued",
})
return
} else {
release = p.lim.Track(host, model)
}
defer release()