Skip to content

perf: Pi infrastructure tuning (Backfill Place, atomic power upsert, healthchecks, test isolation, idle-session reaper) - #51

Merged
danielroberts20 merged 11 commits into
mainfrom
perf/pi-infrastructure
Oct 7, 2026
Merged

danielroberts20 merged 11 commits into
mainfrom
perf/pi-infrastructure

Conversation

@danielroberts20

@danielroberts20 danielroberts20 commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Summary

An infrastructure and performance pass over the Pi that runs TravelNet. The common thread: stop long database write locks, cut per-run overhead of the frequent flows, make failures visible, and keep dev work from starving production.

Changes

Commit What Measured
fix(power) Shelly upsert does its read-modify-write in one BEGIN IMMEDIATE transaction; the 5-minute flows use plain functions, log the happy path at DEBUG and drop the per-run INFO relay ~41 → ~8 Prefect DB rows per run; fixes a lost-update race
perf(places) Backfill Place: indexed range lookups on the base tables instead of scanning location_unified, lookups on a read-only connection, writes in 500-row transactions lookup 1.15 s → 3 ms; run ~15 min → ~1 s; ends the daily "database is locked" failures of the flows that overlapped it. Identical results on 598 real lookups
perf(location) Noise flagging runs daily (it ran hourly despite being described as daily) and horizontal_accuracy is indexed tier-1 query 4.6 s → 3 ms
test conftest.py points Prefect at a closed port; the FX and GBP flow tests call .fn() The suite used to write real runs into the production Prefect server; suite time halves
feat(ops) GET /health; Docker healthchecks for the API, dashboard, nginx and Prefect worker; services reach Prefect over the Docker network
feat(ops) (draft) Self-healing host health monitor in docs/drafts, with --dry-run not installed by this PR
test(ops) Tests for the reboot script waiting on in-flight flow runs the script is gitignored, so the tests skip when it is absent
feat(ops) Reaper for idle dev sessions (systemd user units in docs/drafts)
docs Audit and change log (public copy: hardening details and internal addresses omitted)

Deploy notes

  • Restart the API and the Prefect worker (flows and the new index are loaded at start-up); docker compose up -d applies the compose healthchecks and the internal Prefect URL.
  • nginx.conf gains a /nginx-health location on port 80.
  • The new index (idx_overland_accuracy) is created automatically on API start.
  • The Dashboard needs its companion PR for /healthz (Dashboard repo).

Testing

Covered by the unit tests added in each commit (the suite grows by ~120 tests, including mutation-checked tests for the new safety properties). CI runs the suite on this branch.

🤖 Generated with Claude Code

danielroberts20 and others added 9 commits October 7, 2026 08:51
Get Power Statistics read the day's aggregate on one connection and wrote it
on another, so a concurrent writer could lose an update and the read->write
upgrade failed immediately with 'database is locked'. PowerDailyTable
.upsert_reading now does the read-modify-write in one BEGIN IMMEDIATE
transaction (merge_reading is a pure function).

Both 5-minute flows (Get Power Statistics, Check Watchdog) now use plain
functions instead of Prefect tasks, log the happy path at DEBUG and drop the
per-run INFO relay hook, cutting ~41 Prefect DB rows per run to ~8. Failure
notifications and the watchdog staleness logic are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…actions in Backfill Place

Backfill Place held one write transaction for its whole ~15 minute run, and
each lookup scanned the entire location_unified view (~1.2 s per row). Every
morning that made Get Power Statistics and Geocode Places fail with
'database is locked'.

