fix(deploy): the sidecars' downloads try again within the hour after a failed round

A failed BRouter segment or listing, a failed tile cut or an unwritable folder waited the whole refresh period, 30 days without routes or a map; they try again after an hour, doubling to a day. Photon's failed download backs off from a day to a week, and a dead Photon is noticed within a minute. The downloads' network is an ordinary bridge the three share, as the comments now say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-30 12:55:42 +01:00
commit ba77248930
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
8 changed files with 102 additions and 19 deletions

View file

@ -3,7 +3,9 @@
# every 5°×5° segment of BRouter's project whose south-west corner lies in # every 5°×5° segment of BRouter's project whose south-west corner lies in
# BROUTER_REGION (`west,south,east,north` in whole degrees, multiples of 5; Europe's # BROUTER_REGION (`west,south,east,north` in whole degrees, multiples of 5; Europe's
# `-25,35,40,70` by default), downloaded where the project has a newer one than the # `-25,35,40,70` by default), downloaded where the project has a newer one than the
# volume holds, then again every BROUTER_REFRESH_DAYS days (30). Each is written beside # volume holds, then again every BROUTER_REFRESH_DAYS days (30); after a round where the
# listing or a segment didn't come, again in an hour, the wait doubling up to a day
# while rounds keep failing. Each is written beside
# its old self and moved over it, so BRouter never reads half a file. Segments of # its old self and moved over it, so BRouter never reads half a file. Segments of
# the sea, which the project doesn't have, are skipped. # the sea, which the project doesn't have, are skipped.
set -eu set -eu
@ -25,8 +27,9 @@ name() {
} }
refresh() { refresh() {
complete=0
listing=$(curl -fsS "$base/") || { echo "The segments' listing didn't come." >&2; return 0; } listing=$(curl -fsS "$base/") || { echo "The segments' listing didn't come." >&2; return 0; }
fetched=0 kept=0 fetched=0 kept=0 failed=0
lon=$west lon=$west
while [ "$lon" -le "$east" ]; do while [ "$lon" -le "$east" ]; do
lat=$south lat=$south
@ -36,8 +39,11 @@ refresh() {
# Only where the project's is newer than ours (If-Modified-Since): a # Only where the project's is newer than ours (If-Modified-Since): a
# current one comes back empty. # current one comes back empty.
if [ -e "$file" ]; then set -- -z "$file"; else set --; fi if [ -e "$file" ]; then set -- -z "$file"; else set --; fi
if curl -fsS -R --retry 3 "$@" -o "$file.part" "$base/$file" && if ! curl -fsS -R --retry 3 "$@" -o "$file.part" "$base/$file"; then
[ -s "$file.part" ]; then rm -f "$file.part"
echo "Segment $file didn't come." >&2
failed=$((failed + 1))
elif [ -s "$file.part" ]; then
mv "$file.part" "$file" mv "$file.part" "$file"
fetched=$((fetched + 1)) fetched=$((fetched + 1))
else else
@ -49,10 +55,30 @@ refresh() {
done done
lon=$((lon + 5)) lon=$((lon + 5))
done done
echo "Segments: $fetched fetched, $kept current, $(du -sh . | cut -f1) in all." echo "Segments: $fetched fetched, $kept current, $failed failed, $(du -sh . | cut -f1) in all."
if [ "$failed" -eq 0 ]; then complete=1; fi
} }
# Seconds to wait after the $1th incomplete round in a row: $2, doubling up to $3.
backoff() {
pause=$2 n=1
while [ "$n" -lt "$1" ] && [ "$pause" -lt "$3" ]; do
pause=$((pause * 2)) n=$((n + 1))
done
if [ "$pause" -gt "$3" ]; then pause=$3; fi
echo "$pause"
}
failures=0
while :; do while :; do
refresh refresh
sleep $((days * 86400)) if [ "$complete" -eq 1 ]; then
failures=0
sleep $((days * 86400))
else
failures=$((failures + 1))
pause=$(backoff "$failures" 3600 86400)
echo "The round wasn't complete; trying again in $((pause / 60)) minutes." >&2
sleep "$pause"
fi
done done

View file

@ -42,7 +42,8 @@ services:
MAP_REGION: ${MAP_REGION:--25,35,45,75} MAP_REGION: ${MAP_REGION:--25,35,45,75}
volumes: volumes:
- ./maps/tiles:/tiles - ./maps/tiles:/tiles
# Out to Protomaps' builds alone; on a network of its own, beside nothing. # Out to Protomaps' builds on `downloads`, an ordinary bridge with a way out to
# anywhere, shared with BRouter's segments and the search's downloads.
networks: networks:
- downloads - downloads
mem_limit: 512m mem_limit: 512m

View file

@ -48,7 +48,8 @@ services:
PHOTON_REGION: ${PHOTON_REGION:-europe} PHOTON_REGION: ${PHOTON_REGION:-europe}
volumes: volumes:
- ./photon/data:/data - ./photon/data:/data
# Out to GraphHopper's downloads alone; on a network of its own, beside nothing. # Out to GraphHopper's downloads on `downloads`, an ordinary bridge with a way out
# to anywhere, shared with BRouter's segments and the map's downloads.
networks: networks:
- downloads - downloads
mem_limit: 256m mem_limit: 256m

View file

@ -88,7 +88,8 @@ services:
BROUTER_REGION: ${BROUTER_REGION:--25,35,40,70} BROUTER_REGION: ${BROUTER_REGION:--25,35,40,70}
volumes: volumes:
- ./brouter/segments:/segments4 - ./brouter/segments:/segments4
# Out to brouter.de alone; on a network of its own, beside nothing. # Out to brouter.de on `downloads`, an ordinary bridge with a way out to anywhere,
# shared with the map's and the search's downloads where their compose files are on.
networks: networks:
- downloads - downloads
mem_limit: 64m mem_limit: 64m

View file

@ -3,7 +3,9 @@
# newest of Protomaps' daily builds of the planet, cut to MAP_REGION (`west,south, # newest of Protomaps' daily builds of the planet, cut to MAP_REGION (`west,south,
# east,north` in degrees; by default Europe's, the box BRouter's segments cover) at # east,north` in degrees; by default Europe's, the box BRouter's segments cover) at
# zooms 0 to 15, into /tiles/europe.pmtiles; again every MAP_REFRESH_DAYS days (30) # zooms 0 to 15, into /tiles/europe.pmtiles; again every MAP_REFRESH_DAYS days (30)
# where a newer build has come. The cut is written into /tiles/next/ and moved over # where a newer build has come; after a round where the builds' list or the cut
# failed, again in an hour, the wait doubling up to a day while rounds keep failing.
# The cut is written into /tiles/next/ and moved over
# the old one, so the server never reads half a file; until it is done, the old one # the old one, so the server never reads half a file; until it is done, the old one
# serves. Both at once take twice a cut's room. # serves. Both at once take twice a cut's room.
set -eu set -eu
@ -12,9 +14,13 @@ builds=https://build-metadata.protomaps.dev/builds.json
region=${MAP_REGION:--25,35,45,75} region=${MAP_REGION:--25,35,45,75}
days=${MAP_REFRESH_DAYS:-30} days=${MAP_REFRESH_DAYS:-30}
cd /tiles cd /tiles
mkdir -p next
refresh() { refresh() {
complete=0
if ! mkdir -p next; then
echo "/tiles/next can't be made; the tiles stay as they are." >&2
return 0
fi
build=$(curl -fsS "$builds" | grep -o '"key": *"[0-9]\{8\}\.pmtiles"' | build=$(curl -fsS "$builds" | grep -o '"key": *"[0-9]\{8\}\.pmtiles"' |
grep -o '[0-9]\{8\}\.pmtiles' | sort | tail -n 1) || build= grep -o '[0-9]\{8\}\.pmtiles' | sort | tail -n 1) || build=
if [ -z "$build" ]; then if [ -z "$build" ]; then
@ -23,6 +29,7 @@ refresh() {
fi fi
if [ -e europe.build ] && [ "$(cat europe.build)" = "$build" ]; then if [ -e europe.build ] && [ "$(cat europe.build)" = "$build" ]; then
echo "The tiles are $build's, the newest." echo "The tiles are $build's, the newest."
complete=1
return 0 return 0
fi fi
rm -f next/europe.pmtiles rm -f next/europe.pmtiles
@ -31,13 +38,33 @@ refresh() {
mv next/europe.pmtiles europe.pmtiles mv next/europe.pmtiles europe.pmtiles
echo "$build" >europe.build echo "$build" >europe.build
echo "The tiles are $build's now, $(du -h europe.pmtiles | cut -f1)." echo "The tiles are $build's now, $(du -h europe.pmtiles | cut -f1)."
complete=1
else else
rm -f next/europe.pmtiles rm -f next/europe.pmtiles
echo "The cut of $build failed; the tiles stay as they are." >&2 echo "The cut of $build failed; the tiles stay as they are." >&2
fi fi
} }
# Seconds to wait after the $1th incomplete round in a row: $2, doubling up to $3.
backoff() {
pause=$2 n=1
while [ "$n" -lt "$1" ] && [ "$pause" -lt "$3" ]; do
pause=$((pause * 2)) n=$((n + 1))
done
if [ "$pause" -gt "$3" ]; then pause=$3; fi
echo "$pause"
}
failures=0
while :; do while :; do
refresh refresh
sleep $((days * 86400)) if [ "$complete" -eq 1 ]; then
failures=0
sleep $((days * 86400))
else
failures=$((failures + 1))
pause=$(backoff "$failures" 3600 86400)
echo "The round wasn't complete; trying again in $((pause / 60)) minutes." >&2
sleep "$pause"
fi
done done

View file

@ -5,7 +5,9 @@
# where none is there or the one there is older than PHOTON_REFRESH_DAYS days (30), # where none is there or the one there is older than PHOTON_REFRESH_DAYS days (30),
# unpacked as it comes into /data/next and checked against its MD5 on the way, then # unpacked as it comes into /data/next and checked against its MD5 on the way, then
# marked ready for serve.sh, which puts it in place. Its archive is never kept: only # marked ready for serve.sh, which puts it in place. Its archive is never kept: only
# the old database and the new one take room at once. # the old database and the new one take room at once. Looked at daily; after a failed
# download, again a day later, the wait doubling up to a week while downloads keep
# failing.
set -eu set -eu
base=https://download1.graphhopper.com/public base=https://download1.graphhopper.com/public
@ -21,7 +23,9 @@ fresh() {
} }
fetch() { fetch() {
failed=1
if [ -e next/ready ] || fresh; then if [ -e next/ready ] || fresh; then
failed=0
return 0 return 0
fi fi
rm -rf next rm -rf next
@ -41,6 +45,7 @@ fetch() {
: >next/fetched : >next/fetched
: >next/ready : >next/ready
echo "Photon's database of $region is ready, $(du -sh next/photon_data | cut -f1)." echo "Photon's database of $region is ready, $(du -sh next/photon_data | cut -f1)."
failed=0
else else
wait "$sum" 2>/dev/null || true wait "$sum" 2>/dev/null || true
rm -rf next rm -rf next
@ -48,7 +53,26 @@ fetch() {
fi fi
} }
# Seconds to wait after the $1th failed download in a row: $2, doubling up to $3.
backoff() {
pause=$2 n=1
while [ "$n" -lt "$1" ] && [ "$pause" -lt "$3" ]; do
pause=$((pause * 2)) n=$((n + 1))
done
if [ "$pause" -gt "$3" ]; then pause=$3; fi
echo "$pause"
}
failures=0
while :; do while :; do
fetch fetch
sleep 86400 if [ "$failed" -eq 0 ]; then
failures=0
sleep 86400
else
failures=$((failures + 1))
pause=$(backoff "$failures" 86400 604800)
echo "Trying again in $((pause / 3600)) hours." >&2
sleep "$pause"
fi
done done

View file

@ -2,7 +2,9 @@
# Serves Photon on the database in /data/current (Halkyon Train's ADR 0030, D50), once # Serves Photon on the database in /data/current (Halkyon Train's ADR 0030, D50), once
# data.sh has brought one; and where data.sh has made a newer one (/data/next/ready), # data.sh has brought one; and where data.sh has made a newer one (/data/next/ready),
# stops Photon, puts the new one in the old one's place and starts it again: a # stops Photon, puts the new one in the old one's place and starts it again: a
# minute or two without search, once a month. PHOTON_HEAP is Java's heap (6g). # minute or two without search, once a month. Looked at every minute, which also
# starts a Photon that stopped (killed for memory) again. PHOTON_HEAP is Java's
# heap (6g).
set -eu set -eu
cd /data cd /data
@ -42,7 +44,7 @@ while [ ! -d current/photon_data ] && ! swap; do
done done
start start
while :; do while :; do
sleep 3600 & sleep 60 &
wait $! || true wait $! || true
if [ -e next/ready ]; then if [ -e next/ready ]; then
stop stop

View file

@ -223,7 +223,8 @@ Steps, in order; ✱ marks a step as root.
Learn's server are recreated with it (Train's site, and the register's member); the Learn's server are recreated with it (Train's site, and the register's member); the
instance is away for seconds. Train's server applies its migrations as it starts. instance is away for seconds. Train's server applies its migrations as it starts.
The segments come in the first hour or so (`docker compose logs brouter-segments` The segments come in the first hour or so (`docker compose logs brouter-segments`
says when); until then BRouter finds no route. says when); until then BRouter finds no route. Where brouter.de doesn't answer or
the folder isn't BRouter's, the log says so and it tries again an hour later.
7. **Check it**: `https://train.halkyon.eu/` answers; in Train, signed in with a Learn 7. **Check it**: `https://train.halkyon.eu/` answers; in Train, signed in with a Learn
account, a route plans between two points of the region. account, a route plans between two points of the region.
@ -266,7 +267,7 @@ Steps, in order; ✱ marks a step as root.
4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Caddy and 4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Caddy and
Train's server are recreated with it. The first cut takes hours Train's server are recreated with it. The first cut takes hours
(`docker compose logs -f map-tiles-refresh` says when it is done); until then Train's (`docker compose logs -f map-tiles-refresh` says when it is done); until then Train's
maps stay empty. maps stay empty. A failed cut is tried again an hour later.
5. **Check it**: in Train, signed in, a workout's map is drawn and names the instance as 5. **Check it**: in Train, signed in, a workout's map is drawn and names the instance as
its source; `curl -I https://train.halkyon.eu/tiles/europe/0/0/0.mvt` is refused its source; `curl -I https://train.halkyon.eu/tiles/europe/0/0/0.mvt` is refused
with 401. Not yet tried on the server (29 Oct 2026): whether `pmtiles serve` reads a with 401. Not yet tried on the server (29 Oct 2026): whether `pmtiles serve` reads a
@ -309,7 +310,7 @@ Steps, in order; ✱ marks a step as root.
4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Train's 4. **Start it** (the operator, as `deploy`): `docker compose up -d --build`. Train's
server is recreated with it. The first database takes an hour or more server is recreated with it. The first database takes an hour or more
(`docker compose logs -f photon-data` says when it is ready); Photon starts on it (`docker compose logs -f photon-data` says when it is ready); Photon starts on it
within the next hour, and until then Train's planner offers no search. within a minute, and until then Train's planner offers no search.
5. **Check it**: in Train, signed in, the planner's search finds a town of the region. 5. **Check it**: in Train, signed in, the planner's search finds a town of the region.
Not yet tried on the server (29 Oct 2026): the database's size unpacked, Photon's Not yet tried on the server (29 Oct 2026): the database's size unpacked, Photon's
memory on Europe's, and that 1.3.0 reads GraphHopper's `1.0` build. memory on Europe's, and that 1.3.0 reads GraphHopper's `1.0` build.