fix: satellite-download chunk-basiert (max 500 Futures im RAM), restart: on-failure

This commit is contained in:
Conrad Schulz 2026-06-13 17:52:55 +00:00
parent 62494438ef
commit aaf499352b
5 changed files with 43 additions and 33 deletions

View file

@ -32,7 +32,7 @@ services:
- SAT_THREADS=16 - SAT_THREADS=16
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
command: ["python3", "-u", "/scripts/download-satellite.py", "/data/satellite.mbtiles"] command: ["python3", "-u", "/scripts/download-satellite.py", "/data/satellite.mbtiles"]
restart: "no" restart: on-failure
deploy: deploy:
resources: resources:
limits: limits:

View file

@ -100,8 +100,8 @@ Sources-Cache unter `./build/sources/` bei Rebuild nicht löschen (spart Re-
Der Download läuft in einem eigenen Container vollständig unabhängig von Der Download läuft in einem eigenen Container vollständig unabhängig von
SSH-Sessions und VS Code. Resume-fähig: bei Unterbrechung einfach neu starten. SSH-Sessions und VS Code. Resume-fähig: bei Unterbrechung einfach neu starten.
**Ressourcen:** 16 Threads, 1 GB RAM-Limit (IO-bound), 4 CPUs. **Ressourcen:** 16 Threads, 1 GB RAM-Limit (effektiv ~40 MB, chunk-basiertes Processing), 4 CPUs.
**Robustheit:** Download schreibt in `satellite.mbtiles.part` → Martin ignoriert diese Datei während des Downloads. Nach Abschluss automatische Umbenennung zu `satellite.mbtiles`. **Robustheit:** Download schreibt in `satellite.mbtiles.part` → Martin ignoriert diese Datei während des Downloads. Nach Abschluss automatische Umbenennung zu `satellite.mbtiles`. Bei OOM startet der Container automatisch neu (`restart: on-failure`).
```bash ```bash
cd /mnt/services-data/rd13_tile_server cd /mnt/services-data/rd13_tile_server

View file

@ -92,14 +92,21 @@ Fix 3: YAML-Duplikat `reservations` in docker-compose.download.yml bereinigt.
Martin gestoppt bis Download fertig. Martin gestoppt bis Download fertig.
### Prompt 15 ### Prompt 16
Sicher, es macht nichts wenn wir ihn einfach auslassen bis wir fertig sind. Was machen wir wenn wieder das mem limit vom download gerissen wird?
### Antwort 15 ### Antwort 16
Martin bleibt gestoppt. Download läuft mit .part-Datei, 1GB Limit, 16 Threads. Eigentlicher Bug identifiziert: `futs = [submit_tile(...) for ...]` legte trotz Semaphore alle 5,5 Mio Future-Objekte gleichzeitig in eine Python-Liste → ~500 MB allein für die Liste.
Status:
- `satellite.mbtiles.part` wächst (neuer aktiver Download) Fix: Chunk-basiertes Processing mit Generator:
- `satellite.mbtiles` (3.5 GB, alter Resume-Stand) bleibt als Backup - `tile_coords()` Generator liefert (z,x,y) lazy ohne alles im RAM
- Jeweils 500 Coords zu einem Chunk zusammenfassen
- Nur 500 Futures gleichzeitig im Speicher statt 5,5 Mio
- Ergebnis: ~37 MB RAM statt 500+ MB
Zusätzlich: `restart: on-failure` in docker-compose.download.yml bei OOM automatischer Neustart mit Resume.
Verifiziert: `CPU: 10% RAM: 36.92MiB / 1GiB`, Fortschritts-Logs erscheinen korrekt.
### Antwort 10 ### Antwort 10

View file

@ -1,6 +1,6 @@
# PROJECT_CONTEXT rd13_tile_server # PROJECT_CONTEXT rd13_tile_server
**Letzte Aktualisierung:** 2026-06-13 Martin gestoppt (wartend). Satellit-Download läuft in `.part`-Datei (3.5 GB bereits heruntergeladen, resume aktiv). Fixes: .part-Pattern, OOM-Limit 1GB, YAML-Duplikat. **Letzte Aktualisierung:** 2026-06-13 Satellit-Download läuft stabil: chunk-basiert (max 500 Futures, ~37MB RAM), restart:on-failure, .part-Datei. Martin gestoppt bis Fertigstellung.
--- ---

View file

@ -80,6 +80,14 @@ def log(msg):
print(msg, flush=True) print(msg, flush=True)
def tile_coords():
"""Generator: liefert alle (z,x,y)-Koordinaten ohne sie alle im RAM zu halten."""
for z in range(MAX_ZOOM + 1):
for x in range(1 << z):
for y in range(1 << z):
yield z, x, y
def main(): def main():
total = sum(4 ** z for z in range(MAX_ZOOM + 1)) total = sum(4 ** z for z in range(MAX_ZOOM + 1))
log("[Sat] Output: %s" % OUTPUT) log("[Sat] Output: %s" % OUTPUT)
@ -89,31 +97,26 @@ def main():
conn = init_db(OUTPUT) conn = init_db(OUTPUT)
t0 = time.time() t0 = time.time()
n = 0 n = 0
# Bounded queue: max THREADS*4 Futures gleichzeitig im Speicher CHUNK = 500 # max Futures gleichzeitig im Speicher (nicht 5.5 Mio)
# verhindert OOM bei 5.5 Mio Tasks
sem = threading.Semaphore(THREADS * 4)
def submit_tile(ex, z, x, y): coords = tile_coords()
sem.acquire()
fut = ex.submit(do_tile, conn, z, x, y)
fut.add_done_callback(lambda _: sem.release())
return fut
with ThreadPoolExecutor(max_workers=THREADS) as ex: with ThreadPoolExecutor(max_workers=THREADS) as ex:
futs = [] while True:
for z in range(MAX_ZOOM + 1): chunk = [(z, x, y) for z, x, y in (next(coords, None) for _ in range(CHUNK)) if (z, x, y) != (None, None, None)]
for x in range(1 << z): if not chunk:
for y in range(1 << z): break
futs.append(submit_tile(ex, z, x, y)) futs = {ex.submit(do_tile, conn, z, x, y): 1 for z, x, y in chunk}
for n, f in enumerate(as_completed(futs), 1): for f in as_completed(futs):
f.result() f.result()
if n % 1000 == 0 or n == total: n += 1
dt = time.time() - t0 or 0.001 if n % 1000 == 0 or n == total:
eta = (total - n) / n * dt / 3600 dt = time.time() - t0 or 0.001
log("[Sat] %5.1f%% %d/%d ok=%d skip=%d err=%d %.0ft/s ETA=%.1fh" % ( eta = (total - n) / n * dt / 3600
n / total * 100, n, total, log("[Sat] %5.1f%% %d/%d ok=%d skip=%d err=%d %.0ft/s ETA=%.1fh" % (
stats["ok"], stats["skip"], stats["err"], n / total * 100, n, total,
n / dt, eta)) stats["ok"], stats["skip"], stats["err"],
n / dt, eta))
with _lock: with _lock:
if pending: if pending:
conn.executemany("INSERT OR REPLACE INTO tiles VALUES(?,?,?,?)", pending) conn.executemany("INSERT OR REPLACE INTO tiles VALUES(?,?,?,?)", pending)