Skip to content

Commit

Permalink
Use last-modified headers when serving existing tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
pnorman committed Mar 15, 2024
1 parent c7d97fd commit b1932e8
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 16 deletions.
36 changes: 26 additions & 10 deletions tilekiln/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from tilekiln.tileset import Tileset
from tilekiln.storage import Storage

HTTP_TIME = "%a, %d %b %Y %H:%M:%S GMT"

# Constants for MVTs
MVT_MIME_TYPE = "application/vnd.mapbox-vector-tile"

Expand Down Expand Up @@ -129,9 +131,20 @@ def serve_tile(prefix: str, zoom: int, x: int, y: int):
if prefix not in tilesets:
raise HTTPException(status_code=404, detail=f"Tileset {prefix} not found on server.")

return Response(tilesets[prefix].get_tile(Tile(zoom, x, y)),
media_type=MVT_MIME_TYPE,
headers=STANDARD_HEADERS)
tile, generated = tilesets[prefix].get_tile(Tile(zoom, x, y))

if tile is None:
raise HTTPException(status_code=404,
detail=f"Tile {prefix}/{zoom}/{x}/{y} not found in storage.")

# We use the generated timestamp on the assumption that a specific
# x/y/z will not be generated twice in the same ms.
headers: dict[str, str] = {}
if generated is not None:
headers = {"Last-Modified": generated.strftime(HTTP_TIME),
"E-tag": generated.strftime("%s.%f")}
return Response(tile, media_type=MVT_MIME_TYPE,
headers=STANDARD_HEADERS | headers)


@live.head("/{prefix}/{zoom}/{x}/{y}.mvt")
Expand All @@ -142,20 +155,23 @@ def live_serve_tile(prefix: str, zoom: int, x: int, y: int):
raise HTTPException(status_code=404, detail=f"Tileset {prefix} not found on server.")

# Attempt to serve a stored tile
existing = tilesets[prefix].get_tile(Tile(zoom, x, y))
existing, generated = tilesets[prefix].get_tile(Tile(zoom, x, y))

# Handle storage hits
if existing is not None:
return Response(existing,
media_type=MVT_MIME_TYPE,
headers=STANDARD_HEADERS)
headers: dict[str, str] = {}
if generated is not None:
headers = {"Last-Modified": generated.strftime(HTTP_TIME),
"E-tag": generated.strftime("%s.%f")}
return Response(existing, media_type=MVT_MIME_TYPE,
headers=STANDARD_HEADERS | headers)

# Storage miss, so generate a new tile
global kiln
tile = Tile(zoom, x, y)
generated = kiln.render(tile)
response = kiln.render(tile)
# TODO: Make async so tile is saved and response returned in parallel
tilesets[prefix].save_tile(tile, generated)
return Response(generated,
tilesets[prefix].save_tile(tile, response)
return Response(response,
media_type=MVT_MIME_TYPE,
headers=STANDARD_HEADERS)
15 changes: 10 additions & 5 deletions tilekiln/storage.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import datetime
import gzip
import json
import sys
Expand Down Expand Up @@ -197,17 +198,19 @@ def truncate_tables(self, id: str, zooms=None):
self.__truncate_table(cur, id, zoom)
conn.commit()

def get_tile(self, id: str, tile: Tile) -> bytes | None:
def get_tile(self, id: str, tile: Tile) -> tuple[bytes | None, datetime.datetime | None]:
with self.__pool.connection() as conn:
with conn.cursor() as cur:
cur.execute(f'''SELECT tile FROM "{self.__schema}"."{id}"
conn.execute("SET TIMEZONE TO 'GMT'")
with conn.cursor(row_factory=psycopg.rows.dict_row) as cur:
cur.execute(f'''SELECT generated, tile FROM "{self.__schema}"."{id}"
WHERE zoom = %s AND x = %s AND y = %s''',
(tile.zoom, tile.x, tile.y), binary=True)
result = cur.fetchone()
if result is None:
return None
return gzip.decompress(result[0])
return None, None
return gzip.decompress(result["tile"]), result["generated"]

# TODO: Needs to return timestamp written to the DB
def save_tile(self, id: str, tile: Tile, tiledata: bytes, render_time=0):
with self.__pool.connection() as conn:
with conn.cursor() as cur:
Expand Down Expand Up @@ -372,6 +375,8 @@ def __delete_tile(self, cur, id: str, tile: Tile):
WHERE zoom = %s AND x = %s AND y = %s''',
(tile.zoom, tile.x, tile.y))

# TODO: The statement should compare to the existing value and if they are the same not
# update in order to keep the last-modified header the same and improve caching.
def __write_to_storage(self, id, tile: Tile, tiledata, cur):
tablename = f"{id}_z{tile.zoom}"
cur.execute(f'''INSERT INTO "{self.__schema}"."{tablename}" (zoom, x, y, tile)
Expand Down
3 changes: 2 additions & 1 deletion tilekiln/tileset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
import datetime

from tilekiln.config import Config
from tilekiln.tile import Tile
Expand Down Expand Up @@ -50,7 +51,7 @@ def update_storage_metadata(self) -> None:
self.storage.set_metadata(self.id, self.minzoom, self.maxzoom,
self.tilejson)

def get_tile(self, tile: Tile) -> bytes | None:
def get_tile(self, tile: Tile) -> tuple[bytes | None, datetime.datetime | None]:
return self.storage.get_tile(self.id, tile)

def save_tile(self, tile: Tile, data: bytes) -> None:
Expand Down

0 comments on commit b1932e8

Please sign in to comment.