Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,15 @@ async def lifespan(app: FastAPI):

from services.region_lookup import warm_borders

await run_in_threadpool(warm_borders)
try:
await run_in_threadpool(warm_borders)
except Exception:
# Same policy as prime_pipeline_at_startup above: a missing or corrupt
# borders file costs /api/towers its region detection until the next
# restart (classify_region still loads on demand and surfaces the real
# error there); raising would abort the lifespan and take the whole
# API down with it.
logging.exception("Warming the border polygons failed; region lookup will load on demand")

# Restore persisted state before accepting connections
restored = restore_snapshot()
Expand Down
9 changes: 8 additions & 1 deletion backend/services/region_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ def _load_borders() -> None:
return
with open(_BORDERS_PATH) as f:
data = json.load(f)
loaded: dict[str, BaseGeometry] = {}
for feature in data["features"]:
admin = feature["properties"].get("ADMIN")
source = _ADMIN_TO_SOURCE.get(admin)
if source is not None:
_geoms[source] = shape(feature["geometry"])
loaded[source] = shape(feature["geometry"])
# Published complete or not at all: the fast path above reads _geoms
# without the lock, so filling it feature-by-feature would let a
# concurrent caller see {"us"} mid-parse, skip the load, and silently
# misclassify a Canadian point as unsupported. dict.update runs under
# one GIL hold, so readers see the border set whole or empty.
_geoms.update(loaded)


def warm_borders() -> None:
Expand Down
6 changes: 5 additions & 1 deletion backend/tests/test_adsb_seed_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ def _assert_derived(hexn: str, raw: dict) -> None:
("ground", {"alt_baro": "ground", "gs": None, "track": "unknown"}),
]

# The sim push endpoint rejects non-transponder hexes (id_utils.is_transponder_hex),
# so its writer fixtures must look like real 24-bit addresses, not readable labels.
_SIM_CASE_HEX = {name: f"a1b2c{i}" for i, (name, _kin) in enumerate(_WRITE_CASES)}


class TestDerivedFieldsAtWriteTime:
"""Every path that writes state.adsb_aircraft stores the SI-unit fields
Expand Down Expand Up @@ -284,7 +288,7 @@ async def test_sim_ingest_writer(self, name, kin):
# what is under test, not the gate in front of it.
from routes.sim_ingest import sim_push_adsb_positions

hexn = f"sim{name}"
hexn = _SIM_CASE_HEX[name]
entry = {"hex": hexn, "lat": 33.9, "lon": -84.6, **kin}
await sim_push_adsb_positions(body={"ts_ms": 4242, "aircraft": [entry]}, _key=None)

Expand Down
4 changes: 1 addition & 3 deletions backend/tests/test_region_lookup_warm.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ def test_a_warm_cache_is_never_re_parsed(self):
_clear_cache()
region_lookup.warm_borders()

with unittest.mock.patch.object(
region_lookup, "shape", side_effect=AssertionError("re-parsed after warm-up")
):
with unittest.mock.patch.object(region_lookup, "shape", side_effect=AssertionError("re-parsed after warm-up")):
assert region_lookup.classify_region(42.38708028093612, -71.24905416622781) == "us"
assert region_lookup.classify_region(48.8566, 2.3522) is None

Expand Down
Loading