Repository navigation
test(integration): make the llm-log plan and aging tests chunk-boundary proof (Thursday 00:00 UTC hypertable chunk) - #138
Conversation
…ry proof (Thursday 00:00 UTC hypertable chunk) context_llm_log is a TimescaleDB hypertable with 7-day chunks laid on the Unix-epoch grid, so every chunk starts on a Thursday 00:00 UTC. Right after that boundary the newest chunk holds only the hours since midnight. Three integration tests seeded relative to now() and broke deterministically in that window (local 3/3 each on 52a35d0, CI root 41944930718 and PRs #134, #136 identical): 1. TestDistillSpendGuard/WindowAndBackoffExpiryLetItRunAgain aged the log with UPDATE created_at = created_at - 3 h. A row moved across a chunk boundary is refused by the chunk's range CHECK, not re-routed: "violates check constraint constraint_1 (SQLSTATE 23514)", 00:00-03:00 UTC every Thursday, latent since the test landed. Fix: dsAgePipeline copies the pipeline's rows into a temp table, deletes them and re-inserts them aged, in one transaction, so every row lands in the chunk of its new created_at. The guard reads the raw log, not the rollup, so the window it sees is the one the UPDATE meant to produce; the asserted story (trip, both clocks run out, next tick is ok with budget 1) is unchanged. 2. TestMigration158OnPopulatedHypertable/per-consumer window: the seed ended at now(), so the newest chunk in the 30-day window was one heap page (22 rows) and a Seq Scan is the cheaper plan there: "... and 1 seq scan(s), want idx_llm_log_consumer only". Fix: seed, consumer rows and window end at anchor, the start of the chunk holding now() (time_bucket over the hypertable's own chunk interval with origin => epoch; the default origin 2000-01-03 is a Monday, three days off the chunk grid). The test asserts that the newest chunk ends exactly at anchor. Every chunk the window touches now carries the full seed density at every clock; the gate is unchanged: one index scan on idx_llm_log_consumer per chunk, no seq scan, no enable_seqscan, no loosened count. 3. TestQuotaSpendPlan: the relief lookups take their created_at bound at run time, so every chunk is planned; on the Thursday-morning chunk (four rows per key) the planner reads the chunk's created_at index - correct for a chunk of a few pages, meaningless for the access path at scale: "a raw scan reads _hyper_1_6_chunk_context_llm_log_created_at_idx, not idx_llm_log_apikey" (6 of 6 plans). Fix: the accountant runs on a test clock (SetClockForTest, existing seam) one to two hours before anchor, keeping the wall clock's minute of the hour so the edges keep every width (#129); the seed and the 30-day tenant count end at that clock. The newest chunk holds almost seven days, the oldest about three, at every wall clock. All checks stay hard and unchanged: two-sided created_at bound, raw rows <= ceiling 6, rollup via idx_llm_usage_key, exact created_at MCV statistics on every chunk, idx_llm_log_apikey on every raw scan of every chunk. No rows lie after the clock's next full hour, so the rollup is the same seed moved in time: the run logs the same figures as the red run (raw rows 3/5/5, ceiling 6, 1441 tenant rows in 30 days, 10 executed relief lookups). Red -> green, -count=3 each, Thursday 2026-11-05 01:42-01:50 UTC: TestDistillSpendGuard 0/3 -> 3/3 (red: SQLSTATE 23514) TestMigration158OnPopulatedHypertable 0/3 -> 3/3 (red: 1 seq scan) TestQuotaSpendPlan 0/3 -> 3/3 (red: 6 created_at_idx plans) Negative probe: the fixed quota test with its clock moved to Thursday 01:xx (anchor + 1 h) is red again with the same 6 findings; the plans show the chunk-6 created_at_idx scans are the relief-lookup subplans. Packages with -tags=integration -p 1 -count=1 (top-level pass/fail/skip): events 263/0/2, llmlog 44/0/4, backends 102/0/0. golangci-lint (with and without the integration tag) 0 issues, go vet -tags=integration ./... rc 0, both deadcode gates ok. Test files only; no production code, migration or config. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for this test-only change addressing chunk-boundary sensitivity in three integration tests.
No open issues 🎉
What changed in this PR
This test-only PR addresses #137 by making three integration tests independent of the Thursday 00:00 UTC TimescaleDB chunk boundary. Production code and existing plan assertions remain unchanged.
Changes:
- Ages distill log rows through transactional delete and reinsert, avoiding cross-chunk update failures.
- Aligns migration-test seed data and query windows with the hypertable chunk grid.
- Uses a chunk-relative quota test clock while preserving wall-clock minute variation and strict index checks.
| File | Description |
|---|---|
go/internal/llmlog/consumer_attribution_integration_test.go |
Aligns seed and consumer-query timestamps with chunk boundaries and verifies the newest chunk’s end. |
go/internal/events/distill_spend_integration_test.go |
Adds transactional row aging that supports movement across chunks. |
go/internal/backends/quota_plan_integration_test.go |
Aligns seeding, row counts, and accountant windows to a test clock before the chunk boundary. |
Review effort: Balanced
Fixes #137. Test-only: three integration tests fail deterministically after the Thursday 00:00 UTC chunk boundary of the
context_llm_loghypertable (7-day chunks), because the newest chunk is almost empty then. Root52a35d0b, #134 and #136 are red in theheavyandrestshards for exactly this reason; #135 is not involved (its head was green at 23:51 UTC with identical test files).What changes
TestDistillSpendGuard/WindowAndBackoffExpiryLetItRunAgain(internal/events/distill_spend_integration_test.go): aging the llm log byUPDATE created_atmoved rows across the chunk boundary, which TimescaleDB rejects (constraint_1, SQLSTATE 23514). Rows are now deleted and re-inserted 3 h earlier in one transaction via a temp table. The rollup trigger counts them twice on the way; the guard reads the raw log (distill_spend.go:313), so the checked invariant is unchanged.TestMigration158OnPopulatedHypertable(internal/llmlog/consumer_attribution_integration_test.go): seed and window are aligned with the chunk grid viatime_bucket('7 days', now(), origin => 'epoch')(withoutoriginthe bucket starts on Monday, not Thursday — checked against the live DB). The test asserts that the newest chunk ends exactly there. Scan counting is unchanged; noenable_seqscan.TestQuotaSpendPlan(internal/backends/quota_plan_integration_test.go): the test now runs on a test clock 1–2 h before the chunk boundary through the existingSetClockForTestseam, with the minute still taken from the wall clock (so test(backends): TestQuotaSpendPlan is planner-dependent in CI — custom plan touches 102 raw rows via the created_at chunk index instead of idx_llm_log_apikey (2 of 3 rest-shard runs on Go-untouched PRs) #129 stays covered). Every assertion stays hard, including the index check on every chunk. Rejected alternatives: a page-count threshold for the index assertion (the Thursday chunk already has ~400 rows at 01:40 UTC, so the threshold would have been a guess that weakens the assertion), and filling the current chunk with seed rows (writes into the future and inflates the rollup).Verification (
-tags=integration -p 1, inside the red window 01:42–01:50 UTC)52a35d0b:252 … violates check constraint "constraint_1" (SQLSTATE 23514)0/3:411 … and 1 seq scan(s), want idx_llm_log_consumer only0/3idx_llm_log_consumereach):365 a raw scan reads "_hyper_1_6_chunk_context_llm_log_created_at_idx"0/3Package suites: events 263 pass / 0 fail / 2 skip, llmlog 44/0/4, backends 102/0/0.
golangci-lintwith and without the integration tag 0 issues,go vet -tags=integration ./...rc 0, both deadcode gates ok. Not run: the repo-widego test -short -race ./...(no non-test file changed; CI covers it).Report:
.project/plan-openai-api-2026-11-03/reports/bau/fix-ci-thursday-chunk-boundary.md.Summary by CodeRabbit