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:
1 parent
327d6a69ae
commit
83bc36aa79
5 files changed
+90
-4
No files matched your search
+39
-4
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user