Pace provider traffic and identify genuine foreground bank requests
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user