3303a55b20
Closes #100. Each Site runs its own Poll Lane: an independent goroutine with its own rest and pace from the registry (`backend/internal/latest/sites.go`), replacing the shared cooldown/interval/stagger/batch configuration. Rest (1h, all six Sites including the browser trio) is enforced by the due query's WHERE clause; the Lane sleeps its effective gap between fetches — the registry 10s, or rest/eligible when a Site holds enough Series, floored at 1s with a Site-naming warning when the floor engages. Lane-local failure handling: - Two challenge-held results stop that Site's Lane for 15m; the probes keep their stamp, untried Series stay due. - A lost browser sets a shared Poller flag: the other browser Lanes skip their passes for the same 15m (no stamp-per-pass-per-Lane on a dead tab), then decay and probe again. - Browser wake gate preserved (5 due, or one waiting 15m, ADR-0005); one tab shared by the three browser Sites; "browser lane behind by X" logged every pass. - Cover work (healing a stored source URL and filling a blank from the series page) runs in the background so a slow CDN cannot consume a Lane's gap. Removed: `LATEST_CHAPTER_POLL_{COOLDOWN,BROWSER_COOLDOWN,INTERVAL,BATCH,STAGGER}` and the 6h browser rest. Only `LATEST_CHAPTER_POLL_ENABLED` remains; DEPLOY.md documents the exact `.env` edit. ADR-0010 records the decisions. Reviewed-on: #106 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
92 lines
4.8 KiB
Markdown
92 lines
4.8 KiB
Markdown
# ADR-0010: Poll Lanes — one independent Poll stream per Site
|
|
|
|
Date: 2026-08-16
|
|
Status: accepted
|
|
|
|
## Decision
|
|
|
|
Replace the single shared polling pace with one **Poll Lane** per Site: an
|
|
independent goroutine that polls only that Site's Series, paced by that Site's
|
|
registry entry. Pace moves out of config and into the Site registry
|
|
(`internal/latest/sites.go`): every entry carries a `Rest` (how long a Series
|
|
rests between Polls) and a `Gap` (how long the Lane waits between fetches).
|
|
|
|
Rest is enforced by the due query's WHERE clause (`latest_checked_at <= now -
|
|
Rest`), never by a timer — the same mechanism that enforced the old cooldown.
|
|
The Lane enforces its own gap by sleeping between fetches. `effectiveGap` is
|
|
the registry gap, or one hour divided by the Site's eligible Series count when
|
|
that is smaller, never below one second.
|
|
|
|
The five environment settings that used to size the shared pace —
|
|
`LATEST_CHAPTER_POLL_COOLDOWN`, `_BROWSER_COOLDOWN`, `_INTERVAL`, `_BATCH`,
|
|
`_STAGGER` — are deleted. Only the kill switch `LATEST_CHAPTER_POLL_ENABLED`
|
|
remains. No deployed `.env` may carry the deleted knobs.
|
|
|
|
## Why
|
|
|
|
The shared pace capped the whole backend at roughly 180 Polls an hour (one
|
|
20-second stagger across one queue). ~60 Series today, scaling to hundreds or
|
|
thousands, would stretch the hour beyond what the New Chapter signal can
|
|
tolerate. Worse, the queue mixed Sites with very different costs: kagane and
|
|
comix pay seconds of a serialized single-tab Chrome per Poll (a challenged
|
|
page, ADR-0005), and one hostile Site burning its challenge timeout made every
|
|
other Site's Series wait — "one hostile Site can eat most of an hour".
|
|
|
|
Lanes fix both at once:
|
|
|
|
- **Throughput scales per Site.** The six Lanes fetch concurrently; a Lane's
|
|
own gap paces it. The browser Lanes' combined ceiling stays about 360 Polls
|
|
an hour (one tab), and when they cannot keep up the wait past Rest grows and
|
|
is logged every pass — the "behind by X" measurement, so the decision to
|
|
give browser Sites more pages is made from data.
|
|
- **Hostility is contained.** A refusal (two challenge-held reads in one
|
|
pass) stops only that Site's Lane for `refuseBackoff` (15m); the rest of
|
|
that Lane's Series stay unstamped and due. A lost browser gates the other
|
|
browser Lanes' passes for the same window — the flag is shared Poller
|
|
state, so the loss is noticed once instead of once per Lane per pass, and
|
|
decays after 15m so the Lanes probe again. One Site can no longer tax the
|
|
others.
|
|
|
|
## Tradeoffs and rejections
|
|
|
|
- **Per-Site env knobs** (e.g. `KAGANE_POLL_GAP`) rejected: the registry is
|
|
the single place pace lives, testable and reviewable; config knobs would
|
|
recreate the shared-pace sprawl with six times the surface. All six entries
|
|
are deliberately uniform at first — rest an hour, gap ten seconds — so the
|
|
structure exists to differ without inventing numbers for Sites that have
|
|
not earned them.
|
|
- **Dynamic gap** (`rest / eligible`) is the one knob that stays automatic:
|
|
a Site with more Series than one per ten seconds would otherwise back up
|
|
behind its own gap, and the per-Series share of the hour is the natural
|
|
pace. The ten-second default is not arbitrary: one request per ten seconds
|
|
is the strictest rate rule a free-plan Site can even express (per-zone
|
|
rate limiting, as documented in
|
|
`docs/research/cloudflare-bot-scoring-and-poll-cadence.md`), so the
|
|
default pace is exactly what the most restrictive Site would demand of us.
|
|
The computed gap never goes below one second and logs loudly when the
|
|
floor engages.
|
|
- **Timer-based pacing** rejected: the old ticker made the poller's rate a
|
|
function of wall clock rather than of what was due. The due-query cutoff is
|
|
the only rate authority; the Lane sleep just prevents hammering.
|
|
- **Batch size** (the old `_BATCH` cap) is gone with the shared pace: a Lane
|
|
processes everything due, paced by its gap. There is no global queue left
|
|
to bound.
|
|
|
|
## Constraints preserved
|
|
|
|
- Stamp-before-fetch ("attempted" semantics): an untried Series stays due, so
|
|
a browser that appears after a restart finds its full queue waiting.
|
|
- Browser wake gate (ADR-0005): a browser Lane leaves Chrome asleep below
|
|
five due Series and 15 minutes of wait, per Lane.
|
|
- The browser is not in the API stack (ADR-0006): an unreachable browser
|
|
degrades a Lane exactly as an unset `BROWSER_WS_URL` — browser-only Sites
|
|
skipped, plain-TLS unaffected, stored covers still served.
|
|
- Cover heals moved to background goroutines (joined by the test suite via
|
|
`waitCovers`) so a slow cover CDN cannot consume a Lane's gap.
|
|
|
|
Supersedes the pace mechanics of ADR-0003's "raise throughput instead" note
|
|
(the stagger cut it rejected is what the per-Lane gap replaces) and the
|
|
6-hour browser cooldown introduced with the browser-backed Sites; the
|
|
1-hour browser rest was already cleared as safe by
|
|
`docs/research/cloudflare-bot-scoring-and-poll-cadence.md`.
|