Add a Postgres advisory-lock leader election for multi-host deploys (#229)

The subscription notifier elects one leader via a host-local flock
(THERMOGRAPH_SINGLETON_LOCK) so multiple uvicorn workers on one host don't
each run it. Under multi-host Swarm that guard is insufficient: each host
would independently elect its own leader, multiplying Open-Meteo quota use
N-fold again.

Add claim_pg(key) alongside the existing claim(lock_path): a cluster-wide
Postgres advisory lock, visible to every host talking to the same database.
The holding connection is dedicated and kept for the process lifetime
(advisory locks are session-scoped); a dead connection is dropped and
re-election retried on the next call.

claim_leader() dispatches between the two mechanisms from env:
THERMOGRAPH_SINGLETON_PG (+ Postgres) -> claim_pg; else
THERMOGRAPH_SINGLETON_LOCK -> claim; else always leader, unchanged. Wired in
at web/app.py in place of the direct claim() call. Off by default, so
today's single-host behavior is unaffected.
This commit is contained in:
Emi Griffith 2026-07-20 17:07:52 -07:00 committed by GitHub
parent 1f7e8552cf
commit 213b3818e7

View file

@ -49,6 +49,14 @@ THERMOGRAPH_COOKIE_SECURE=1
# not once per worker. ~200 MB RAM per worker. # not once per worker. ~200 MB RAM per worker.
WORKERS=4 WORKERS=4
# THERMOGRAPH_SINGLETON_LOCK arbitrates workers on ONE host. Under multi-host Swarm,
# each host would independently elect its own leader — multiplying the Open-Meteo
# quota use N-fold again. Set THERMOGRAPH_SINGLETON_PG=1 (with THERMOGRAPH_DATABASE_URL
# pointing at Postgres) to switch to a cluster-wide Postgres advisory lock instead, so
# exactly one host — not one per host — runs the notifier. Leave unset on a single-host
# deploy (today's default); the flock above is sufficient there.
#THERMOGRAPH_SINGLETON_PG=1
# Base path the app is served under. # Base path the app is served under.
# / -> app at the domain root (Thermograph owns the whole domain — # / -> app at the domain root (Thermograph owns the whole domain —
# this is what the Caddyfile expects: thermograph.org proxies "/") # this is what the Caddyfile expects: thermograph.org proxies "/")