From ee2588c268dc7e1426a3a9e5b29c73f7fa8865cf Mon Sep 17 00:00:00 2001 From: yetone Date: Tue, 6 Oct 2026 21:29:55 +0800 Subject: [PATCH] =?UTF-8?q?provider:=20a=20key=20that=20says=20it=20is=20o?= =?UTF-8?q?ut=20of=20its=20windows=20while=20they=20are=20being=20read=20h?= =?UTF-8?q?as=20them=20read=20again=20once=20that=20reading=20is=20back=20?= =?UTF-8?q?(TestRestingKeyReadAgainInAnOrderedGroup=20red=20on=20macOS=20C?= =?UTF-8?q?I,=20runs=2037467016755=20and=20on=20main=20since=20555f5706)?= =?UTF-8?q?=20=E2=80=94=20since=20555f5706=20the=20first=20planning=20of?= =?UTF-8?q?=20a=20lone=20key=20reads=20its=20windows=20behind=20the=20requ?= =?UTF-8?q?est.=20When=20that=20reading=20was=20still=20out=20as=20the=20k?= =?UTF-8?q?ey=20refused=20for=20quota,=20StaleKeyAllowance=20only=20zeroed?= =?UTF-8?q?=20the=20time=20it=20was=20read,=20and=20the=20reading,=20asked?= =?UTF-8?q?=20before=20the=20refusal,=20came=20back,=20set=20it=20to=20now?= =?UTF-8?q?=20and=20was=20trusted=20for=20a=20minute:=20the=20rest=20out?= =?UTF-8?q?=20of=20the=20key's=20windows=20wasn't=20lifted=20by=20the=20re?= =?UTF-8?q?ading=20planned=20after=20it,=20though=20its=20limit=20had=20be?= =?UTF-8?q?en=20raised.=20StaleKeyAllowance=20now=20marks=20a=20reading=20?= =?UTF-8?q?that=20is=20out=20(stale),=20and=20once=20it=20is=20back=20the?= =?UTF-8?q?=20key's=20windows=20are=20read=20again=20at=20once;=20forgetti?= =?UTF-8?q?ng=20every=20key's=20windows=20clears=20the=20mark.=20TestKeyAl?= =?UTF-8?q?lowanceStaleMidRead=20(the=20first=20reading=20held=20until=20a?= =?UTF-8?q?fter=20StaleKeyAllowance,=20then=20answered;=20the=20second=20a?= =?UTF-8?q?nswered=20with=20the=20week=20half=20used)=20fails=20without=20?= =?UTF-8?q?the=20change=20(asked=20once)=20and=20passes=20-race=20-count?= =?UTF-8?q?=3D20=20with=20the=20other=20TestKeyAllowance=20tests.=20The=20?= =?UTF-8?q?gateway=20test=20fails=20every=20time=20with=20its=20first=20re?= =?UTF-8?q?ading=20delayed=20300=20ms=20("still=20rests=20by=20window=20?= =?UTF-8?q?=E2=80=A6,=20its=20windows=20read=202=20times",=20as=20CI=20sai?= =?UTF-8?q?d)=20and=20passes=20with=20the=20change.=20go=20test=20-race=20?= =?UTF-8?q?./internal/provider=20(8=20of=209=20runs=20pass;=20the=20first?= =?UTF-8?q?=20run's=20failure=20wasn't=20kept=20and=20didn't=20come=20back?= =?UTF-8?q?),=20go=20test=20-race=20-count=3D10=20-run=20'TestResting|Key|?= =?UTF-8?q?Lone|Allowance'=20./internal/gateway,=20go=20vet,=20gofmt=20-l?= =?UTF-8?q?=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/provider/key_allowance.go | 11 +++++++-- internal/provider/sub2api_usage_test.go | 31 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) 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.