diff --git a/docs/MAPS.md b/docs/MAPS.md index 0a538aa..16051f8 100644 --- a/docs/MAPS.md +++ b/docs/MAPS.md @@ -218,7 +218,11 @@ environment: - **`LIDAR_TILE_CACHE_ONLY=1`** — une tuile absente ou périmée est servie transparente avec `X-Tile-Pending: 1` et `Cache-Control: no-store` (le navigateur la redemande : dès que la maintenance l'a rendue, elle apparaît). - Plus aucun rendu, ni rapatriement amont, sur le chemin des requêtes. + Plus aucun rendu local sur le chemin des requêtes ; avec `LIDAR_MAPS_URL`, + une tuile manquante (ex. zoom au-delà de `LIDAR_TILE_BACKGROUND_MAX_Z`) + y est rapatriée — un téléchargement de quelques dizaines de Ko, mis en + cache — puis servie : la navigation couvre tous les zooms sans jamais + calculer sur la petite machine. - **`LIDAR_TILE_BACKGROUND=1`** — un sondeur rescane les dalles à intervalle régulier (`LIDAR_TILE_BACKGROUND_INTERVAL`, 120 s) ; chaque dalle nouvelle ou régénérée (le worker vient de produire, le cache à la demande vient de diff --git a/lidar_pipeline/mapserve.py b/lidar_pipeline/mapserve.py index 83fa791..b8b28c9 100644 --- a/lidar_pipeline/mapserve.py +++ b/lidar_pipeline/mapserve.py @@ -571,11 +571,30 @@ async def tile(layer: str, z: int, x: int, name: str, v: Optional[str] = None): raise HTTPException(404, "tuile hors grille") if _env_flag("LIDAR_TILE_CACHE_ONLY"): - # Mode cache seule : aucune navigation ne déclenche de rendu (ni amont). + # Mode cache seule : aucune navigation ne déclenche de RENDU local. + # Une tuile absente (ex. zoom > LIDAR_TILE_BACKGROUND_MAX_Z, jamais + # couverte par la maintenance) est rapatriée du serveur amont + # LIDAR_MAPS_URL — un simple téléchargement de quelques dizaines de + # Ko, mis en cache, sans décodage ni calcul sur cette machine. data, state = await run_in_threadpool( tiles_mod.cached_tile, OUTPUT_DIR, layer, z, x, y, scale, fmt) pending = state == "pending" empty = state == "empty" + if pending and MAPS_URL: + suffix = "@2x" if scale == 2 else "" + key = (layer, z, x, y, scale, fmt) + entry = _acquire_lock(key) + try: + with entry[0]: + data = _fetch_upstream(f"tiles/{layer}/{z}/{x}/{y}{suffix}.{fmt}") + if data: + tiles_mod._write_atomic( + tiles_mod.tile_cache_path(OUTPUT_DIR, layer, z, x, y, + scale, fmt), data) + pending = False + empty = False + finally: + _release_lock(key, entry) else: data = await run_in_threadpool(_tile_bytes, layer, z, x, y, scale, fmt) pending = False diff --git a/lidar_pipeline/tests/test_mapserve.py b/lidar_pipeline/tests/test_mapserve.py index cd67a06..61e86de 100644 --- a/lidar_pipeline/tests/test_mapserve.py +++ b/lidar_pipeline/tests/test_mapserve.py @@ -823,3 +823,36 @@ def test_ui_offers_generation_controls(): assert "all_missing: true" in _MAP_JS # Fin de run : rafraîchissement des couches + relance de la maintenance assert "genAfterRun" in _MAP_JS and "api/map/background" in _MAP_JS + + +def test_cache_only_fetches_missing_tile_upstream(tmp_path, monkeypatch): + """Cache seule + LIDAR_MAPS_URL : une tuile absente est rapatriee du serveur + amont (aucun rendu local : les zooms au-dela de la maintenance s'affichent). + """ + import lidar_pipeline.mapserve as mapserve + from lidar_pipeline import tiles + _setup(tmp_path, monkeypatch) + monkeypatch.setenv("LIDAR_TILE_CACHE_ONLY", "1") + monkeypatch.setattr(mapserve, "MAPS_URL", "http://amont:8975") + monkeypatch.setattr(mapserve, "_UPSTREAM", {"fails": 0, "until": 0.0}) + calls = [] + PNG = bytes([0x89, 0x50, 0x4E, 0x47, 13, 10, 0x1A, 10]) + b'TUILE-AMONT' + + def fake_fetch(rel_path): + calls.append(rel_path) + return PNG + + monkeypatch.setattr(mapserve, "_fetch_upstream", fake_fetch) + try: + z = 17 # jamais couvert par la maintenance (max_z 16 par defaut) + x, y = _tile_of_cell(1054, 6882, z) + resp = asyncio.run(mapserve.tile("aspect", z, x, f"{y}@2x.webp")) + assert calls == [f"tiles/aspect/{z}/{x}/{y}@2x.webp"] + assert resp.body == PNG + assert resp.headers.get("X-Tile-Pending") != "1" # servie, plus en attente + # Mise en cache : la seconde demande ne repasse pas par l'amont + asyncio.run(mapserve.tile("aspect", z, x, f"{y}@2x.webp")) + assert len(calls) == 1 + assert tiles.tile_cache_path(tmp_path, "aspect", z, x, y, 2, "webp").is_file() + finally: + monkeypatch.delenv("LIDAR_TILE_CACHE_ONLY")