forked from hans/Nominatim
Adds lazy loading for search endpoint availability
Introduces a mechanism to defer the search endpoint's availability check until the first request, improving startup robustness. If the search table is unavailable due to DB issues, the endpoint now responds with a 503 or 404 as appropriate, and retries the check on subsequent requests. This ensures that downtime or partial DB failures no longer prevent the API from initializing or serving reverse-only mode.
This commit is contained in:
@@ -184,6 +184,10 @@ class APIMiddleware:
|
|||||||
formatter = load_format_dispatcher('v1', self.api.config.project_dir)
|
formatter = load_format_dispatcher('v1', self.api.config.project_dir)
|
||||||
for name, func in await api_impl.get_routes(self.api):
|
for name, func in await api_impl.get_routes(self.api):
|
||||||
endpoint = EndpointWrapper(name, func, self.api, formatter)
|
endpoint = EndpointWrapper(name, func, self.api, formatter)
|
||||||
|
# If func is a LazySearchEndpoint, give it a reference to wrapper
|
||||||
|
# so it can replace wrapper.func dynamically
|
||||||
|
if hasattr(func, 'set_wrapper'):
|
||||||
|
func.set_wrapper(endpoint)
|
||||||
self.app.add_route(f"/{name}", endpoint)
|
self.app.add_route(f"/{name}", endpoint)
|
||||||
if legacy_urls:
|
if legacy_urls:
|
||||||
self.app.add_route(f"/{name}.php", endpoint)
|
self.app.add_route(f"/{name}.php", endpoint)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import Optional, Any, Type, Dict, cast, Sequence, Tuple
|
|||||||
from functools import reduce
|
from functools import reduce
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
import asyncio
|
||||||
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
@@ -124,6 +125,12 @@ def parse_geometry_details(adaptor: ASGIAdaptor, fmt: str) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_search_name(conn: sa.engine.Connection) -> bool:
|
||||||
|
""" Check if the search_name table exists in the database.
|
||||||
|
"""
|
||||||
|
return sa.inspect(conn).has_table('search_name')
|
||||||
|
|
||||||
|
|
||||||
async def status_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
async def status_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
||||||
""" Server glue for /status endpoint. See API docs for details.
|
""" Server glue for /status endpoint. See API docs for details.
|
||||||
"""
|
"""
|
||||||
@@ -441,6 +448,61 @@ async def polygons_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
|||||||
return build_response(params, params.formatting().format_result(results, fmt, {}))
|
return build_response(params, params.formatting().format_result(results, fmt, {}))
|
||||||
|
|
||||||
|
|
||||||
|
async def search_unavailable_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
||||||
|
""" Server glue for /search endpoint in reverse-only mode.
|
||||||
|
Returns 404 when search functionality is not available.
|
||||||
|
"""
|
||||||
|
params.raise_error('Search not available (reverse-only mode)', 404)
|
||||||
|
|
||||||
|
|
||||||
|
class LazySearchEndpoint:
|
||||||
|
"""
|
||||||
|
Lazy-loading search endpoint that replaces itself after first successful check.
|
||||||
|
|
||||||
|
- Falcon: EndpointWrapper stores this instance in wrapper.func
|
||||||
|
On first request, replace wrapper.func directly with real endpoint
|
||||||
|
|
||||||
|
- Starlette: _wrap_endpoint wraps this instance in a callback
|
||||||
|
store a delegate function and call it on subsequent requests
|
||||||
|
"""
|
||||||
|
def __init__(self, api: NominatimAPIAsync, real_endpoint: EndpointFunc):
|
||||||
|
self.api = api
|
||||||
|
self.real_endpoint = real_endpoint
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
self._wrapper: Any = None # Store reference to Falcon's EndpointWrapper
|
||||||
|
self._delegate: Optional[EndpointFunc] = None
|
||||||
|
|
||||||
|
def set_wrapper(self, wrapper: Any) -> None:
|
||||||
|
self._wrapper = wrapper
|
||||||
|
|
||||||
|
async def __call__(self, api: NominatimAPIAsync, params: ASGIAdaptor) -> Any:
|
||||||
|
if self._delegate is None:
|
||||||
|
async with self._lock:
|
||||||
|
# Double-check after acquiring lock (thread safety)
|
||||||
|
if self._delegate is None:
|
||||||
|
try:
|
||||||
|
async with api.begin() as conn:
|
||||||
|
has_table = await conn.connection.run_sync(
|
||||||
|
has_search_name)
|
||||||
|
|
||||||
|
if has_table:
|
||||||
|
# For Starlette
|
||||||
|
self._delegate = self.real_endpoint
|
||||||
|
# For Falcon
|
||||||
|
if self._wrapper is not None:
|
||||||
|
self._wrapper.func = self.real_endpoint
|
||||||
|
else:
|
||||||
|
self._delegate = search_unavailable_endpoint
|
||||||
|
if self._wrapper is not None:
|
||||||
|
self._wrapper.func = search_unavailable_endpoint
|
||||||
|
|
||||||
|
except (PGCORE_ERROR, sa.exc.OperationalError, OSError):
|
||||||
|
# No _delegate set, so retry on next request
|
||||||
|
params.raise_error('Search temporarily unavailable', 503)
|
||||||
|
|
||||||
|
return await self._delegate(api, params)
|
||||||
|
|
||||||
|
|
||||||
async def get_routes(api: NominatimAPIAsync) -> Sequence[Tuple[str, EndpointFunc]]:
|
async def get_routes(api: NominatimAPIAsync) -> Sequence[Tuple[str, EndpointFunc]]:
|
||||||
routes = [
|
routes = [
|
||||||
('status', status_endpoint),
|
('status', status_endpoint),
|
||||||
@@ -451,15 +513,13 @@ async def get_routes(api: NominatimAPIAsync) -> Sequence[Tuple[str, EndpointFunc
|
|||||||
('polygons', polygons_endpoint),
|
('polygons', polygons_endpoint),
|
||||||
]
|
]
|
||||||
|
|
||||||
def has_search_name(conn: sa.engine.Connection) -> bool:
|
|
||||||
insp = sa.inspect(conn)
|
|
||||||
return insp.has_table('search_name')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with api.begin() as conn:
|
async with api.begin() as conn:
|
||||||
if await conn.connection.run_sync(has_search_name):
|
if await conn.connection.run_sync(has_search_name):
|
||||||
routes.append(('search', search_endpoint))
|
routes.append(('search', search_endpoint))
|
||||||
except (PGCORE_ERROR, sa.exc.OperationalError):
|
else:
|
||||||
pass # ignored
|
routes.append(('search', search_unavailable_endpoint))
|
||||||
|
except (PGCORE_ERROR, sa.exc.OperationalError, OSError):
|
||||||
|
routes.append(('search', LazySearchEndpoint(api, search_endpoint)))
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
|
|||||||
Reference in New Issue
Block a user