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
- PYTHONUNBUFFERED=1
command: ["python3", "-u", "/scripts/download-satellite.py", "/data/satellite.mbtiles"]
restart: "no"
restart: on-failure
deploy:
resources:
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
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

View file

@ -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

View file

@ -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.
---

View file

@ -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)