Weather review fixes: zone-fetch time budget, prod NWS_USER_AGENT (Refs #237)

- Cap one refresh's NWS zone fetches at 15 s wall clock. They run under the
  feed lock, so a hung zone endpoint could otherwise stall every
  /weather/alerts request for minutes; unfinished zones retry next refresh.
- Prod c2-core.env.j2 never set NWS_USER_AGENT, so the VM would have sent the
  contact-less placeholder. Default it to the site URL (overridable via
  vault_nws_user_agent); warn on the .env.example example.com placeholder too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 02:58:32 -04:00
co-authored by Claude Opus 5.5
parent fb1b6956c2
commit bdbde359a1
4 changed files with 40 additions and 2 deletions
+21
View File
@@ -216,3 +216,24 @@ def test_wind_unavailable_reasons():
with patch.object(weather.httpx, "AsyncClient", FakeHttp({weather.METAR_URL: Resp(status=503)})):
body = client.get("/weather/wind", params={"lat": 41.03, "lon": -73.76}).json()
assert body["available"] is False and "unavailable" in body["reason"] and "503" in body["error"]
def test_slow_zone_endpoint_cannot_hold_the_feed_lock():
"""Zone fetches run under the feed lock; a hung zone endpoint must not stall
every /weather/alerts request -- the alert lands in zone_only instead."""
import time
class SlowZones(FakeHttp):
async def get(self, url, params=None, headers=None):
if url.startswith(weather.NWS_ZONE_PREFIX):
await asyncio.sleep(5)
return await super().get(url, params, headers)
http = SlowZones({weather.NWS_ALERTS_URL: Resp({"features": [_alert("z1", "Flood Watch", None, [ZONE_A])]}),
ZONE_A: Resp({"geometry": ZONE_POLY})})
with patch.object(weather, "ZONE_BUDGET_S", 0.2), patch.object(weather.httpx, "AsyncClient", http):
t0 = time.time()
feed = asyncio.run(weather.alerts.get())
assert time.time() - t0 < 2
assert feed.error is None and [z["id"] for z in feed.zone_only] == ["z1"]
assert weather.zones.get(ZONE_A) == (False, None) # not cached as a failure; retried next refresh