Skip to content

mono - test: Stop expiry tests from racing a 100 ms TTL - #2182

Open
jaredwray wants to merge 1 commit into
mainfrom
claude/wizardly-lamport-lyi7hy
Open

jaredwray wants to merge 1 commit into
mainfrom
claude/wizardly-lamport-lyi7hy

Conversation

@jaredwray

Copy link
Copy Markdown
Owner

Please check if the PR fulfills these requirements

  • Followed the Contributing and Code of Conduct guidelines.
  • Tests for the changes have been added (for bug fixes/features) with 100% code coverage. Test-only change; no source changes and coverage is unchanged.

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Test fix (timing-dependent tests).

Problem

Seven tests in six adapters set a value that expires in 100 ms, then read it back straight away expecting it to still be there. If set() plus that first get() take longer than 100 ms, the read finds the value already expired and the test fails. That happens under CI load, when every package tests at once.

The Postgres copy needed a retry in the #2180 branch run: "populates the expires column for non-JSON encoded values…", 592 ms, retry x1.

Reproduced locally by delaying the first get() by 150 ms, standing in for a slow round trip. Every current test failed: SQLite, Postgres, both Valkey tests, and Redis.

While checking these I found a second problem. The Postgres and MySQL copies of "populates the expires column…" can't catch the bug they are named after:

  • Keyv's own expiry check (checkExpired, on by default) deletes the expired value itself.
  • So the test passed even when the adapter left the expires column empty.
  • Confirmed by patching the Postgres adapter to store NULL in that column: the current test still passed.
  • The SQLite copy already turns checkExpired off for exactly this reason.

Changes

Postgres, MySQL, SQLite and Cloudflare KV (the adapter decides expiry with Date.now()):

  • The tests freeze Date with vi.useFakeTimers({ toFake: ["Date"] }), then move it past the TTL with vi.setSystemTime() instead of sleeping.
  • Only Date is faked, so the database drivers' real timers keep running.
  • The first read can no longer race the clock, and each test runs 200 ms faster.

Redis and Valkey (the server expires keys on its own clock, so faking Date can't help):

  • The expiry goes from 100 ms to 1 s, and the wait afterwards from 200–300 ms to 1.2 s.
  • The first read now has ten times the headroom.
  • Cost: about 3 seconds more test time in total.

Postgres and MySQL "populates the expires column…":

  • Turn checkExpired off, as the SQLite copy already does, so expiry comes only from the store's expires column.

The other short-expiry tests I checked only assert expiry after a wait, which is safe, so I left them alone.

Verification

  • 150 ms delay on the first read: each current test fails, and the fixed version passes. Run for SQLite, Postgres, both Valkey tests, and Redis.

  • Postgres adapter patched to leave expires empty: the current test passes, which is the gap described above. The fixed test fails with expected 'calco' to be undefined.

  • Full runs with retries off:

    Package Scope Tests Server
    SQLite full suite 168/168 local file
    Postgres test/test.ts 153/153 local PostgreSQL 16
    Cloudflare KV test/test.ts 157/157 Miniflare
    Redis test/set.test.ts 12/12 local redis-server
    Valkey test/set.test.ts 10/10 local redis-server standing in
  • MySQL: not run locally, since no server is available here. Its change mirrors the Postgres one, and its get, has and clearExpired read Date.now() the same way. CI covers it.

  • Lint: biome check --error-on-warnings is clean on all six files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01X5LJCvt3pkR7FtfyAdzg5x


Generated by Claude Code

Seven tests in six adapters set a value that expires in 100 ms, then read
it back expecting it to still be there. When set() and get() together
take longer than 100 ms under CI load, the read finds the value expired.
The postgres copy needed a retry in the #2180 branch run.

- postgres, mysql, sqlite and cloudflare-kv decide expiry with
  Date.now(), so these tests now freeze Date and move it past the TTL
  instead of sleeping. They can't race the clock, and run 200 ms faster.
- redis and valkey expire keys on the server's clock, so those tests get
  a 1 s window instead of 100 ms.

The postgres and mysql copies also turn off Keyv's own expiry check,
as the sqlite copy already did. With it on, Keyv deleted the expired
value itself, so the tests passed even when the adapter left the
expires column empty, the bug they are meant to catch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5LJCvt3pkR7FtfyAdzg5x
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T14:11:35.130983Z 68e334c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copy link
Copy Markdown
Owner Author

cloudflare-keyv-integration is red, but the failure isn't from this PR.

What the check runs: pnpm test:live, which calls the real Cloudflare KV REST API through storage/cloudflare-kv/test/live/live.test.ts. This PR doesn't touch that file or any adapter source. Its only Cloudflare KV change is in test/test.ts, which the live config (test/live/**) doesn't include.

What fails: "sets, gets, checks, and deletes a value", on all three attempts.

  • delete(key) returns true, but the get(key) right after it still returns the value.
  • The other four live tests pass.
  • Cloudflare KV is eventually consistent, so a read straight after a write or delete can be served from cache for up to 60 s.
  • The same test passed on main on Sep 28. That points to Cloudflare-side caching rather than a code change.

Fix: none exists yet. Proposed patch for a separate PR: poll until the delete is visible, instead of asserting it on the very next read.

expect(await store.delete(key)).toBe(true);
// KV is eventually consistent: a read right after a delete can still be served from cache.
await vi.waitFor(async () => expect(await store.get(key)).toBeUndefined(), { timeout: 65_000, interval: 2_000 });
expect(await store.has(key)).toBe(false);

That also needs vi imported, and testTimeout in vitest.live.config.ts raised above 65 s.

Re-running the job once now.


Generated by Claude Code

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (53d0658) to head (68e334c).

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #2182      +/-   ##
===========================================
+ Coverage   99.96%   100.00%   +0.03%     
===========================================
  Files          56        56              
  Lines        5790      5790              
  Branches      996       998       +2     
===========================================
+ Hits         5788      5790       +2     
+ Misses          2         0       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants