Pace provider traffic and identify genuine foreground bank requests

This commit is contained in:
Lars Nolden
2026-09-10 17:47:44 +02:00
parent ba3ea6ae5a
commit 4324660888
13 changed files with 817 additions and 32 deletions
+2 -1
View File
@@ -49,7 +49,8 @@ func (c *Client) rateControl() *ratelimit.Controller {
if gate := c.rate.Load(); gate != nil {
return gate
}
gate := &ratelimit.Controller{}
// Conservative 20-RPM ceiling, independent of model/provider quota claims.
gate := &ratelimit.Controller{MinimumInterval: 3 * time.Second, InitialBackoff: 15 * time.Second}
if c.rate.CompareAndSwap(nil, gate) {
return gate
}
+4 -1
View File
@@ -13,6 +13,7 @@ import (
"testing"
"finance-duck/internal/domain"
"finance-duck/internal/ratelimit"
)
func fixture() (domain.Facts, domain.Dataset) {
@@ -37,7 +38,9 @@ func mockClient(t *testing.T, handler http.HandlerFunc) *Client {
t.Helper()
server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return &Client{APIKey: "test-secret", Model: "test/strict-model", BaseURL: server.URL, HTTPClient: server.Client()}
client := &Client{APIKey: "test-secret", Model: "test/strict-model", BaseURL: server.URL, HTTPClient: server.Client()}
client.rate.Store(&ratelimit.Controller{})
return client
}
func TestExplicitDefaultsAreOptInAndBypassAI(t *testing.T) {
@@ -12,8 +12,63 @@ import (
"sync/atomic"
"testing"
"time"
"finance-duck/internal/ratelimit"
)
func TestProductionRatePolicyAcrossModelSnapshots(t *testing.T) {
t.Run("successful classifications stay paced", func(t *testing.T) {
f, d := fixture()
arrivals := make(chan time.Time, 2)
c := mockClient(t, func(w http.ResponseWriter, r *http.Request) {
arrivals <- time.Now()
reply(w, validAnswer)
})
// Remove only the fixture's fast policy, exercising real lazy production
// initialization and the same shared controller used by previews.
c.rate.Store(nil)
snapshot := c.WithModel("test/preview-model")
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
for _, client := range []*Client{c, snapshot} {
p, err := client.Classify(ctx, f, d, true)
if err != nil || p.Enrichment.Classification.Source != "openrouter" {
t.Fatalf("production-paced classification failed: %+v, %v", p, err)
}
}
first, second := <-arrivals, <-arrivals
if gap := second.Sub(first); gap < 3*time.Second-25*time.Millisecond {
t.Fatalf("model snapshots burst after success: %v", gap)
}
})
t.Run("first failure retains conservative retry deadline", func(t *testing.T) {
f, d := fixture()
var calls atomic.Int32
c := mockClient(t, func(w http.ResponseWriter, r *http.Request) {
calls.Add(1)
w.WriteHeader(http.StatusTooManyRequests)
})
c.rate.Store(nil)
snapshot := c.WithModel("test/preview-model")
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, err := c.Classify(ctx, f, d, true)
var limit *ratelimit.RateLimitError
if !errors.Is(err, context.DeadlineExceeded) || !errors.As(err, &limit) {
t.Fatalf("production retry wait lost cancellation or quota identity: %v", err)
}
if delay := time.Until(limit.RetryAt()); delay < 14*time.Second {
t.Fatalf("production retry used a short per-record fallback: %v", delay)
}
probeCtx, probeCancel := context.WithTimeout(context.Background(), time.Second)
defer probeCancel()
_, err = snapshot.Classify(probeCtx, f, d, true)
if !errors.As(err, &limit) || errors.Is(err, context.DeadlineExceeded) || calls.Load() != 1 {
t.Fatalf("preview discarded production cooldown: %v, calls=%d", err, calls.Load())
}
})
}
func TestRateLimitRetryPreservesPrivateRequest(t *testing.T) {
f, d := fixture()
f.Counterparty = "Alice Privateperson"