diff --git a/pyproject.toml b/pyproject.toml index a007460..605ebde 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "pygeofilter>=0.2.0,<0.3.0", "ciso8601~=2.3", "starlette-cramjam>=0.4,<0.5", + "aiocache", ] [project.optional-dependencies] diff --git a/tests/conftest.py b/tests/conftest.py index 951318e..9e2417e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,9 @@ import os from contextlib import asynccontextmanager +from typing import Any, Dict +import aiocache import psycopg import pytest from pytest_postgresql.janitor import DatabaseJanitor @@ -101,6 +103,17 @@ def database_url(database): return db_url +def _setup_cache(): + config: Dict[str, Any] = { + "cache": "aiocache.SimpleMemoryCache", + "serializer": { + "class": "aiocache.serializers.PickleSerializer", + }, + "ttl": 100, + } + aiocache.caches.set_config({"default": config}) + + def create_tipg_app( postgres_settings: PostgresSettings, db_settings: DatabaseSettings, @@ -134,6 +147,7 @@ async def lifespan(app: FastAPI): spatial_extent=db_settings.spatial_extent, datetime_extent=db_settings.datetime_extent, ) + _setup_cache() yield await close_db_connection(app) @@ -175,6 +189,7 @@ def app(database_url, monkeypatch): monkeypatch.setenv("TIPG_DEFAULT_MINZOOM", str(5)) monkeypatch.setenv("TIPG_DEFAULT_MAXZOOM", str(12)) + monkeypatch.setenv("TIPG_CACHE_DISABLE", "TRUE") monkeypatch.setenv("TIPG_DEBUG", "TRUE") from tipg.main import app, db_settings, postgres_settings diff --git a/tipg/collections.py b/tipg/collections.py index 4592cd7..1caf6a2 100644 --- a/tipg/collections.py +++ b/tipg/collections.py @@ -1,10 +1,13 @@ """tipg.dbmodel: database events.""" import datetime +import hashlib +import json import re from functools import lru_cache from typing import Any, Dict, List, Optional, Tuple, TypedDict, Union +from aiocache import cached from buildpg import RawDangerous as raw from buildpg import asyncpg, clauses from buildpg import funcs as pg_funcs @@ -794,6 +797,14 @@ async def features( prev=max(offset - limit, 0) if offset else None, ) + @cached( + key_builder=lambda _f, + self, + pool, + tms, + tile, + **kwargs: f"{self.id}-{tms.id}-{tile.x}-{tile.y}-{tile.z}-{hashlib.md5(json.dumps(kwargs, sort_keys=True).encode('utf-8')).hexdigest()}", + ) async def get_tile( self, *, @@ -813,6 +824,7 @@ async def get_tile( limit: Optional[int] = None, ): """Build query to get Vector Tile.""" + print(f"{tile.x}-{tile.y}-{tile.z}") limit = limit or mvt_settings.max_features_per_tile geometry_column = self.get_geometry_column(geom) diff --git a/tipg/factory.py b/tipg/factory.py index 1845787..c2022c7 100644 --- a/tipg/factory.py +++ b/tipg/factory.py @@ -45,7 +45,7 @@ from tipg.errors import MissingGeometryColumn, NoPrimaryKey, NotFound from tipg.resources.enums import MediaType from tipg.resources.response import GeoJSONResponse, SchemaJSONResponse, orjsonDumps -from tipg.settings import FeaturesSettings, MVTSettings, TMSSettings +from tipg.settings import CacheSettings, FeaturesSettings, MVTSettings, TMSSettings from fastapi import APIRouter, Depends, Path, Query from fastapi.responses import ORJSONResponse @@ -59,6 +59,7 @@ tms_settings = TMSSettings() mvt_settings = MVTSettings() features_settings = FeaturesSettings() +cache_settings = CacheSettings() jinja2_env = jinja2.Environment( @@ -1604,6 +1605,8 @@ async def collection_get_tile( limit=limit, geom=geom_column, dt=datetime_column, + cache_write=cache_settings.disable is False, + cache_read=cache_settings.disable is False, ) return Response(bytes(tile), media_type=MediaType.mvt.value) diff --git a/tipg/main.py b/tipg/main.py index a98186e..8f88717 100644 --- a/tipg/main.py +++ b/tipg/main.py @@ -1,8 +1,9 @@ """tipg app.""" from contextlib import asynccontextmanager -from typing import Any, List +from typing import Any, Dict, List +import aiocache import jinja2 from tipg import __version__ as tipg_version @@ -13,6 +14,7 @@ from tipg.middleware import CacheControlMiddleware, CatalogUpdateMiddleware from tipg.settings import ( APISettings, + CacheSettings, CustomSQLSettings, DatabaseSettings, PostgresSettings, @@ -28,6 +30,21 @@ postgres_settings = PostgresSettings() db_settings = DatabaseSettings() custom_sql_settings = CustomSQLSettings() +cache_settings = CacheSettings() + + +def setup_cache(): + """Setup aiocache.""" + config: Dict[str, Any] = { + "cache": "aiocache.SimpleMemoryCache", + "serializer": { + "class": "aiocache.serializers.PickleSerializer", + }, + } + if cache_settings.ttl is not None: + config["ttl"] = cache_settings.ttl + + aiocache.caches.set_config({"default": config}) @asynccontextmanager @@ -56,6 +73,8 @@ async def lifespan(app: FastAPI): datetime_extent=db_settings.datetime_extent, ) + setup_cache() + yield # Close the Connection Pool diff --git a/tipg/settings.py b/tipg/settings.py index f57855b..c255424 100644 --- a/tipg/settings.py +++ b/tipg/settings.py @@ -192,3 +192,19 @@ def sql_files(self) -> Optional[List[pathlib.Path]]: return list(self.custom_sql_directory.glob("*.sql")) return None + + +class CacheSettings(BaseSettings): + """Cache settings""" + + # TTL of the cache in seconds + ttl: int = 300 + + # Whether or not caching is enabled + disable: bool = False + + model_config = { + "env_prefix": "TIPG_CACHE_", + "env_file": ".env", + "extra": "ignore", + }