diff --git a/docker-compose.download.yml b/docker-compose.download.yml index e9f2540..66dd8c2 100644 --- a/docker-compose.download.yml +++ b/docker-compose.download.yml @@ -32,7 +32,7 @@ services: - SAT_THREADS=16 - PYTHONUNBUFFERED=1 command: ["python3", "-u", "/scripts/download-satellite.py", "/data/satellite.mbtiles"] - restart: "no" + restart: on-failure deploy: resources: limits: diff --git a/docs/ADMIN.md b/docs/ADMIN.md index 6691995..8c8c355 100644 --- a/docs/ADMIN.md +++ b/docs/ADMIN.md @@ -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 SSH-Sessions und VS Code. Resume-fähig: bei Unterbrechung einfach neu starten. -**Ressourcen:** 16 Threads, 1 GB RAM-Limit (IO-bound), 4 CPUs. -**Robustheit:** Download schreibt in `satellite.mbtiles.part` → Martin ignoriert diese Datei während des Downloads. Nach Abschluss automatische Umbenennung zu `satellite.mbtiles`. +**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`. Bei OOM startet der Container automatisch neu (`restart: on-failure`). ```bash cd /mnt/services-data/rd13_tile_server diff --git a/docs/history/prompts/2026-06-13_tile-raster-architecture_session.md b/docs/history/prompts/2026-06-13_tile-raster-architecture_session.md index 9e31267..df8c20e 100644 --- a/docs/history/prompts/2026-06-13_tile-raster-architecture_session.md +++ b/docs/history/prompts/2026-06-13_tile-raster-architecture_session.md @@ -92,14 +92,21 @@ Fix 3: YAML-Duplikat `reservations` in docker-compose.download.yml bereinigt. Martin gestoppt bis Download fertig. -### Prompt 15 -Sicher, es macht nichts wenn wir ihn einfach auslassen bis wir fertig sind. +### Prompt 16 +Was machen wir wenn wieder das mem limit vom download gerissen wird? -### Antwort 15 -Martin bleibt gestoppt. Download läuft mit .part-Datei, 1GB Limit, 16 Threads. -Status: -- `satellite.mbtiles.part` wächst (neuer aktiver Download) -- `satellite.mbtiles` (3.5 GB, alter Resume-Stand) bleibt als Backup +### Antwort 16 +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. + +Fix: Chunk-basiertes Processing mit Generator: +- `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 diff --git a/docs/history/summary/PROJECT_CONTEXT.md b/docs/history/summary/PROJECT_CONTEXT.md index a5a141f..9e6aa06 100644 --- a/docs/history/summary/PROJECT_CONTEXT.md +++ b/docs/history/summary/PROJECT_CONTEXT.md @@ -1,6 +1,6 @@ # 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. --- diff --git a/scripts/download-satellite.py b/scripts/download-satellite.py index 7112aaf..2c45bc5 100755 --- a/scripts/download-satellite.py +++ b/scripts/download-satellite.py @@ -80,6 +80,14 @@ def log(msg): 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(): total = sum(4 ** z for z in range(MAX_ZOOM + 1)) log("[Sat] Output: %s" % OUTPUT) @@ -89,31 +97,26 @@ def main(): conn = init_db(OUTPUT) t0 = time.time() n = 0 - # Bounded queue: max THREADS*4 Futures gleichzeitig im Speicher - # verhindert OOM bei 5.5 Mio Tasks - sem = threading.Semaphore(THREADS * 4) + CHUNK = 500 # max Futures gleichzeitig im Speicher (nicht 5.5 Mio) - def submit_tile(ex, z, x, y): - sem.acquire() - fut = ex.submit(do_tile, conn, z, x, y) - fut.add_done_callback(lambda _: sem.release()) - return fut + coords = tile_coords() with ThreadPoolExecutor(max_workers=THREADS) as ex: - futs = [] - for z in range(MAX_ZOOM + 1): - for x in range(1 << z): - for y in range(1 << z): - futs.append(submit_tile(ex, z, x, y)) - for n, f in enumerate(as_completed(futs), 1): - f.result() - if n % 1000 == 0 or n == total: - dt = time.time() - t0 or 0.001 - eta = (total - n) / n * dt / 3600 - log("[Sat] %5.1f%% %d/%d ok=%d skip=%d err=%d %.0ft/s ETA=%.1fh" % ( - n / total * 100, n, total, - stats["ok"], stats["skip"], stats["err"], - n / dt, eta)) + while True: + chunk = [(z, x, y) for z, x, y in (next(coords, None) for _ in range(CHUNK)) if (z, x, y) != (None, None, None)] + if not chunk: + break + futs = {ex.submit(do_tile, conn, z, x, y): 1 for z, x, y in chunk} + for f in as_completed(futs): + f.result() + n += 1 + if n % 1000 == 0 or n == total: + dt = time.time() - t0 or 0.001 + eta = (total - n) / n * dt / 3600 + log("[Sat] %5.1f%% %d/%d ok=%d skip=%d err=%d %.0ft/s ETA=%.1fh" % ( + n / total * 100, n, total, + stats["ok"], stats["skip"], stats["err"], + n / dt, eta)) with _lock: if pending: conn.executemany("INSERT OR REPLACE INTO tiles VALUES(?,?,?,?)", pending)