Skip to content

Review the collector: the raw line no longer waits on the database - #58

Merged
baz8080 merged 3 commits into
mainfrom
claude/busy-volta-pbvvy5
Sep 24, 2026
Merged

baz8080 merged 3 commits into
mainfrom
claude/busy-volta-pbvvy5

Conversation

@baz8080

@baz8080 baz8080 commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

lift_status/ and scripts/ were written before PRs here went through a review agent, and have barely changed since (parse.py, client.py and __main__.py not at all). They are also the only code whose mistakes a rebuild cannot undo: a response that never reached the raw log is gone. So they got one whole-file review, and the rest of the repository did not: lift_access/ and most of lift_site/ have been reviewed diff by diff since 2026-08-26, and the real-corpus and golden tests pin what they publish.

The review made ten findings. Eight are fixed here, one turned out not to be a real path, and one is left for now. All ten are in notes/collector-review.md. A second review, of this PR itself, is addressed in the last commit.

Fixed

  • A corrupt database cost the response. Store() opened SQLite before the raw line was written, so a database broken by a power cut stopped every poll from reaching the log, with a traceback and no alert. append_raw now writes the line before the database is opened, and it is the only way to write one. A database failure is exit 7, and the banner says what to do for a lock, a full card or corruption. A fetch that failed keeps its own alert even when the database is broken too.
  • A full SD card never reached the storage alert. check_writable touches an empty file, which passes on a full disk. An OSError from the append now goes to the storage banner.
  • A line cut off by a power cut swallowed the next one. The append now adds the missing newline first, so only the fragment is lost.
  • A truncated or corrupt gzip body escaped the client. EOFError and zlib.error are neither an OSError nor an HTTPException. They are now a TransientError, so they get retried and logged.
  • A second outage within a day of the first was silent. The alert dedup marker outlived a recovery. It now clears after four consecutive clean runs (two hours), and a suppressed failure resets the count, so a flapping API is still one alert a day.
  • systemd could kill a poll before it logged anything. TimeoutStartSec=60 was below the client's own worst case, so it is now 300.
  • The backup could hang forever. A oneshot unit has no start timeout by default, and ssh had no keepalive. ssh now has ConnectTimeout and ServerAliveInterval, and the unit has a 15-minute cap. The script traps TERM, so hitting the cap still alerts.
  • A sort -u merge replayed out of order. sort -u orders lines by body, but replay followed line order, so two collectors' merged logs would have rebuilt the wrong history. Replay now sorts each file by fetched_at_utc, stably. The cost, written into the note: after a power cut, a catch-up poll stamped up to an hour early is applied before runs it followed. On the real data this moves nothing.

Left for now

  • time-sync.target does not wait for NTP unless systemd-time-wait-sync.service is enabled. Enabling it risks polls that silently never run if NTP is unreachable, so it is left as it is, by the owner's call.

Deploying

The unit files and the backup script only take effect on the Pi after scripts/install-native.sh and a daemon-reload.

Verification

  • ruff check and scripts/no-em-dash.sh are clean
  • Full suite OK with LIFT_STATUS_DATA_DIR set. The 12 skips were already there: no survey directory, and running as root
  • Collector suites pass on Python 3.11
  • rebuild then stats against lifts-data gives identical output before and after, on every commit
  • The backup script, run with a git fetch that hangs and its process group sent TERM, sends its alert with exit 143

🤖 Generated with Claude Code

https://claude.ai/code/session_01AnrcmsrTHYCgnVGyqBqji4

lift_status/ and scripts/ predate review on this repository and are the
only code whose mistakes a rebuild cannot undo, so they were reviewed
whole, once. Seven of ten findings are fixed here; notes/collector-review.md
has all ten, including the one that was not a real path and the two left
open as decisions.

- Write the raw line before opening SQLite. A database left corrupt by a
  power cut stopped every poll from reaching the log, with no alert; it
  is now exit 7 with a banner saying the response was kept.
- Route an OSError from the append to the storage alert: the empty probe
  file in check_writable passes on a full SD card.
- Finish a line a power cut left without a newline before appending, so
  the fragment no longer takes the next good record with it.
- Treat a truncated or corrupt gzip body (EOFError, zlib.error) as a
  transient error instead of letting it escape unlogged.
- Clear the alert dedup marker on a clean run, so the same fault coming
  back within a day alerts again.
- Raise the poll unit's TimeoutStartSec above the client's own worst case,
  and bound the backup unit and its ssh so a stalled push cannot hang it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnrcmsrTHYCgnVGyqBqji4
…same

sort_keys=True exists so two collectors' logs can be merged with sort -u,
which is also how git's conflict on a shared day file would be resolved.
But sort -u orders lines by their first key, body, and replay followed
line order, so a merged file replayed out of time order: closures, miss
counts and listing stretches would all have come out wrong.

iter_raw_lines now sorts each file by fetched_at_utc, stably. The old
line-order rule guarded against clock jumps, but only partly, since a
pre-NTP stamp already lands in the wrong day's file. All 2,224 real lines
are already in time order within their files, and rebuild then stats
gives identical output before and after.

The NTP wait stays as it is, by the owner's call, and the note says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnrcmsrTHYCgnVGyqBqji4
- A fetch that failed keeps its own alert when the database is broken
  too. The database banner said the response was kept when there was no
  response, and hid a rejected key behind exit 7.
- The database banner says what to do for a lock, a full card and
  corruption, instead of telling every error to move the database aside.
- The alert marker clears after four consecutive clean runs, not the
  first. Clearing on one clean poll made a flapping API alert on every
  failure, which is what the repeat window exists to stop.
- backup-to-git.sh traps TERM: dash skips the EXIT trap on a signal it
  does not trap, so the new 15-minute cap ended a stalled push silently.
- Store.write_raw is gone, so the only way to write the raw line is the
  one that does not open the database first; the newline check reads
  through the append handle.
- The replay-order note states the power-cut case it gives up, and the
  CLAUDE.md row is a pointer again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AnrcmsrTHYCgnVGyqBqji4
@baz8080
baz8080 merged commit 765277a into main Sep 24, 2026
3 checks passed
@baz8080
baz8080 deleted the claude/busy-volta-pbvvy5 branch September 24, 2026 08:29
baz8080 added a commit that referenced this pull request Sep 24, 2026
Chapter 17 closes chapter 16's two bugs, neither the way its issue
proposed, and corrects chapter 16's Kishoge diagnosis. Re-measuring for
it found August's national figure moved from 76% to 75% after August
ended: Tullamore's planned works came back on 16 September and the
pooled grace took its August A to a D on the 19th. Narrated as a
finding, not decided.

Chapter 18 is the collector's first whole-file review, and corrects two
sentences of chapter 01: the raw line waited on the database, and a
sort -u merge reorders, so replay now sorts by fetch time.

The closing becomes 19a and 19b, forward pointers go into 01, 10 and 16,
and every current figure is re-measured at 24 September.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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