Skip to content

perf(api): the sync freshness poll rides along the request's D1 batch - #150

Merged
remcovanmook merged 1 commit into
mainfrom
perf/poll-rides-request-batch
Oct 10, 2026
Merged

remcovanmook merged 1 commit into
mainfrom
perf/poll-rides-request-batch

Conversation

@remcovanmook

Copy link
Copy Markdown
Owner

Why

Every 15 s per isolate, the API worker polls _sync_meta to invalidate L1. The poll ran at the top of the request as a separate D1 query, alongside the request's own. If the D1 session or binding keeps only one query in flight, the request waited out a full extra round trip (~20 ms) whenever the poll fired.

Change

  • The poll travels with the request's first D1 call. The router claims it right before dispatch (claimSyncPoll) and hands it to the per-request session wrapper (db.carry). The wrapper sends it in the same batch() as the request's first D1 call, whether that's all(), first() or batch(). It costs no round trip and never competes with the request's query.
  • Its results stay separate. Its rows go to the poll's own apply step, and its D1 metadata doesn't count toward the request's Server-Timing d1.
  • No D1 call, no wait: a request that never reaches D1 (L1/L2 hit) runs the claimed poll after the response, via ctx.waitUntil on the bare session.
  • core/sync_state.js: refresh() is split into the poll statement, applyPoll(), and claimPoll()/runPoll().
  • REST keeps ensureSyncFreshness unchanged.

Tests

  • tests/unit/core/d1stats.test.js (riders):
    • first()/all() send the rider in the same batch, its rows reach apply and not the caller, and its metadata isn't counted;
    • only the first call carries it;
    • batch() appends it and strips its result;
    • takeRider() hands back an unsent rider;
    • a failing apply doesn't fail the request.
  • tests/unit/api/sync_poll_rider_sqlite.test.js (end to end, mocked clock):
    • poll due and an L1 miss: one batch carries both, no separate poll query;
    • within 15 s: no poll;
    • poll due and an L1 hit: the poll runs separately after the response.
  • The SQLite shim counts batched reads like all()/first().
  • Workers 1496/1496, integration 24/24, upstream patterns 60/60, typecheck clean, pre-push hook passed.

🤖 Generated with Claude Code

Every 15 s per isolate the API worker polls _sync_meta to invalidate L1.
It ran at the top of the request as a separate D1 query racing the
request's own: if the D1 session/binding keeps one query in flight, the
request waited out a full extra round trip (~20 ms) on every poll.

The poll is now claimed right before dispatch and attached to the
request's first D1 call through the per-request session wrapper
(core/d1stats.js riders): the same batch() carries it, so it costs no
round trip and never competes with the request's query. Its rows go to
the poll's apply step, its meta doesn't count toward the request's
Server-Timing d1. A request that never reaches D1 (L1/L2 hit) runs the
claimed poll after the response, on the bare session. The REST worker
keeps ensureSyncFreshness unchanged.

core/sync_state.js splits refresh() into the poll statement, applyPoll()
and claimPoll()/runPoll(); the SQLite test shim counts batched reads like
all()/first().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@remcovanmook
remcovanmook merged commit 59da830 into main Oct 10, 2026
10 checks passed
@remcovanmook
remcovanmook deleted the perf/poll-rides-request-batch branch October 10, 2026 15:10
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