diff --git a/internal/provider/key_allowance.go b/internal/provider/key_allowance.go index 5c9cc171..9bda04e8 100644 --- a/internal/provider/key_allowance.go +++ b/internal/provider/key_allowance.go @@ -31,6 +31,9 @@ type keyAllowance struct { a Allowance at time.Time // when read; zero to read again at the next ask loading bool + // stale: made stale (StaleKeyAllowance) while a reading was out, which + // was asked before the key said it was out and is followed by another + stale bool } // keyAllowanceAge is how long a key's windows as read go unasked again; @@ -124,6 +127,10 @@ func readKeyAllowance(p Provider, id string, gen int) { } e.loading, e.at = false, now renewed := err == nil && e.keep(allowanceOf(ws, now), SpentShareOf(p.Routing), now) + if e.stale { + e.stale, e.loading, e.at = false, true, time.Time{} + go readKeyAllowance(p, id, gen) + } c.Unlock() if renewed { tellRenewed("", id) @@ -215,7 +222,7 @@ func StaleKeyAllowance(p Provider) { c := &keyAllowances c.Lock() if e := c.m[keyAllowanceID(p)]; e != nil { - e.at = time.Time{} + e.at, e.stale = time.Time{}, e.loading } c.Unlock() } @@ -228,7 +235,7 @@ func forgetKeyAllowances() { c := &keyAllowances c.Lock() for _, e := range c.m { - e.at, e.loading = time.Time{}, false + e.at, e.loading, e.stale = time.Time{}, false, false } c.gen++ c.Unlock() diff --git a/internal/provider/sub2api_usage_test.go b/internal/provider/sub2api_usage_test.go index 8b9ac54d..a205dfaf 100644 --- a/internal/provider/sub2api_usage_test.go +++ b/internal/provider/sub2api_usage_test.go @@ -348,6 +348,37 @@ func TestKeyAllowanceForgottenMidRead(t *testing.T) { } } +// A key that says it is out of its windows while a reading of them is +// out has them read again once that reading is back, without being asked: +// the reading was asked before, and the rest it would lift is planned on +// the one after (an ordered group's member read as it is planned). +func TestKeyAllowanceStaleMidRead(t *testing.T) { + keyLimitsHome(t) + week := time.Now().Add(72 * time.Hour).Truncate(time.Second) + release := make(chan struct{}) + var first atomic.Bool + srv, asked := sub2apiServer(t, func() ([]byte, int) { + if first.CompareAndSwap(false, true) { + <-release + return weekUsed(t, "800", week), 200 + } + return weekUsed(t, "400", week), 200 + }) + var once sync.Once + t.Cleanup(func() { once.Do(func() { close(release) }) }) + p := sub2apiKey(srv) + KeyAllowance(p) + for asked.Load() < 1 { + time.Sleep(5 * time.Millisecond) + } + StaleKeyAllowance(p) // out of its week, as the request found + once.Do(func() { close(release) }) + waitAllowance(t, p, func(a Allowance) bool { u, _ := a.For("gpt-6-astra", time.Now()); return u > 0 && u < 100 }) + if n := asked.Load(); n != 2 { + t.Fatalf("asked %d times", n) + } +} + // Until the key is first read (magpie just started), its windows are the // ones its card last showed, kept on disk; a window whose reset passed // since is empty again.