Poller command seam: force-poll and pause-lane through the database #119
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #114
Blocked by: #115
Question
How does an admin action reach the poller without the admin page commanding it?
Settled: through the database.
LaneReporterstays one read-only method (LaneStatus()), so theadmin page keeps being testable with a fake and no poller. A forced action is a flag the Lane
notices on its next pass.
To decide:
seriesfor force-poll is the obvious answer; a Lane-levelpause has no table today, and the map's fog already asks whether a pause survives restart.
DueForLatestCheck's existing predicates — the cutoff, theceiling, and the Sighting-deferral guard. A forced Series must jump the queue without breaking
the pace guarantees ADR-0010 protects.
feedback (pending marker on the row? nothing?) must not imply an immediate refetch.
browser).
Answer
Two pieces of database state, written by the admin page and read by the Lane. Nothing calls the
poller:
LaneReporterstays the single read-onlyLaneStatus(), so the admin page keeps beingtestable with a fake and no poller running.
Forced Poll — one column on
seriesMigration 0013 (0012 is #116's index):
ALTER TABLE series ADD COLUMN force_poll_at bigint NOT NULL DEFAULT 0— unix ms, 0 means never asked.ForceSeriesPoll(site, seriesID):UPDATE series SET force_poll_at = $3 WHERE site = $1 AND series_id = $2. Idempotent; pressing again re-stamps the request time.force_poll_at > latest_checked_at. It clears itself with nosecond write and no sweeper, because
checkOnestampsMarkLatestCheckedbefore the fetch(poller.go:428) — the first attempt ends the pending state whatever the attempt returns. That
stamp-before-fetch order is load-bearing here; a change to it silently makes forced requests
sticky.
old pending marker is evidence a Lane is stuck, which is the symptom this dashboard exists to
surface.
DueForLatestCheck(store.go:972) gains the flag in three places, ands.force_poll_atjoins theGROUP BYlist alongside the other series columns so the plain predicate is usable throughout:So a Forced Poll overrides the waiting rules: the one-hour rest cutoff, the Sighting-deferral
clause (issue #103, ADR-0011), and the finished-only bucket — the owner asking is direct evidence
someone cares about a Series every Reader shelved. It never overrides:
series_url <> ''(nothing to fetch — that row's repair is #121), the
bookmarksjoin (an Orphan Series is #125), theLane's refusal backoff (ADR-0010's containment: hand-forcing a request at a Site that is actively
refusing is the one move that makes it worse), the sidecar-down skip, or the Lane's gap. A forced
Series behind a refusing Site simply goes on the first pass after the 15 minutes.
One pass-level gate it does open:
browserWakeDue. A forced Series wakes a sleeping Chrome —the wake thresholds exist to stop the machine waking itself for a single unattended check, and a
human asking is not that. Cost: seconds of the single shared tab per press, and if the home machine
is off nothing happens at all and the request ages visibly.
Paused Lane — one small table
Same migration:
CREATE TABLE poll_lanes (site text PRIMARY KEY, paused_until bigint NOT NULL). Arow exists only while paused; resume deletes it. Store surface:
PauseLane(site, until)(upsert,rejecting an
untilnot in the future),ResumeLane(site),PausedLanes()for the page,LanePausedUntil(site)for the pass.runLanePassreads its pause row first, ahead of the refusal check: when paused it records theLane state and sleeps until the expiry. Its Series stay due and unstamped — the identical state a
missing browser leaves them in, so nothing new has to handle it, and the queue is waiting intact
when the pause lifts.
one surface whose job is proving the poller is alive.
LATEST_CHAPTER_POLL_ENABLED(main.go:98) stays the onlywhole-poller stop and still needs a redeploy; the case for a runtime one is the case where you
already have a shell.
Acquirer.Acquire(acquire.go:67) is its own path — a Reader'sfirst bookmark of a Series on a paused Site still reads the page. Pause governs the Lane only.
Routes and UI, extending #115's map
POST /admin/series/{key}/poll→ForceSeriesPoll, answering with the swapped Series-rowfragment. Both surfaces carry the button, from one handler and one fragment: the Series list
(the real workflow is arriving from a hygiene count into a filtered list) and the detail page,
which embeds the same fragment.
POST /admin/lanes/{site}/pause(duration from the form) andPOST /admin/lanes/{site}/resume,both answering with the swapped lanes block.
Cinder's confirm rule does not apply. Pending renders as
check requested <age> ago, with noETA: the page would have to guess the Lane's next wake, and that guess is wrong on a browser
Site whenever the home machine is off. The button is not offered at all for a Series with no
series_urlor with zero Readers, rather than offered and quietly ineffective.confirm-gated — picking a duration is already deliberate, and the state is visible, bounded and
instantly reversible.
LaneState. Memoryis empty after a restart while a pause survives one, so a memory-only pause would render a
genuinely paused Site as "no data yet". This does not break #115's split rule — that rule forbids
showing the same figure from two sources, and pause has no in-memory twin.
.attentionclass. Never--ember(New Chapter only), never--danger(destruction).
Pushed onto other tickets
SeriesRowprojectsforce_poll_atand exposes the derived pending bool. Theuncheckedandstalefilters are untouched, because force never writeslatest_checked_at.a Lane row with a duration picker, Pause and Resume.
judgeSightingfor free, so"check now" is also the manual remedy for a suspicious Latest Chapter and can mark the Reader who
raised it. Whether the correction UI advertises it that way is #121's call.
a
poll_lanesrow.queue-jump rules), not now: this map produces the spec.
Glossary: CONTEXT.md gains Forced Poll and Paused Lane.
Rejected
latest_checked_at = 0as the force signal — zero migration, but it corrupts the never-checkedand stale counts the landing page exists to show, and makes a pending marker impossible.
poll_commandstable — an audit trail in disguise, and whether admin actions need one is foghanging on #117.
site = '*'pseudo-row for a global one — a second meaning for theprimary key of a six-row table.
Amendment from #117 (closed):
poll_lanesbecomes the per-Site Lane state row, not a pause-only row. #117 persistsrefusal as
poll_lanes.refuse_until(migration 0014), and a refusing-but-unpaused Lane needs arow — so "a row exists only while paused; resume deletes it" cannot survive. Instead:
paused_untilandrefuse_untilboth default 0,ResumeLanezeroespaused_untilratherthan deleting the row, and
PausedLanes()filterspaused_until > now. One row read at the topof a pass now serves both gates.
LaneReporterandLaneStatus()are deleted. This ticket kept them as the single read-onlymethod so the admin page stayed testable with no poller running; #117 reaches the same goal with
no interface at all, because the store becomes the source of Lane state — an admin test inserts
a pass row instead of constructing a fake. The Lanes page reading pause straight from the
database (already decided here) generalises to everything on it.
skip = 'paused'is #117's Lane Pass record of the pause exit specified here, so a pausedLane renders as paused rather than as a Lane with nothing due.
sulthan referenced this issue2026-08-18 06:59:54 +07:00
Amendment from #124 (closed) — one of your three override cases is replaced:
HAVINGshape is void.HAVING (COUNT(*) FILTER (WHERE b.status <> 'finished') > 0 OR s.force_poll_at > s.latest_checked_at)disappears entirely: #124 deletes the finished Lifecycle bucket, so the plainJOIN bookmarksis the whole "someone holds it" test. The gate moves intoWHEREas(s.finished_at = 0 OR s.force_poll_at > s.latest_checked_at), ands.finished_atjoins theGROUP BYlist next tos.force_poll_at.series.finished_at, migration 0016, owner-written). Your rationale carries over word for word — the owner asking is evidence someone cares about a Series the owner shelved rather than one every Reader shelved. Nothing else in your never-overrides list changes.EligibleSeriesCountgainss.finished_at = 0and deliberately no force clause. That count is the pace divisor, so admitting a forced Series would make one press speed up every other fetch on the Site. The asymmetry between the two queries is intentional and load-bearing.finished_at. Your self-clearing story is unchanged forforce_poll_atitself —checkOne's stamp-before-fetch still ends the pending marker — but the finish survives, because at stamp time nothing has been read yet. Un-finishing is a separate owner action on the Series detail page.nothing-eligibleinteraction with #117: a Site whose Series are all finished counts 0 eligible and logs the skip. A Lane that declined and said why, not a stall.sulthan referenced this issue2026-08-20 20:32:21 +07:00