aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/__init__.py2
-rw-r--r--fietsboek/trackmap.py14
-rw-r--r--fietsboek/views/tileproxy.py98
3 files changed, 79 insertions, 35 deletions
diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py
index 3d6c125..797d38b 100644
--- a/fietsboek/__init__.py
+++ b/fietsboek/__init__.py
@@ -174,7 +174,7 @@ def main(global_config, **settings):
config.add_request_method(redis_, name="redis", reify=True)
config.add_request_method(config_, name="config", reify=True)
- config.registry.registerUtility(TileRequester())
+ config.registry.registerUtility(TileRequester(redis.from_url(parsed_config.redis_url)))
jinja2_env = config.get_jinja2_environment()
jinja2_env.filters["format_decimal"] = mod_jinja2.filter_format_decimal
diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py
index b141790..a9fc439 100644
--- a/fietsboek/trackmap.py
+++ b/fietsboek/trackmap.py
@@ -79,18 +79,8 @@ class TrackMapRenderer:
return start_x, start_y
def _load_tile(self, zoom, x, y) -> Image:
- url = (
- self.layer.url
- .unicode_string()
- .replace(quote("{x}"), str(x))
- .replace(quote("{y}"), str(y))
- .replace(quote("{z}"), str(zoom))
- )
- headers = {
- "user-agent": f"Fietsboek-Tile-Proxy/0.10.0",
- }
- imagebytes = self.requester.load_tile(url, headers=headers).content
- return Image.open(io.BytesIO(imagebytes))
+ tile_data = self.requester.load_tile(self.layer, zoom, x, y)
+ return Image.open(io.BytesIO(tile_data))
def _draw_lines(self, image, zoom, start_x, start_y):
starts = self.track.walk(only_points=True)
diff --git a/fietsboek/views/tileproxy.py b/fietsboek/views/tileproxy.py
index 0efe4de..11abf2a 100644
--- a/fietsboek/views/tileproxy.py
+++ b/fietsboek/views/tileproxy.py
@@ -242,15 +242,39 @@ Note that new requests reset the timeout.
class ITileRequester(Interface): # pylint: disable=inherit-non-class
"""An interface to define the tile requester."""
- def load_tile(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
+ def load_url(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
"""Loads a tile at the given URL.
+ This is a low-level version of :meth:`~ITileRequester.load_tile`.
+
:param url: The URL of the tile to load.
:param headers: Additional headers to send.
:return: The response.
"""
raise NotImplementedError()
+ def load_tile(
+ self,
+ layer: TileLayerConfig,
+ zoom: int,
+ x: int,
+ y: int,
+ *,
+ headers: Optional[dict[str, str]] = None,
+ use_cache: bool = True,
+ ) -> bytes:
+ """Loads a tile from the given layer.
+
+ :param layer: The configured tile layer.
+ :param zoom: The zoom level.
+ :param x: The tile's x coordinate.
+ :param y: The tile's y coordinate.
+ :param headers: Additional headers.
+ :param use_cache: Whether to use the cache (if available).
+ :return: The bytes of the tile.
+ """
+ raise NotImplementedError()
+
@implementer(ITileRequester)
class TileRequester: # pylint: disable=too-few-public-methods
@@ -261,7 +285,7 @@ class TileRequester: # pylint: disable=too-few-public-methods
servers by not hammering them with too many connections.
"""
- def __init__(self):
+ def __init__(self, redis):
self.session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_maxsize=MAX_CONCURRENT_CONNECTIONS,
@@ -271,14 +295,56 @@ class TileRequester: # pylint: disable=too-few-public-methods
self.session.mount("https://", adapter)
self.lock = threading.Lock()
self.closer = None
-
- def load_tile(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
- """Implementation of :meth:`ITileRequester.load_tile`."""
+ self.redis = redis
+
+ def load_url(self, url: str, headers: Optional[dict[str, str]] = None) -> requests.Response:
+ """Implementation of :meth:`ITileRequester.load_url`."""
+ if headers is None:
+ headers = {}
+ if "user-agent" not in headers:
+ headers["user-agent"] = f"Fietsboek/{__VERSION__}"
response = self.session.get(url, headers=headers, timeout=TIMEOUT.total_seconds())
response.raise_for_status()
self._schedule_session_close()
return response
+ def load_tile(
+ self,
+ layer: TileLayerConfig,
+ zoom: int,
+ x: int,
+ y: int,
+ *,
+ headers: Optional[dict[str, str]] = None,
+ use_cache: bool = True,
+ ) -> bytes:
+ """Implementation of :meth:`ITileRequester.load_tile`."""
+ cache_key = f"tile:{layer.layer_id}-{x}-{y}-{zoom}"
+
+ if use_cache and self.redis is not None:
+ cached = self.redis.get(cache_key)
+ if cached is not None:
+ LOGGER.debug("Cache hit for %s/z:%s/x:%s/y:%s", layer.layer_id, zoom, x, y)
+ return cached
+
+ url = (
+ layer.url.unicode_string()
+ .replace(quote("{x}"), str(x))
+ .replace(quote("{y}"), str(y))
+ .replace(quote("{z}"), str(zoom))
+ )
+
+ # Avoid doing actual requests during tests
+ if url.startswith("http://localhost:0"):
+ LOGGER.debug("Skipping tile request for testing URL")
+ return b""
+
+ response = self.load_url(url, headers=headers)
+ tile_data = response.content
+ if use_cache and self.redis is not None:
+ self.redis.set(cache_key, tile_data, ex=TTL)
+ return tile_data
+
def _schedule_session_close(self):
with self.lock:
if self.closer:
@@ -332,27 +398,15 @@ def tile_proxy(request):
LOGGER.debug("Aborted attempt to contact %s due to previous timeouts", provider)
raise HTTPGatewayTimeout(f"Avoiding request to {provider}")
- url = (
- tile_sources[provider]
- .url.unicode_string()
- .replace(quote("{x}"), str(x))
- .replace(quote("{y}"), str(y))
- .replace(quote("{z}"), str(z))
- )
- # Avoid doing actual requests during tests
- if url.startswith("http://localhost:0"):
- LOGGER.debug("Skipping tile proxy request for testing URL")
- return Response(b"", content_type="image/png")
- headers = {
- "user-agent": f"Fietsboek-Tile-Proxy/{__VERSION__}",
- }
+ headers = {}
from_mail = request.config.email_from
if from_mail:
headers["from"] = from_mail
loader: ITileRequester = request.registry.getUtility(ITileRequester)
try:
- resp = loader.load_tile(url, headers=headers)
+ # We already tried the cache, so bypass it here
+ resp = loader.load_tile(tile_sources[provider], z, x, y, headers=headers, use_cache=False)
except ReadTimeout:
LOGGER.debug("Proxy timeout when accessing %r", url)
request.redis.incr(timeout_tracker)
@@ -365,8 +419,8 @@ def tile_proxy(request):
status_code = exc.response.status_code
return Response(f"Failed to get tile from {provider}", status_code=status_code)
else:
- request.redis.set(cache_key, resp.content, ex=TTL)
- return Response(resp.content, content_type=resp.headers.get("Content-type", content_type))
+ request.redis.set(cache_key, resp, ex=TTL)
+ return Response(resp, content_type=resp.headers.get("Content-type", content_type))
def sources_for(request: Request) -> list[TileLayerConfig]: