Wire production CLI catalog and scheduled LLM client
This commit is contained in:
43
internal/framework/llm/scheduled_client.go
Normal file
43
internal/framework/llm/scheduled_client.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
type scheduledClient struct {
|
||||
client contracts.StructuredLLMClient
|
||||
scheduler *Scheduler
|
||||
}
|
||||
|
||||
func NewScheduledClient(client contracts.StructuredLLMClient, scheduler *Scheduler) contracts.StructuredLLMClient {
|
||||
return &scheduledClient{
|
||||
client: client,
|
||||
scheduler: scheduler,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *scheduledClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
if c == nil {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("scheduled LLM client must not be nil")
|
||||
}
|
||||
if c.client == nil {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("scheduled LLM client inner client must not be nil")
|
||||
}
|
||||
if c.scheduler == nil {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("scheduled LLM client scheduler must not be nil")
|
||||
}
|
||||
|
||||
var response contracts.StructuredCompletionResponse
|
||||
err := c.scheduler.Run(ctx, func(ctx context.Context) error {
|
||||
var callErr error
|
||||
response, callErr = c.client.CompleteStructured(ctx, req, out)
|
||||
return callErr
|
||||
})
|
||||
if err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, err
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
115
internal/framework/llm/scheduled_client_test.go
Normal file
115
internal/framework/llm/scheduled_client_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
)
|
||||
|
||||
func TestScheduledClientEnforcesSchedulerLimit(t *testing.T) {
|
||||
scheduler, err := NewScheduler(1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewScheduler() error = %v, want nil", err)
|
||||
}
|
||||
inner := &blockingStructuredClient{
|
||||
release: make(chan struct{}),
|
||||
}
|
||||
client := NewScheduledClient(inner, scheduler)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 3; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var out map[string]any
|
||||
if _, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{}, &out); err != nil {
|
||||
t.Errorf("CompleteStructured() error = %v, want nil", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
waitForAtomicAtLeast(t, &inner.calls, 1)
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if got := atomic.LoadInt32(&inner.maxInFlight); got > 1 {
|
||||
t.Fatalf("max in-flight calls = %d, want <= 1", got)
|
||||
}
|
||||
close(inner.release)
|
||||
wg.Wait()
|
||||
if got := atomic.LoadInt32(&inner.calls); got != 3 {
|
||||
t.Fatalf("calls = %d, want 3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduledClientPropagatesClientError(t *testing.T) {
|
||||
scheduler, err := NewScheduler(1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewScheduler() error = %v, want nil", err)
|
||||
}
|
||||
expected := errors.New("provider unavailable")
|
||||
client := NewScheduledClient(&errorStructuredClient{err: expected}, scheduler)
|
||||
|
||||
_, err = client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{}, &struct{}{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatalf("CompleteStructured() error = %v, want %v", err, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheduledClientPropagatesSchedulerError(t *testing.T) {
|
||||
scheduler, err := NewScheduler(1)
|
||||
if err != nil {
|
||||
t.Fatalf("NewScheduler() error = %v, want nil", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
client := NewScheduledClient(&errorStructuredClient{}, scheduler)
|
||||
|
||||
_, err = client.CompleteStructured(ctx, contracts.StructuredCompletionRequest{}, &struct{}{})
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("CompleteStructured() error = %v, want context canceled", err)
|
||||
}
|
||||
}
|
||||
|
||||
type blockingStructuredClient struct {
|
||||
release chan struct{}
|
||||
inFlight int32
|
||||
maxInFlight int32
|
||||
calls int32
|
||||
}
|
||||
|
||||
func (c *blockingStructuredClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
atomic.AddInt32(&c.calls, 1)
|
||||
current := atomic.AddInt32(&c.inFlight, 1)
|
||||
for {
|
||||
seen := atomic.LoadInt32(&c.maxInFlight)
|
||||
if current <= seen || atomic.CompareAndSwapInt32(&c.maxInFlight, seen, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
defer atomic.AddInt32(&c.inFlight, -1)
|
||||
|
||||
select {
|
||||
case <-c.release:
|
||||
case <-ctx.Done():
|
||||
return contracts.StructuredCompletionResponse{}, ctx.Err()
|
||||
}
|
||||
if target, ok := out.(*map[string]any); ok {
|
||||
*target = map[string]any{"ok": true}
|
||||
}
|
||||
return contracts.StructuredCompletionResponse{
|
||||
Content: json.RawMessage(`{"ok":true}`),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type errorStructuredClient struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *errorStructuredClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
return contracts.StructuredCompletionResponse{}, c.err
|
||||
}
|
||||
Reference in New Issue
Block a user