aee20a6abc
llama-server is started without -np, so it serves one request at a time and everything else queues. Mail extraction is allowed two minutes on a Thinking 1.7B, and the reader hands core up to 25 messages back to back. A turn arriving mid-extraction therefore waited for whatever was left of that budget: the router timed out into the classifier cascade and its 36.8% floor, and the phraser, which has no floor, simply waited. Memory evaluation had the same shape with a five minute budget. llm.Gate is the bound. Foreground requests never wait. Background requests run one at a time and yield while a foreground request is in flight, plus a quiet window after it that covers the gap between the router call and the phraser call of one turn. Clients get their priority from llmClientFor or llmBackgroundClientFor, so which side a caller is on is decided at wiring time. It gates only what goes through those clients, which the comment on Gate says. mail intake: the extraction timeout no longer wraps the capture writes. A model answering at 119 seconds of a 120 second budget left the first CaptureTask one second and the third none, so candidates the model had already produced were dropped with a deadline error. The mailbox name is validated before it becomes provenance, since "email:" is not a source and neither is an arbitrary string posted at the socket. The enable log prints the normalised candidate bound rather than the configured one, which said "max 0" and then wrote three. Found in review of #64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
102 lines
2.7 KiB
Go
102 lines
2.7 KiB
Go
package llm
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Background work must not start while he is waiting on a turn. llama-server
|
|
// serves one request at a time, so an extraction that starts first holds the
|
|
// slot for its whole budget.
|
|
func TestGateBackgroundWaitsForForeground(t *testing.T) {
|
|
g := NewGate(0)
|
|
g.poll = time.Millisecond
|
|
done := g.Foreground()
|
|
|
|
started := make(chan struct{})
|
|
go func() {
|
|
release, err := g.AcquireBackground(context.Background())
|
|
if err != nil {
|
|
t.Errorf("acquire: %v", err)
|
|
return
|
|
}
|
|
close(started)
|
|
release()
|
|
}()
|
|
|
|
select {
|
|
case <-started:
|
|
t.Fatal("background work started while a foreground request was in flight")
|
|
case <-time.After(20 * time.Millisecond):
|
|
}
|
|
done()
|
|
select {
|
|
case <-started:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("background work never started after the foreground request finished")
|
|
}
|
|
}
|
|
|
|
// Only one background request at a time, whatever the queue depth upstream. A
|
|
// first poll of a mailbox with 40 unseen messages must not put 40 extractions
|
|
// on the slot.
|
|
func TestGateOneBackgroundAtATime(t *testing.T) {
|
|
g := NewGate(0)
|
|
g.poll = time.Millisecond
|
|
first, err := g.AcquireBackground(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("first: %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
if _, err := g.AcquireBackground(ctx); err == nil {
|
|
t.Fatal("a second background request ran alongside the first")
|
|
}
|
|
first()
|
|
second, err := g.AcquireBackground(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("second after release: %v", err)
|
|
}
|
|
second()
|
|
}
|
|
|
|
// The quiet window covers the gap between the router call and the phraser call
|
|
// of one turn, so an extraction cannot slip in mid-turn.
|
|
func TestGateQuietWindow(t *testing.T) {
|
|
now := time.Now()
|
|
g := NewGate(time.Minute)
|
|
g.poll = time.Millisecond
|
|
g.now = func() time.Time { return now }
|
|
g.Foreground()()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
if _, err := g.AcquireBackground(ctx); err == nil {
|
|
t.Fatal("background work started inside the quiet window")
|
|
}
|
|
now = now.Add(2 * time.Minute)
|
|
release, err := g.AcquireBackground(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("acquire after the quiet window: %v", err)
|
|
}
|
|
release()
|
|
}
|
|
|
|
// Foreground never waits, whatever else is in flight.
|
|
func TestGateForegroundNeverBlocks(t *testing.T) {
|
|
g := NewGate(time.Minute)
|
|
release, err := g.AcquireBackground(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("acquire: %v", err)
|
|
}
|
|
defer release()
|
|
done := make(chan struct{})
|
|
go func() { g.Foreground()(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("a foreground request waited behind background work")
|
|
}
|
|
}
|