docs(geocoding): post-migration log + Photon weekly-refresh operator scripts

- Decision report: status flipped to MIGRATED; added migration log with
  five WSL2 gotchas (bzip2 missing, no official Photon image,
  firewall=true blocks cross-LAN, vmIdleTimeout=-1 ineffective,
  PowerShell pre-expansion of bash $(...)) and resource snapshot.
- mana-geocoding CLAUDE.md: PHOTON_SELF_API_URL note now reflects live
  primary status on mana-gpu since 2026-04-28.
- photon-self/: operator scripts for the weekly DB refresh — update.sh
  (atomic-swap with rollback), systemd unit + timer (Sun 03:30 +30min
  jitter, Persistent=true), README with re-installation instructions
  for DR. Currently installed and enabled on mana-gpu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 21:31:08 +02:00
parent 7ebbf064ce
commit fc49198992
6 changed files with 254 additions and 5 deletions

View file

@ -159,10 +159,10 @@ GEOCODING_PROVIDERS=photon-self,pelias,photon,nominatim
PROVIDER_TIMEOUT_MS=8000 # per-provider request timeout (cold-start safe)
PROVIDER_HEALTH_CACHE_MS=30000 # health-cache TTL — skip dead providers
# --- Self-hosted Photon (privacy: 'local', primary post-migration) ----
# Set this to point at the GPU-server-hosted Photon. When unset, the
# `photon-self` slot is not registered and the chain falls back to
# public providers as before.
# --- Self-hosted Photon (privacy: 'local', PRIMARY since 2026-04-28) --
# Live on mana-gpu (Windows 11, WSL2-Ubuntu, Docker, Photon Europe-wide
# Java JAR + OpenSearch). Cross-LAN reach via WSL2 mirrored networking.
# Set in .env.macmini; flow into the container via docker-compose env.
PHOTON_SELF_API_URL=http://192.168.178.11:2322
# --- Pelias (legacy, currently stopped — privacy: 'local') ------------

View file

@ -0,0 +1,78 @@
# photon-self — operator files for the GPU-server Photon
Source-of-truth copies of the scripts and systemd units that run on
`mana-gpu` to host the self-hosted Photon. Versioned here so the setup
can be rebuilt in DR scenarios without recreating from memory.
## What lives where
| File | Where it runs | Purpose |
|---|---|---|
| `photon-update.sh` | inside the WSL2 Ubuntu distro on mana-gpu, at `/usr/local/bin/photon-update.sh` | Weekly index refresh — download new tarball, atomic swap, restart container, rollback on failure |
| `photon-update.service` | `/etc/systemd/system/photon-update.service` | Oneshot wrapper that invokes the script |
| `photon-update.timer` | `/etc/systemd/system/photon-update.timer` | Sunday 03:30 + 30-min jitter, persistent across reboots |
## Re-installation (after a clean Windows reinstall etc.)
After you've followed [docs/runbooks/photon-on-mana-gpu.md](../../../docs/runbooks/photon-on-mana-gpu.md)
to get WSL2 + Docker + the initial Photon container running:
```bash
# Run inside WSL2 Ubuntu as root:
cp /mnt/c/path/to/repo/services/mana-geocoding/photon-self/photon-update.sh \
/usr/local/bin/photon-update.sh
chmod +x /usr/local/bin/photon-update.sh
cp /mnt/c/path/to/repo/services/mana-geocoding/photon-self/photon-update.service \
/etc/systemd/system/
cp /mnt/c/path/to/repo/services/mana-geocoding/photon-self/photon-update.timer \
/etc/systemd/system/
systemctl daemon-reload
systemctl enable --now photon-update.timer
systemctl list-timers photon-update.timer # verify next run
```
## Manual trigger
To force a refresh outside the schedule:
```bash
# Inside WSL2 Ubuntu as root
systemctl start photon-update.service
journalctl -u photon-update.service -f # watch progress
tail -f /var/log/photon-update.log # script-level detail
```
## What the update script does
```
1. curl new tarball → /opt/photon-data/photon-db.tar.bz2.new
2. Verify size ≥ 25 GB (sanity guard against truncated downloads)
3. tar -xjf into /opt/photon-data/photon_data.new
4. docker stop photon
5. mv old → .old, mv new → live (atomic-ish — both renames in same FS)
6. docker start photon
7. Poll /api?q=Konstanz for up to 180 s
- On success: rm -rf .old (cleanup)
- On failure: rollback (mv live → .bad, mv .old → live, restart)
```
The rollback path is the load-bearing part — a corrupted GraphHopper
dump or a Photon version-mismatch can otherwise leave the service in a
non-serving state until the operator notices.
## Why systemd timer instead of cron
WSL2 Ubuntu has systemd enabled by default since the 0.67.6 release.
Timers give us:
- `Persistent=true` — runs missed jobs at next boot if the GPU server
was off Sunday morning. Cron just skips them.
- `RandomizedDelaySec=30min` — spreads 100s of weekly jobs across the
GraphHopper CDN window, polite-neighbour style.
- `journalctl -u photon-update` — structured logs in one place.
- Status-checkable with `systemctl list-timers`.
The downside (more files on disk than a single crontab entry) is
negligible.

