Repository navigation
provider: a background reading of an account writes its quota history too - #1318
Conversation
… too GET /v1/magpie/quotas/history was written only by the Usage page's reads, so a magpie only ever serving other magpies - nobody opens its Usage page - kept an empty history while its routing read each account every few minutes. When a login's fresh reading is kept, it is now noted in the history as a page reading is, named for its account (a login's reading itself carries no user); a read that failed is skipped, not told again. The test is the reporter's story: a background LoginUsage, then the history, no Usage page involved; it fails on main. Keys' background readings (KeyAllowance) are not noted here - the page names a key's history line by its key name, which routing's reading does not have. Invited by yetone on #1317 after 137ebee closed the cards half of Signed-off-by: Charan Rathore <[email protected]> #1313.
|
Reviewed at 080a832 (merged onto main ddb6f80). Not merging yet. The double-point question checks out, but the write path needs another round. What was run (Docker, golang:1.26.8,
So no double points. A page read through What needs changing:
Suggested shape: note once per |
…quest path Per review on #1318: a kept reading per account meant N full read/parse/write cycles of quota-history.json for N accounts, and the write ran before close(r.done), so routing's waiters on a reading waited on the disk, the writes serialized under quotaHistMu. loginUsageAt now notes the batch once after wg.Wait(), each reading named for its account, through noteHistory, whose write runs in its own goroutine; a cached reading notes nothing new (its ReadAt is no newer) and a failed one is skipped. Tests: several accounts cost one write (TestBackgroundReadsNoteHistoryOnce) and a held write holds no waiter (TestHistoryWriteHoldsNoWaiter). Signed-off-by: Charan Rathore <[email protected]>
|
Both findings reproduced here before changing anything (on 080a832, with a counter/blocker around the per-account note): two accounts cost 2 noteQuotaHistory calls, and a held history write held LoginUsage past 2 s until released. Both real. Fixed in d709188, along your suggested shape:
Regression tests, both in internal/provider/quota_history_background_test.go:
Run here (-tags nogui): the three history tests pass, also at -count=2; full go test ./internal/provider ok (45.8s); go vet clean. Not run: full gateway suite (this box's per-command limit; its quota/remote tests passed before), WebKit/GUI, a real ChatGPT account. |
|
Reviewed What was run (
One Cause: the default Suggested fix (either):
Smaller point: in |
The detached write goroutine outlived its caller: it read Path() and wrote quota-history.json after a test had returned, racing t.TempDir removal and leaking into the next test's HOME (go test ./internal/provider red with the PR, green on main). Note synchronously in loginUsageAt after wg.Wait(): every reading has completed and released its in-flight marker by then, so no routing waiter is held. Only the LoginUsage caller pays for one write per batch, and Allowances already calls it from its own background goroutine. TestHistoryWriteHoldsNoWaiter now holds the write during the first LoginUsage and asserts a second caller on the same account answers from the completed readings, with a timeout on the write-start wait. Signed-off-by: Charan Rathore <[email protected]>
|
Thanks for the careful run. The mechanism is clear from your logs - the detached goroutine reads Path() and writes after the test has returned, racing TempDir removal and the next test's HOME. I took your first option: the note is now synchronous in loginUsageAt after wg.Wait(). Every reading has completed and released its in-flight marker by then, so no routing waiter is held. Only the LoginUsage caller pays for the one write per batch, and Allowances already calls it from its own background goroutine. TestHistoryWriteHoldsNoWaiter now checks a second caller on the same account, as you suggested: the write is held during the first LoginUsage, the test asserts that caller has not returned (200ms window), then a second LoginUsage on the same account answers from the completed readings. The write-start wait has a 10s timeout now. On the previous head this test fails immediately ("LoginUsage returned before the held write finished"), so it pins the new semantics. Ran on beccb9a: the three history tests with -race -count=3 pass; go test -tags nogui ./internal/provider passes; go vet and gofmt clean. Not run: the full ./... suite and the GUI. One honest note: my machine did not reproduce the 3/3 red you saw (one full package run passed even before this change) - the fix removes the cause your runs identified rather than a failure I could trigger here. |
|
Reviewed What was run (macOS, go1.26.8,
Non-blocking, for a follow-up:
|
|
已在 v0.1.1124 发布,谢谢 @charan-rathore! |
|
Thanks for the quick release! |
…e code standards, adds gofmt and a Linux/Windows go vet to the checks, and adds lessons on Windows CI, telemetry disclosure, timing tests, guarantee tests, real vendor pages, CI's Go version and large commits without a body 192 commits and 18 releases on 10-08 (through 3c62913). The two lessons seen a third day keep their concrete steps in acceptance criteria 4 and 5 (git grep a reworded string, a gateway guard through the real server stack). bbf9914's syscall.Getsid shows a cross-OS build doesn't compile _test.go, so vet joins the checks. #1318's test that passes with the waiter held is filed as #1358. Ran: read both files after the edit; docs only, no code changed.
… of cached readings notes nothing new (#1358) TestHistoryWriteHoldsNoWaiter is named for the guarantee that a history write never holds a caller waiting on a reading, but its second caller arrives after the reading has landed and answers from the cache: with the note moved back into the reading's goroutine before close(r.done) (the regression it is for), the test still passes. The new TestHistoryWriteHoldsNoReadingWaiter starts caller B while the reading is still in flight, so B waits on the reading's done channel: with the regression injected, both callers are held ("both callers held by one history write", 5/5); on main, the reading's done channel closes before the note, and caller B answers. Also from the #1318 review: every LoginUsage noted its batch's history by reading and parsing quota-history.json under quotaHistMu even when every reading in the batch was answered from the minute's cache, so a cached batch re-read the file for points it already had (TestCachedBatchNoteSkipsTheRead failed with "a cached batch read the history file 1 times"). noteQuotaHistory now skips a batch whose readings are all no newer than what was last noted for their account; the noted times are kept per history file and in memory, so a new file (or a test's fresh home) starts clean, and a fresh reading is always newer than what was noted and goes through as before. Verified: go test -tags nogui ./internal/provider -count=1 (full package); the five history tests under -race -count=5; gofmt -l clean on the three touched files; go vet -tags nogui ./internal/provider; go build and GOOS=linux/GOOS=windows go build -tags nogui. Generated-by: GLM-5.3-Flash (ZCode) Review-run: with the note moved into the reading's goroutine before close(r.done), TestHistoryWriteHoldsNoReadingWaiter fails 5/5 ("both callers held by one history write") while TestHistoryWriteHoldsNoWaiter still passes; on origin/main's code it passes 10/10 under -race; the skip taken out fails TestCachedBatchNoteSkipsTheRead ("read the history file 1 times"); skip always taken fails 5 history tests; no per-path reset fails TestMergeQuotaHistory; no goroutine left past the tests; go test -tags nogui ./internal/provider/... ok; history tests -race -count=10 ok; gofmt -l clean; go vet -tags nogui ./internal/provider/ on darwin, GOOS=linux and GOOS=windows ok.
Taking up the follow-up you named on #1317:
/v1/magpie/quotas/historywas written only by the Usage page's reads, so a magpie only ever serving other magpies (nobody opens its Usage page) kept an empty history while routing read each account every few minutes.The fix
When a login's fresh background reading is kept (
loginReading), it is now noted in the quota history the same way a page reading is, named for its account - a login's reading itself carries no user. A read that failed is skipped insidenoteQuotaHistory, not told again; a cached minute-old reading writes nothing new.Test
TestBackgroundLoginReadNotesQuotaHistory(internal/provider/quota_history_background_test.go): the reporter's story from #1313 - a backgroundLoginUsageagainst a fake Codex backend, thenQuotaHistories, no Usage page involved.go test -tags nogui ./internal/provider/passes (45.8s), plus-count=2on the history tests and the gateway's quota/remote-magpie tests;go vet -tags noguiclean on internal/provider.Not run: the full gateway suite (exceeds this box's per-command limit; the quota and remote-magpie tests in it pass), the WebKit/GUI lanes (no GUI here), and an end-to-end run against a real ChatGPT account.
Out of scope
Keys' background readings (
KeyAllowance) are not noted here. The page names a key's history line by its key name (or mask, when several), and routing's reading of a key does not have that name, so noting it there would split the line. Happy to take it as a follow-up if you want it.