Files
mangaBookmark/docs/adr/0013-commands-through-the-database.md
sulthan fe2cd12049 feat(web,store,latest): forced poll — Check now stamps a Series for the next Lane pass (#146)
The control writes series.force_poll_at (column landed in migration 0014)
and never commands the poller: pending is derived as force_poll_at >
latest_checked_at and self-clears because the check stamp is written before
the fetch. The due query's forced flag overrides the rest cutoff, the
Sighting-deferral and finished-only clauses, jumps the queue, and wakes a
sleeping browser Lane; it never overrides an empty series_url, the Bookmarks
join, the refusal backoff, the sidecar-down skip or the Lane gap.

ADRs: 0013-commands-through-the-database.
2026-08-22 00:53:51 +07:00

3.0 KiB

ADR-0013: Commands through the database

Date: 2026-08-22 Status: accepted

Decision

Owner interventions are facts about rows, never commands to the poller. Check now (POST /admin/series/{key}/poll) writes one stamp — series.force_poll_at, unix ms, zero meaning never asked (the column landed in migration 0014) — and the poller's next pass reads it through Store.DueForLatestCheck. The control never signals the running process, so a request survives a restart, and the whole surface is testable with no poller running at all.

Pending is derived, never stored: a Series is pending while force_poll_at > latest_checked_at. It self-clears with no second write and no sweeper because the check stamp is written before the fetch (the same "attempted" discipline as ADR-0010) — the first attempt ends the pending state whatever the attempt returns. There is no expiry: a request the Lane never reaches keeps ageing in the UI, and an old pending marker is itself the evidence that a Lane is stuck. Writing again re-stamps the request time; the write is idempotent.

Queue-jump rules. A forced Series overrides exactly three gates in the due query: the rest cutoff, the Sighting-deferral clause and the finished-only bucket, and it sorts to the front of the queue (ORDER BY forced DESC, reader_count DESC, latest_checked_at ASC). It never overrides an empty series_url (nothing to fetch), the Bookmarks join (a Series no Reader holds has no consumer for the result), the Lane's refusal backoff, the sidecar-down skip, or the Lane's gap — the last three are poller-side gates the query cannot see and must not. The one pass-level gate a forced Series does open is the browser wake threshold: a human asking wakes a sleeping Chrome, where the thresholds exist to stop the machine waking itself for one unattended check. If the home machine is off, nothing happens and the request ages visibly, which is correct.

Rejected: zeroing the check stamp as the force signal. It would corrupt the never-checked and stale counts the landing page exists to show, and make a pending marker impossible.

Why

A stuck-looking Series previously waited for its turn in the Lane's hour, and there was no way to ask for one check sooner. A direct poller command would have been lost on every restart and untestable without a running poller; a row the poller already reads is neither. Deriving pending from the two stamps keeps the flag honest across restarts and makes the mechanism two column writes and three query clauses instead of a state machine.

Constraints

  • The finished-status clause the force flag overrides is today's Lifecycle test; a later spec in this series deletes it wholesale rather than amending it, so the clause stays as it stands.
  • The control is unconfirmed (it takes nothing away) and renders no .confirm-row; it is hidden on a Series with no series_url and on an orphan — the same pair the due query refuses to override.
  • The answer to a press is the freshly rendered row, so the figures describe the state after the press.