Lookups now use indexed timestamp-range queries on the base tables
(database/location/nearest_place.py, ~3 ms; identical results to the old
query on 598 real lookups), run on a read-only connection, and the results
are written in 500-row transactions, so the write lock is held for
milliseconds. A full run now takes about a second. Rows are only updated if
their place is still NULL.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The retroactive noise flow ran hourly although described as daily, and its
tier-1 query full-scanned the 389k-row location_overland table (4.6 s).
Ingest already flags noise in real time, so run it once a day at 04:00 local
and add idx_overland_accuracy (a plain index: the threshold is an editable
setting passed as a bound parameter, which a partial index cannot serve).
Tier-1 query: 4,564 ms -> 3 ms.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The developer profile points PREFECT_API_URL at the production server, so
tests that ran flows through the Prefect engine wrote real flow runs into it
(dozens of failed 'Backfill FX' / 'Backfill GBP' runs).

conftest.py now points Prefect at a closed local port before it is imported,
so engine use fails fast. test_get_fx_up_to_date.py and test_backfill_gbp.py
call .fn() with the run logger and task objects patched, and assert the retry
policy and failure/success hooks as configuration. No production code
changed; the full suite is about twice as fast.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…ect URL

- GET /health on the API (async, no DB, no auth; blocked on the public
  hostnames by the existing path allow-list).
- Docker healthchecks for the API, dashboard (/healthz), nginx
  (/nginx-health) and the Prefect worker (runner health endpoint via
  PREFECT_RUNNER_SERVER_ENABLE); dashboard and worker wait for a healthy API.
- ingest, worker and dashboard reach Prefect at http://prefect-server:4200/api
  over the Docker network instead of the Tailscale hostname; PREFECT_UI_URL
  keeps browser-facing links working.
- Proposed prefect-server.service with a healthcheck (docs/drafts).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Proposed replacement for scripts/check_system_health.py, kept in docs/drafts
because root's cron runs the live script straight from this path. Adds
mount, Docker-health, restart-count, per-container OOM, systemd-service,
backup-age and Prefect DB-size checks; restarts unhealthy containers with a
budget of 3 per 6 hours; replaces the permanently-tripped 'swap > 60%' alert
with swap-in rate; matches container names exactly (the old substring match
treated 'travelnet' as running whenever 'travelnet-nginx' was); restarts
prefect-server through systemd; and has a --dry-run mode.

server/tests/test_health_script.py tests the live script once it has the new
checks, otherwise this draft.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Runs the real script end to end with stub curl/docker/sudo on PATH (each test
first asserts the stubs are what the script will resolve, so nothing can
reboot). The script itself is gitignored, so the module skips when it is
absent (fresh checkout, CI).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
scripts/reap_idle_dev_sessions.py finds Claude Code sessions (matched on
argv[0]), treats a session as active if its project transcript was written
or its process tree used CPU within the idle window (default 3 h), and stops
idle ones (SIGTERM tree, SIGKILL after a grace period) with one Pushcut
notification. Never touches its own process chain, guards against PID reuse,
needs two observations before stopping, caps stops per run, and has
--dry-run. systemd user service/timer drafts in docs/drafts.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Findings, measurements, decisions and the change log from the 2026-10-07
infrastructure and performance audit of the Pi.

This public copy omits security-hardening findings, internal hostnames and
addresses, and personal details; those are kept in a private copy.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Deploying travelnet with  Cloudflare Pages  Cloudflare Pages

Latest commit: dc86a02
Status: ✅  Deploy successful!
Preview URL: https://145a8525.travelnet.pages.dev
Branch Preview URL: https://perf-pi-infrastructure.travelnet.pages.dev

View logs

docs/ is a Jekyll site (docs.travelnet.dev). The infrastructure audit and the
docs/drafts/ files are working notes meant for the repo, not pages for the
website, so exclude them from the build.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Deploying travelnet-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: dc86a02
Status:🚫  Build failed.

View logs

The first live run stopped the session it was being developed in after 3 h
without a transcript write while the user was away. The app resumes a stopped
session from its transcript, so nothing was lost, but a conversation that is
merely waiting on the user should survive longer. The tests now state their
window explicitly instead of relying on the default, and a new test pins the
6 h default.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
@danielroberts20
danielroberts20 merged commit 3fb2762 into main Oct 7, 2026
3 of 4 checks passed
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.

1 participant