Schedule poll/notify and the digest as systemd timers

news-cycle.timer runs poll then notify every 15 minutes; news-digest.timer runs
the briefing at 08:45 daily. Both enabled and verified: a manual cycle fetched
1047 items, embedded 95 new ones and clustered them in 74s.

Units are installed as COPIES rather than symlinks. The previous service's units
were symlinked into docs/projects/news/deploy/, and when that repo was renamed
to news_triage they became dangling and the timer failed every ten minutes,
unnoticed, until this rebuild found it. Copying costs a re-copy after editing;
symlinking cost days of silent failure.

Deliberate details: the poll ExecStart is `-` prefixed so a failed fetch does not
skip notify; RandomizedDelaySec spreads polling off the quarter hour rather than
hitting 56 sources at once; the digest is Persistent so a machine that was off
at 08:45 still sends rather than skipping the day.
This commit is contained in:
connor committed 2026-09-08 17:36:03 -04:00
1 parent 327d6a69ae
commit 83bc36aa79
5 files changed
+90 -4

No files matched your search

+39 -4
View File
@@ -127,12 +127,47 @@ sudo systemctl disable --now news-update.timer
| `POST /api/feedback/.../bogus` | `{"error":"bad signature"}` | reaches the app, unauthenticated, and rejects a forgery | | `POST /api/feedback/.../bogus` | `{"error":"bad signature"}` | reaches the app, unauthenticated, and rejects a forgery |
| `POST /api/feedback/.../<valid HMAC>` | `{"ok":true}` and a row in `feedback` | **the loop that never once worked in v0.2** | | `POST /api/feedback/.../<valid HMAC>` | `{"ok":true}` and a row in `feedback` | **the loop that never once worked in v0.2** |
## 3. Scheduling ## 3. Scheduling — APPLIED 2026-09-08
Two systemd timers, units in this directory:
| unit | cadence | does |
|---|---|---|
| `news-cycle.timer` | every 15 min | `poll` then `notify` |
| `news-digest.timer` | daily 08:45 | `digest` |
```sh
sudo cp deploy/news-*.service deploy/news-*.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now news-cycle.timer news-digest.timer
``` ```
*/15 * * * * podman exec news-backend python -m news.cli poll
*/20 * * * * podman exec news-backend python -m news.cli notify **Copy the units, do not symlink them.** The previous service's units were
45 8 * * * podman exec news-backend python -m news.cli digest symlinks into `docs/projects/news/deploy/`, and when that repo was renamed to
`news_triage` they became dangling and the timer failed every ten minutes,
unnoticed, for days. The cost of copying is remembering to re-copy after editing
a unit here; the cost of symlinking is a silent failure the next time a
directory moves.
Design notes:
- `news-cycle.service` has two `ExecStart` lines and the first is prefixed `-`,
so a failed fetch does not skip `notify` -- there may still be ripe clusters
from an earlier cycle worth sending.
- `RandomizedDelaySec=90` on the cycle: feeds get polled by everyone on the
quarter hour, and this both spreads that load and avoids hitting all 56
sources at the same instant.
- `Persistent=true` on the digest, so a machine that was off at 08:45 still
sends the briefing on the next boot rather than silently skipping a day.
- `TimeoutStartSec=20min` on the cycle: a cold start embeds the whole backlog at
~11 items/sec. Steady state is far quicker -- a real incremental cycle
measured 74s for 95 new items.
Reading the logs is noisier than it should be, because podman writes its own
container-exec events into the same unit journal:
```sh
journalctl -u news-cycle -o cat | grep -vE '^[0-9]{4}-[0-9]{2}-[0-9]{2} .* container '
``` ```
## 4. Verifying it actually works ## 4. Verifying it actually works
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=news: fetch, embed, cluster, then send any alerts
Documentation=file:///home/connor/docs/projects/news/deploy/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
# `-` on the poll so a failed fetch does not skip notify: there may still be
# ripe clusters from an earlier cycle worth sending.
ExecStart=-/usr/bin/podman exec connor_news-backend_1 python -m news.cli poll
ExecStart=/usr/bin/podman exec connor_news-backend_1 python -m news.cli notify
# A cold cycle embeds the whole backlog at ~11 items/sec, so it can legitimately
# run for minutes. Steady state is far quicker; this is a ceiling, not a target.
TimeoutStartSec=20min
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=news: run a poll/notify cycle every 15 minutes
[Timer]
OnBootSec=3min
OnUnitActiveSec=15min
# Feeds are polled by many people on the quarter hour; spread the load and stay
# a good citizen. Also avoids every source being hit at the same instant.
RandomizedDelaySec=90
AccuracySec=30s
[Install]
WantedBy=timers.target
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=news: write the daily briefing and push a pointer to it
Documentation=file:///home/connor/docs/projects/news/deploy/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/podman exec connor_news-backend_1 python -m news.cli digest
# One LLM call, which can take ~20s; allow for a slow upstream.
TimeoutStartSec=5min
+12
View File
@@ -0,0 +1,12 @@
[Unit]
Description=news: daily briefing at 08:45 local
[Timer]
OnCalendar=*-*-* 08:45:00
# If the machine was asleep or down at 08:45, still send it on the next boot
# rather than silently skipping the day.
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.target