View file

@ -0,0 +1,10 @@
[Unit]
Description=Weekly Photon DB refresh from GraphHopper
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/photon-update.sh
# Don't fail loudly if it can't run — next week's timer tries again
SuccessExitStatus=0

View file

@ -0,0 +1,60 @@
#!/bin/bash
# Weekly Photon DB update.
# - Downloads the latest tarball from GraphHopper
# - Verifies size before swapping (don't replace good data with a partial)
# - Atomic swap via mv → restart container
# - Keeps one previous version as .old in case of bad index
set -euo pipefail
DATA_DIR=/opt/photon-data
URL=https://download1.graphhopper.com/public/europe/photon-db-europe-1.0-latest.tar.bz2
MIN_SIZE=$((25 * 1024 * 1024 * 1024)) # 25 GB minimum, real one is ~29 GB
LOG=/var/log/photon-update.log
exec >>"$LOG" 2>&1
echo "=== $(date -Iseconds) — photon-update starting ==="
cd "$DATA_DIR"
# Download to .new — don't touch the live tarball
curl --silent --show-error --output photon-db.tar.bz2.new "$URL"
ACTUAL=$(stat -c %s photon-db.tar.bz2.new)
if [ "$ACTUAL" -lt "$MIN_SIZE" ]; then
echo "FAIL: downloaded tarball only $((ACTUAL / 1024 / 1024)) MB, expected ≥25 GB. Aborting."
rm -f photon-db.tar.bz2.new
exit 1
fi
echo "Downloaded $((ACTUAL / 1024 / 1024)) MB OK"
# Unpack to a fresh dir alongside the live one
rm -rf photon_data.new
mkdir photon_data.new
tar -xjf photon-db.tar.bz2.new -C photon_data.new --strip-components=1
# Stop the container, swap, restart
docker stop photon
mv photon_data photon_data.old || true
mv photon_data.new photon_data
mv photon-db.tar.bz2 photon-db.tar.bz2.old || true
mv photon-db.tar.bz2.new photon-db.tar.bz2
docker start photon
# Wait for it to come up + smoke
for i in $(seq 1 90); do
if curl -fsS --max-time 2 http://localhost:2322/api?q=Konstanz >/dev/null 2>&1; then
echo "OK — Photon ready after $i seconds with new index"
# Cleanup old version on success
rm -rf photon_data.old photon-db.tar.bz2.old
echo "=== $(date -Iseconds) — photon-update complete ==="
exit 0
fi
sleep 2
done
echo "FAIL — Photon did not become ready within 180 s after swap. Rolling back."
docker stop photon
mv photon_data photon_data.bad
mv photon_data.old photon_data
docker start photon
exit 1

View file

@ -0,0 +1,13 @@
[Unit]
Description=Trigger weekly Photon DB refresh
[Timer]
# Sunday 03:30 — outside likely-usage hours
OnCalendar=Sun *-*-* 03:30:00
# Run on next boot if the system was off at the scheduled time
Persistent=true
# Spread across 30 min so we don't hammer GraphHopper at exactly :30:00
RandomizedDelaySec=30min
[Install]
WantedBy=timers.target