2121from redis import asyncio as aioredis
2222
2323from 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
2425from adsb_api .utils .api_tar import router as tar_router
2526from adsb_api .utils .api_v2 import router as v2_router
2627from adsb_api .utils .dependencies import browser , feederData , provider , redisVRS
2728from 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
3132PROJECT_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+
3343description = """
3444The adsb.lol API is a free and open source
3545API 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" )
143156async 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)
176195async 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 )
181200async 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)
206223async 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 )
242262async 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)
385395async 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" ])
0 commit comments