Add explicit LLM concurrency controls
This commit is contained in:
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/chunking"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
@@ -18,6 +21,54 @@ type fakeStructuredLLMClient struct {
|
||||
calls []StructuredCompletionRequest
|
||||
}
|
||||
|
||||
type sleepingValidationClient struct {
|
||||
inFlight int32
|
||||
maxInFlight int32
|
||||
}
|
||||
|
||||
type boundedScheduler struct {
|
||||
permits chan struct{}
|
||||
}
|
||||
|
||||
func newBoundedScheduler(max int) *boundedScheduler {
|
||||
return &boundedScheduler{permits: make(chan struct{}, max)}
|
||||
}
|
||||
|
||||
func (s *boundedScheduler) Run(ctx context.Context, fn func(context.Context) error) error {
|
||||
select {
|
||||
case s.permits <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
defer func() { <-s.permits }()
|
||||
return fn(ctx)
|
||||
}
|
||||
|
||||
func (c *sleepingValidationClient) CompleteStructured(ctx context.Context, req StructuredCompletionRequest, out any) (StructuredCompletionResponse, error) {
|
||||
_ = req
|
||||
current := atomic.AddInt32(&c.inFlight, 1)
|
||||
for {
|
||||
prior := atomic.LoadInt32(&c.maxInFlight)
|
||||
if current <= prior || atomic.CompareAndSwapInt32(&c.maxInFlight, prior, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-time.After(20 * time.Millisecond):
|
||||
case <-ctx.Done():
|
||||
atomic.AddInt32(&c.inFlight, -1)
|
||||
return StructuredCompletionResponse{}, ctx.Err()
|
||||
}
|
||||
atomic.AddInt32(&c.inFlight, -1)
|
||||
target := out.(*LLMValidationResponse)
|
||||
*target = LLMValidationResponse{
|
||||
Validations: []LLMValidationDecision{
|
||||
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
|
||||
},
|
||||
}
|
||||
return StructuredCompletionResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeStructuredLLMClient) CompleteStructured(ctx context.Context, req StructuredCompletionRequest, out any) (StructuredCompletionResponse, error) {
|
||||
_ = ctx
|
||||
f.calls = append(f.calls, req)
|
||||
@@ -191,3 +242,34 @@ func TestLLMBackedValidatorUnknownProposalIndexFails(t *testing.T) {
|
||||
t.Fatalf("expected unknown index error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMBackedValidatorRespectsSchedulerConcurrency(t *testing.T) {
|
||||
scheduler := newBoundedScheduler(2)
|
||||
client := &sleepingValidationClient{}
|
||||
v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model")
|
||||
if err != nil {
|
||||
t.Fatalf("new validator error: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
|
||||
req.LLMClient = client
|
||||
req.Scheduler = scheduler
|
||||
if _, runErr := v.Validate(context.Background(), req); runErr != nil {
|
||||
t.Errorf("Validate error: %v", runErr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if got := atomic.LoadInt32(&client.maxInFlight); got > 2 {
|
||||
t.Fatalf("expected scheduler cap <= 2, got %d", got)
|
||||
}
|
||||
if got := atomic.LoadInt32(&client.maxInFlight); got < 2 {
|
||||
t.Fatalf("expected observed concurrency of at least 2, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user