Skip to content

Commit a97d99e

Browse files
committed
h
1 parent 34fd370 commit a97d99e

8 files changed

Lines changed: 334 additions & 293 deletions

File tree

src/adsb_api/app.py

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@
2121
from redis import asyncio as aioredis
2222

2323
from adsb_api.utils.api_routes import router as routes_router
24+
from adsb_api.utils.api_tar import close_http_session as close_tar_http_session
2425
from adsb_api.utils.api_tar import router as tar_router
2526
from adsb_api.utils.api_v2 import router as v2_router
2627
from adsb_api.utils.dependencies import browser, feederData, provider, redisVRS
2728
from adsb_api.utils.models import ApiUuidRequest, PrettyJSONResponse
28-
from adsb_api.utils.settings import (INSECURE, REDIS_HOST, SALT_BEAST,
29+
from adsb_api.utils.settings import (INSECURE, REDIS_KEY_BEAST_CLIENTS, REDIS_KEY_BEAST_RECEIVERS, REDIS_KEY_HUB_AIRCRAFT, REDIS_KEY_MLAT_CLIENTS, REDIS_KEY_MLAT_SYNC, REDIS_KEY_MLAT_TOTALCOUNT, REDIS_HOST, SALT_BEAST,
2930
SALT_MLAT, SALT_MY)
3031

3132
PROJECT_PATH = pathlib.Path(__file__).parent.parent.parent
3233

34+
# Shared aiohttp session for external HTTP requests
35+
_http_session: aiohttp.ClientSession | None = None
36+
37+
async def get_http_session() -> aiohttp.ClientSession:
38+
global _http_session
39+
if _http_session is None:
40+
_http_session = aiohttp.ClientSession()
41+
return _http_session
42+
3343
description = """
3444
The adsb.lol API is a free and open source
3545
API for the [adsb.lol](https://adsb.lol) project.
@@ -132,18 +142,26 @@ async def startup_event():
132142
await redisVRS.dispatch_background_task()
133143
await feederData.dispatch_background_task()
134144
try:
135-
await browser.start()
136-
except:
145+
# Add timeout to prevent hanging if CDP browser is unavailable
146+
await asyncio.wait_for(browser.start(), timeout=5.0)
147+
except asyncio.TimeoutError:
148+
print("browser.start() timed out after 5s - CDP browser may be unavailable")
149+
except Exception:
137150
traceback.print_exc()
138151

139152
ensure_uuid_security()
140153

141154

142155
@app.on_event("shutdown")
143156
async def shutdown_event():
157+
global _http_session
144158
await provider.shutdown()
145159
await redisVRS.shutdown()
146160
await browser.shutdown()
161+
await close_tar_http_session()
162+
if _http_session:
163+
await _http_session.close()
164+
_http_session = None
147165

148166

149167
@app.get(
@@ -155,17 +173,18 @@ async def mlat_receivers(
155173
server: str,
156174
host: str | None = Header(default=None, include_in_schema=False),
157175
):
158-
# if the host is not mlat.adsb.lol,
159-
# return a 404
160176
if host != "mlat.adsb.lol":
161177
print(f"failed mlat_sync host={host}, server={server} (not mlat.adsb.lol)")
162178
return {"error": "not found"}
163179

164-
if server not in provider.mlat_sync_json.keys():
165-
print(f"failed mlat_sync host={host}, server={server} (not in {provider.mlat_sync_json.keys()})")
180+
mlat_sync = await provider._json_get(REDIS_KEY_MLAT_SYNC)
181+
if not mlat_sync:
182+
return {"error": "not found"}
183+
if server not in mlat_sync:
184+
print(f"failed mlat_sync host={host}, server={server} (not in {mlat_sync.keys()})")
166185
return {"error": "not found"}
167186

168-
return provider.mlat_sync_json[server]
187+
return mlat_sync[server]
169188

170189

171190
@app.get(
@@ -174,25 +193,23 @@ async def mlat_receivers(
174193
include_in_schema=False,
175194
)
176195
async def mlat_totalcount_json():
177-
return provider.mlat_totalcount_json
196+
return await provider._json_get(REDIS_KEY_MLAT_TOTALCOUNT) or {}
178197

179198

180199
@app.get("/metrics", include_in_schema=False)
181200
async def metrics():
182-
"""
183-
Return metrics for Prometheus
184-
"""
201+
# Parallel JSON gets with parsing
202+
data = await provider._json_gets([REDIS_KEY_BEAST_CLIENTS, REDIS_KEY_BEAST_RECEIVERS, REDIS_KEY_MLAT_CLIENTS, REDIS_KEY_HUB_AIRCRAFT])
203+
aircraft_count = data.get(REDIS_KEY_HUB_AIRCRAFT)
204+
185205
metrics = [
186-
"adsb_api_beast_total_receivers {}".format(len(provider.beast_receivers)),
187-
"adsb_api_beast_total_clients {}".format(len(provider.beast_clients)),
188-
# "adsb_api_mlat_total {}".format(len(provider.mlat_sync_json)),
189-
# new format is {'0a': {clients}, '0b': {clients}}
190-
# so let's make tag for each server
206+
"adsb_api_beast_total_receivers {}".format(len(data.get(REDIS_KEY_BEAST_RECEIVERS) or [])),
207+
"adsb_api_beast_total_clients {}".format(len(data.get(REDIS_KEY_BEAST_CLIENTS) or [])),
191208
*[
192209
'adsb_api_mlat_total{{server="{0}"}} {1}'.format(server, len(clients))
193-
for server, clients in provider.mlat_clients.items()
210+
for server, clients in (data.get(REDIS_KEY_MLAT_CLIENTS) or {}).items()
194211
],
195-
"adsb_api_aircraft_total {}".format(provider.aircraft_totalcount),
212+
f"adsb_api_aircraft_total {int(aircraft_count) if aircraft_count else 0}",
196213
]
197214
return Response(content="\n".join(metrics), media_type="text/plain")
198215

@@ -205,21 +222,24 @@ async def metrics():
205222
)
206223
async def api_me(request: Request):
207224
client_ip = request.client.host
208-
my_beast_clients = provider.get_clients_per_client_ip(client_ip)
209-
mlat_clients = provider.mlat_clients_to_list(client_ip)
225+
my_beast_clients, mlat_clients = await asyncio.gather(
226+
provider.get_clients_per_client_ip(client_ip),
227+
provider.mlat_clients_to_list(client_ip),
228+
)
229+
230+
data = await provider._json_gets([REDIS_KEY_MLAT_CLIENTS, REDIS_KEY_BEAST_CLIENTS, REDIS_KEY_HUB_AIRCRAFT])
231+
mlat_data, beast_data, aircraft_count = data.get(REDIS_KEY_MLAT_CLIENTS) or {}, data.get(REDIS_KEY_BEAST_CLIENTS) or {}, data.get(REDIS_KEY_HUB_AIRCRAFT)
210232

211-
# count all items as mlat_clients format is {'0a': {clients}, '0b': {clients}}
212-
all_mlat_clients = sum([len(i) for i in provider.mlat_clients.values()])
213233
response = {
214234
"_motd": [],
215235
"clients": {
216236
"beast": my_beast_clients,
217237
"mlat": mlat_clients,
218238
},
219239
"global": {
220-
"beast": len(provider.beast_clients),
221-
"mlat": all_mlat_clients,
222-
"aircraft": provider.aircraft_totalcount,
240+
"beast": len(beast_data),
241+
"mlat": sum([len(i) for i in mlat_data.values()]),
242+
"aircraft": int(aircraft_count) if aircraft_count else 0,
223243
},
224244
}
225245

@@ -241,7 +261,7 @@ async def api_me(request: Request):
241261
@app.get("/api/0/my", tags=["v0"], summary="My Map redirect based on IP", include_in_schema=False)
242262
async def api_my(request: Request):
243263
client_ip = request.client.host
244-
my_beast_clients = provider.get_clients_per_client_ip(client_ip)
264+
my_beast_clients = await provider.get_clients_per_client_ip(client_ip)
245265
uids = []
246266
if len(my_beast_clients) == 0:
247267
return RedirectResponse(
@@ -343,27 +363,17 @@ async def planespotters_net_hex(
343363
# check if we have a cached response
344364

345365
if cache := await redisVRS.redis.get(redis_key):
346-
# return the cached response
347366
return orjson.loads(cache)
348-
# if not, query the API
349-
async with aiohttp.ClientSession() as session:
350-
async with session.get(
351-
f"https://api.planespotters.net/pub/photos/hex/{hex}",
352-
params=params,
353-
) as response:
354-
if response.status == 200:
355-
# cache the response for 1h
356-
data = await response.json()
357-
await redisVRS.redis.setex(redis_key, 3600, orjson.dumps(data))
358-
res = data
359-
else:
360-
res = {"error": "not found"}
361-
return PrettyJSONResponse(
362-
content=res,
363-
headers={
364-
"Access-Control-Allow-Origin": "*",
365-
},
366-
)
367+
# if not, query the API (using shared session)
368+
session = await get_http_session()
369+
async with session.get(
370+
f"https://api.planespotters.net/pub/photos/hex/{hex}",
371+
params=params,
372+
) as response:
373+
if response.status == 200:
374+
await redisVRS.redis.setex(redis_key, 3600, orjson.dumps(await response.json()))
375+
return await response.json()
376+
return {"error": "not found"}
367377

368378

369379
@app.options("/0/planespotters_net/hex/{hex}", include_in_schema=False)
@@ -383,9 +393,13 @@ async def planespotters_net_hex_options():
383393
tags=["v0"],
384394
)
385395
async def h3_latency():
396+
data = await provider._json_gets([REDIS_KEY_BEAST_RECEIVERS, REDIS_KEY_BEAST_CLIENTS])
397+
beast_receivers = data.get(REDIS_KEY_BEAST_RECEIVERS) or []
398+
beast_clients = data.get(REDIS_KEY_BEAST_CLIENTS) or []
399+
386400
_h3 = defaultdict(list)
387-
for receiverId, lat, lon in provider.beast_receivers:
388-
for client in provider.beast_clients:
401+
for receiverId, lat, lon in beast_receivers:
402+
for client in beast_clients:
389403
if not client["_uuid"].startswith(receiverId) or client.get("ms", -1) < 0:
390404
continue
391405
_h3[h3.latlng_to_cell(lat, lon, 1)].append(client["ms"])

src/adsb_api/utils/api_routes.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@
1414
router = APIRouter(prefix="/api", tags=["v0"])
1515

1616

17-
async def calc_plausible(route, lat: str, lng: str) -> bool:
18-
"""Calculate if route is plausible for given position (non-blocking)."""
17+
async def calc_plausible(route, lat: float, lng: float) -> bool:
1918
for i in range(len(route.get("_airports", [])) - 1):
2019
a, b = route["_airports"][i], route["_airports"][i + 1]
21-
is_plausible, _ = await plausible(lat, lng, f"{a['lat']:.5f}", f"{a['lon']:.5f}", f"{b['lat']:.5f}", f"{b['lon']:.5f}")
22-
if is_plausible:
20+
if await plausible(round(lat, 3), round(lng, 3), round(a["lat"], 3), round(a["lon"], 3), round(b["lat"], 3), round(b["lon"], 3)):
2321
return True
2422
return False
2523

2624

27-
async def get_route_cached_or_fetch(callsign: str, lat: str, lng: str) -> dict:
25+
async def get_route_cached_or_fetch(callsign: str, lat: float, lng: float) -> dict:
2826
"""Get route from cache or fetch, with plausible calculation."""
2927
if cached := await redisVRS.get_cached_route(callsign):
3028
return cached
@@ -46,7 +44,7 @@ async def api_airport(icao: str):
4644
summary="Route plus plausible flag", description="Data by https://github.com/vradarserver/standing-data/",
4745
include_in_schema=False)
4846
async def api_route3(callsign: str, lat: str, lng: str):
49-
return PrettyJSONResponse(content=await get_route_cached_or_fetch(callsign, lat, lng), headers=CORS_HEADERS)
47+
return PrettyJSONResponse(content=await get_route_cached_or_fetch(callsign, float(lat), float(lng)), headers=CORS_HEADERS)
5048

5149

5250
@router.get("/0/route/{callsign}", response_class=PrettyJSONResponse, tags=["v0"],
@@ -68,15 +66,12 @@ async def api_routeset(planeList: PlaneList):
6866
fetched = await redisVRS.get_routes_bulk(uncached) if uncached else {}
6967

7068
# Merge routes, track uncached for parallel plausible
71-
routes = {}
72-
tasks = []
69+
routes, tasks = {}, []
7370
for p in planeList.planes:
74-
r = cached.get(p.callsign) or fetched.get(p.callsign)
75-
if not r:
76-
r = {"callsign": p.callsign, "airport_codes": "unknown", "_airports": []}
77-
elif p.callsign in uncached and r["airport_codes"] != "unknown":
78-
tasks.append((p.callsign, r, p.lat, p.lng))
71+
r = cached.get(p.callsign) or fetched.get(p.callsign) or {"callsign": p.callsign, "airport_codes": "unknown", "_airports": []}
7972
routes[p.callsign] = r
73+
if p.callsign in uncached and r["airport_codes"] != "unknown":
74+
tasks.append((p.callsign, r, p.lat, p.lng))
8075

8176
# Parallel plausible + cache
8277
if tasks:

src/adsb_api/utils/api_tar.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@
2121
tags=["v0"],
2222
)
2323

24+
# Shared session for HTTP requests (reused across requests)
25+
_http_session: aiohttp.ClientSession | None = None
26+
27+
async def get_http_session() -> aiohttp.ClientSession:
28+
global _http_session
29+
if _http_session is None:
30+
_http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10, connect=2))
31+
return _http_session
32+
33+
async def close_http_session():
34+
global _http_session
35+
if _http_session:
36+
await _http_session.close()
37+
_http_session = None
38+
2439

2540
@router.get(
2641
"/screenshot/",
@@ -52,18 +67,16 @@ async def get_new_screenshot(
5267

5368
min_lat, min_lon, max_lat, max_lon = False, False, False, False
5469
# get the min and max lat/lon from re-api
55-
async with aiohttp.ClientSession() as session:
56-
async with session.get(
57-
f"{REAPI_ENDPOINT}/?find_hex={','.join(icaos)}"
58-
) as response:
59-
data = await response.json()
60-
for aircraft in data["aircraft"]:
61-
if not aircraft.get("lat") or not aircraft.get("lon"):
62-
continue
63-
min_lat = min(min_lat, aircraft["lat"]) if min_lat else aircraft["lat"]
64-
min_lon = min(min_lon, aircraft["lon"]) if min_lon else aircraft["lon"]
65-
max_lat = max(max_lat, aircraft["lat"]) if max_lat else aircraft["lat"]
66-
max_lon = max(max_lon, aircraft["lon"]) if max_lon else aircraft["lon"]
70+
session = await get_http_session()
71+
async with session.get(f"{REAPI_ENDPOINT}/?find_hex={','.join(icaos)}") as response:
72+
data = await response.json()
73+
for aircraft in data["aircraft"]:
74+
if not aircraft.get("lat") or not aircraft.get("lon"):
75+
continue
76+
min_lat = min(min_lat, aircraft["lat"]) if min_lat else aircraft["lat"]
77+
min_lon = min(min_lon, aircraft["lon"]) if min_lon else aircraft["lon"]
78+
max_lat = max(max_lat, aircraft["lat"]) if max_lat else aircraft["lat"]
79+
max_lon = max(max_lon, aircraft["lon"]) if max_lon else aircraft["lon"]
6780
# if they are still not set, return 404. we can't get a fix, so we can't get a screenshot. sorry.
6881
if not min_lat or not min_lon or not max_lat or not max_lon:
6982
return Response(status_code=404)

0 commit comments

Comments
 (0)