mirror of
https://github.com/osm-search/Nominatim.git
synced 2026-02-16 15:47:58 +00:00
apply request timeout also while waiting for a connection from pool
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# This file is part of Nominatim. (https://nominatim.org)
|
||||
#
|
||||
# Copyright (C) 2024 by the Nominatim developer community.
|
||||
# Copyright (C) 2025 by the Nominatim developer community.
|
||||
# For a full list of authors see the git log.
|
||||
"""
|
||||
Implementation of classes for API access via libraries.
|
||||
@@ -14,6 +14,11 @@ import sys
|
||||
import contextlib
|
||||
from pathlib import Path
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from asyncio import timeout_at
|
||||
else:
|
||||
from async_timeout import timeout_at
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.ext.asyncio as sa_asyncio
|
||||
|
||||
@@ -26,6 +31,7 @@ from .connection import SearchConnection
|
||||
from .status import get_status, StatusResult
|
||||
from .lookup import get_places, get_detailed_place
|
||||
from .reverse import ReverseGeocoder
|
||||
from .timeout import Timeout
|
||||
from . import search as nsearch
|
||||
from . import types as ntyp
|
||||
from .results import DetailedResult, ReverseResult, SearchResults
|
||||
@@ -172,12 +178,15 @@ class NominatimAPIAsync:
|
||||
await self.close()
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def begin(self) -> AsyncIterator[SearchConnection]:
|
||||
async def begin(self, abs_timeout: Optional[float] = None) -> AsyncIterator[SearchConnection]:
|
||||
""" Create a new connection with automatic transaction handling.
|
||||
|
||||
This function may be used to get low-level access to the database.
|
||||
Refer to the documentation of SQLAlchemy for details how to use
|
||||
the connection object.
|
||||
|
||||
You may optionally give an absolute timeout until when to wait
|
||||
for a connection to become available.
|
||||
"""
|
||||
if self._engine is None:
|
||||
await self.setup_database()
|
||||
@@ -185,14 +194,15 @@ class NominatimAPIAsync:
|
||||
assert self._engine is not None
|
||||
assert self._tables is not None
|
||||
|
||||
async with self._engine.begin() as conn:
|
||||
async with timeout_at(abs_timeout), self._engine.begin() as conn:
|
||||
yield SearchConnection(conn, self._tables, self._property_cache, self.config)
|
||||
|
||||
async def status(self) -> StatusResult:
|
||||
""" Return the status of the database.
|
||||
"""
|
||||
timeout = Timeout(self.request_timeout)
|
||||
try:
|
||||
async with self.begin() as conn:
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
status = await get_status(conn)
|
||||
except (PGCORE_ERROR, sa.exc.OperationalError):
|
||||
@@ -205,8 +215,9 @@ class NominatimAPIAsync:
|
||||
|
||||
Returns None if there is no entry under the given ID.
|
||||
"""
|
||||
timeout = Timeout(self.request_timeout)
|
||||
details = ntyp.LookupDetails.from_kwargs(params)
|
||||
async with self.begin() as conn:
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
if details.keywords:
|
||||
await nsearch.make_query_analyzer(conn)
|
||||
@@ -217,8 +228,9 @@ class NominatimAPIAsync:
|
||||
|
||||
Returns a list of place information for all IDs that were found.
|
||||
"""
|
||||
timeout = Timeout(self.request_timeout)
|
||||
details = ntyp.LookupDetails.from_kwargs(params)
|
||||
async with self.begin() as conn:
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
if details.keywords:
|
||||
await nsearch.make_query_analyzer(conn)
|
||||
@@ -235,8 +247,9 @@ class NominatimAPIAsync:
|
||||
# There are no results to be expected outside valid coordinates.
|
||||
return None
|
||||
|
||||
timeout = Timeout(self.request_timeout)
|
||||
details = ntyp.ReverseDetails.from_kwargs(params)
|
||||
async with self.begin() as conn:
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
if details.keywords:
|
||||
await nsearch.make_query_analyzer(conn)
|
||||
@@ -251,10 +264,11 @@ class NominatimAPIAsync:
|
||||
if not query:
|
||||
raise UsageError('Nothing to search for.')
|
||||
|
||||
async with self.begin() as conn:
|
||||
timeout = Timeout(self.request_timeout)
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
geocoder = nsearch.ForwardGeocoder(conn, ntyp.SearchDetails.from_kwargs(params),
|
||||
self.request_timeout)
|
||||
timeout)
|
||||
phrases = [nsearch.Phrase(nsearch.PHRASE_ANY, p.strip()) for p in query.split(',')]
|
||||
return await geocoder.lookup(phrases)
|
||||
|
||||
@@ -268,7 +282,8 @@ class NominatimAPIAsync:
|
||||
**params: Any) -> SearchResults:
|
||||
""" Find an address using structured search.
|
||||
"""
|
||||
async with self.begin() as conn:
|
||||
timeout = Timeout(self.request_timeout)
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
details = ntyp.SearchDetails.from_kwargs(params)
|
||||
|
||||
@@ -310,7 +325,7 @@ class NominatimAPIAsync:
|
||||
if amenity:
|
||||
details.layers |= ntyp.DataLayer.POI
|
||||
|
||||
geocoder = nsearch.ForwardGeocoder(conn, details, self.request_timeout)
|
||||
geocoder = nsearch.ForwardGeocoder(conn, details, timeout)
|
||||
return await geocoder.lookup(phrases)
|
||||
|
||||
async def search_category(self, categories: List[Tuple[str, str]],
|
||||
@@ -323,8 +338,9 @@ class NominatimAPIAsync:
|
||||
if not categories:
|
||||
return SearchResults()
|
||||
|
||||
timeout = Timeout(self.request_timeout)
|
||||
details = ntyp.SearchDetails.from_kwargs(params)
|
||||
async with self.begin() as conn:
|
||||
async with self.begin(abs_timeout=timeout.abs) as conn:
|
||||
conn.set_query_timeout(self.query_timeout)
|
||||
if near_query:
|
||||
phrases = [nsearch.Phrase(nsearch.PHRASE_ANY, p) for p in near_query.split(',')]
|
||||
@@ -333,7 +349,7 @@ class NominatimAPIAsync:
|
||||
if details.keywords:
|
||||
await nsearch.make_query_analyzer(conn)
|
||||
|
||||
geocoder = nsearch.ForwardGeocoder(conn, details, self.request_timeout)
|
||||
geocoder = nsearch.ForwardGeocoder(conn, details, timeout)
|
||||
return await geocoder.lookup_pois(categories, phrases)
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ Public interface to the search code.
|
||||
from typing import List, Any, Optional, Iterator, Tuple, Dict
|
||||
import itertools
|
||||
import re
|
||||
import datetime as dt
|
||||
import difflib
|
||||
|
||||
from ..connection import SearchConnection
|
||||
from ..types import SearchDetails
|
||||
from ..results import SearchResult, SearchResults, add_result_details
|
||||
from ..timeout import Timeout
|
||||
from ..logging import log
|
||||
from .token_assignment import yield_token_assignments
|
||||
from .db_search_builder import SearchBuilder, build_poi_search, wrap_near_search
|
||||
@@ -29,10 +29,10 @@ class ForwardGeocoder:
|
||||
"""
|
||||
|
||||
def __init__(self, conn: SearchConnection,
|
||||
params: SearchDetails, timeout: Optional[int]) -> None:
|
||||
params: SearchDetails, timeout: Timeout) -> None:
|
||||
self.conn = conn
|
||||
self.params = params
|
||||
self.timeout = dt.timedelta(seconds=timeout or 1000000)
|
||||
self.timeout = timeout
|
||||
self.query_analyzer: Optional[AbstractQueryAnalyzer] = None
|
||||
|
||||
@property
|
||||
@@ -78,8 +78,6 @@ class ForwardGeocoder:
|
||||
log().section('Execute database searches')
|
||||
results: Dict[Any, SearchResult] = {}
|
||||
|
||||
end_time = dt.datetime.now() + self.timeout
|
||||
|
||||
min_ranking = searches[0].penalty + 2.0
|
||||
prev_penalty = 0.0
|
||||
for i, search in enumerate(searches):
|
||||
@@ -99,7 +97,7 @@ class ForwardGeocoder:
|
||||
min_ranking = min(min_ranking, result.accuracy * 1.2, 2.0)
|
||||
log().result_dump('Results', ((r.accuracy, r) for r in lookup_results))
|
||||
prev_penalty = search.penalty
|
||||
if dt.datetime.now() >= end_time:
|
||||
if self.timeout.is_elapsed():
|
||||
break
|
||||
|
||||
return SearchResults(results.values())
|
||||
|
||||
24
src/nominatim_api/timeout.py
Normal file
24
src/nominatim_api/timeout.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# This file is part of Nominatim. (https://nominatim.org)
|
||||
#
|
||||
# Copyright (C) 2025 by the Nominatim developer community.
|
||||
# For a full list of authors see the git log.
|
||||
"""
|
||||
Helpers for handling of timeouts for request.
|
||||
"""
|
||||
from typing import Union, Optional
|
||||
import asyncio
|
||||
|
||||
|
||||
class Timeout:
|
||||
""" A class that provides helper functions to ensure a given timeout
|
||||
is respected. Can only be used from coroutines.
|
||||
"""
|
||||
def __init__(self, timeout: Optional[Union[int, float]]) -> None:
|
||||
self.abs = None if timeout is None else asyncio.get_running_loop().time() + timeout
|
||||
|
||||
def is_elapsed(self) -> bool:
|
||||
""" Check if the timeout has already passed.
|
||||
"""
|
||||
return (self.abs is not None) and (asyncio.get_running_loop().time() >= self.abs)
|
||||
Reference in New Issue
Block a user