diff --git a/CHANGELOG.md b/CHANGELOG.md index 703d836..ee52cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.4.0] +### Added +- Add policy option to Django settings +### Changed +- Update core API + + ## [0.3.3] ### Added - Clock-PRO policy diff --git a/README.md b/README.md index 9015e0b..77af923 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ CACHES = { "default": { "BACKEND": "theine.adapters.django.Cache", "TIMEOUT": 300, - "OPTIONS": {"MAX_ENTRIES": 10000}, + "OPTIONS": {"MAX_ENTRIES": 10000, "POLICY": "tlfu"}, }, } ``` diff --git a/benchmarks/benchmark_test.py b/benchmarks/benchmark_test.py index 5e28250..906cd6c 100644 --- a/benchmarks/benchmark_test.py +++ b/benchmarks/benchmark_test.py @@ -3,11 +3,11 @@ import uuid from typing import List -import pytest import cacheout -from cachetools import LFUCache, cached, LRUCache - +import pytest from bounded_zipf import Zipf +from cachetools import LFUCache, LRUCache, cached + from theine.thenie import Cache, Memoize REQUESTS = 10000 diff --git a/benchmarks/trace_bench.py b/benchmarks/trace_bench.py index 3a6d382..9f15cb4 100644 --- a/benchmarks/trace_bench.py +++ b/benchmarks/trace_bench.py @@ -1,14 +1,14 @@ import csv -from time import sleep -import matplotlib.pyplot as plt from datetime import timedelta from functools import lru_cache from random import randint +from time import sleep from typing import Callable, Iterable from unittest.mock import Mock -from cachetools import LFUCache, cached +import matplotlib.pyplot as plt from bounded_zipf import Zipf +from cachetools import LFUCache, cached from theine import Cache, Memoize diff --git a/pyproject.toml b/pyproject.toml index df7c21b..6f6d9ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "theine" -version = "0.3.3" +version = "0.4.0" description = "high performance in-memory cache" authors = ["Yiling-J "] readme = "README.md" @@ -8,7 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.7" typing-extensions = "^4.4.0" -theine-core = "^0.3.3" +theine-core = "^0.4.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.1" diff --git a/tests/adapters/settings/theine.py b/tests/adapters/settings/theine.py index 705ac81..55318ee 100644 --- a/tests/adapters/settings/theine.py +++ b/tests/adapters/settings/theine.py @@ -3,7 +3,7 @@ "default": { "BACKEND": "theine.adapters.django.Cache", "TIMEOUT": 60, - "OPTIONS": {"MAX_ENTRIES": 1000}, + "OPTIONS": {"MAX_ENTRIES": 1000, "POLICY": "tlfu"}, }, } diff --git a/tests/test_cache.py b/tests/test_cache.py index 8274cd4..ae2cd7a 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -1,8 +1,9 @@ -import pytest from datetime import timedelta from random import randint from time import sleep +import pytest + from theine.thenie import Cache, sentinel @@ -46,7 +47,7 @@ def test_set_with_ttl(policy): cache = Cache(policy, 500) for i in range(30): key = f"key:{i}" - cache.set(key, key, timedelta(seconds=i)) + cache.set(key, key, timedelta(seconds=i + 1)) key = f"key:{i}:2" cache.set(key, key, timedelta(seconds=i + 100)) assert len(cache) == 60 @@ -100,7 +101,7 @@ def test_set_with_ttl_hashable(policy): cache = Cache(policy, 500) foos = [Foo(i) for i in range(30)] for i in range(30): - cache.set(foos[i], foos[i], timedelta(seconds=i)) + cache.set(foos[i], foos[i], timedelta(seconds=i + 1)) assert len(cache) == 30 assert cache.key_gen.len() == 30 current = 30 diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 63b08cd..72abf66 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -8,8 +8,8 @@ from unittest.mock import Mock import pytest - from bounded_zipf import Zipf + from theine import Cache, Memoize diff --git a/theine/adapters/django.py b/theine/adapters/django.py index 1e70eba..71ef1a8 100644 --- a/theine/adapters/django.py +++ b/theine/adapters/django.py @@ -1,7 +1,6 @@ -import time from datetime import timedelta from threading import Lock -from typing import DefaultDict, Optional, cast +from typing import Optional, cast from django.core.cache.backends.base import DEFAULT_TIMEOUT, BaseCache @@ -12,7 +11,9 @@ class Cache(BaseCache): def __init__(self, name, params): super().__init__(params) - self.cache = Theine("tlfu", self._max_entries) + options = params.get("OPTIONS", {}) + policy = options.get("POLICY", "tlfu") + self.cache = Theine(policy, self._max_entries) def _timeout_seconds(self, timeout) -> Optional[float]: if timeout == DEFAULT_TIMEOUT: diff --git a/theine/exceptions.py b/theine/exceptions.py new file mode 100644 index 0000000..ea96006 --- /dev/null +++ b/theine/exceptions.py @@ -0,0 +1,2 @@ +class InvalidTTL(Exception): + pass diff --git a/theine/thenie.py b/theine/thenie.py index bc2f785..02fbd77 100644 --- a/theine/thenie.py +++ b/theine/thenie.py @@ -6,24 +6,13 @@ from datetime import timedelta from functools import _make_key, update_wrapper from threading import Event, Thread -from typing import ( - Any, - Callable, - Dict, - Hashable, - List, - Optional, - Tuple, - TypeVar, - Union, - cast, - Type, -) - - -from theine_core import LruCore, TlfuCore, ClockProCore +from typing import (Any, Callable, Dict, Hashable, List, Optional, Tuple, Type, + TypeVar, Union, cast) + +from theine_core import ClockProCore, LruCore, TlfuCore from typing_extensions import ParamSpec, Protocol +from theine.exceptions import InvalidTTL sentinel = object() @@ -60,7 +49,7 @@ class Core(Protocol): def __init__(self, size: int): ... - def set(self, key: str, expire: int) -> Tuple[int, Optional[int], Optional[str]]: + def set(self, key: str, ttl: int) -> Tuple[int, Optional[int], Optional[str]]: ... def remove(self, key: str) -> Optional[int]: @@ -69,7 +58,7 @@ def remove(self, key: str) -> Optional[int]: def access(self, key: str) -> Optional[int]: ... - def advance(self, now: int, cache: List, sentinel: Any, kh: Dict, hk: Dict): + def advance(self, cache: List, sentinel: Any, kh: Dict, hk: Dict): ... def clear(self): @@ -84,7 +73,7 @@ def __init__(self, size: int): ... def set( - self, key: str, expire: int + self, key: str, ttl: int ) -> Tuple[int, Optional[int], Optional[int], Optional[str]]: ... @@ -94,7 +83,7 @@ def remove(self, key: str) -> Optional[int]: def access(self, key: str) -> Optional[int]: ... - def advance(self, now: int, cache: List, sentinel: Any, kh: Dict, hk: Dict): + def advance(self, cache: List, sentinel: Any, kh: Dict, hk: Dict): ... def clear(self): @@ -314,11 +303,13 @@ def _access(self, key: Hashable, ttl: Optional[timedelta] = None): else: key_str = self.key_gen(key) - expire = None + ttl_ns = None if ttl is not None: - now = time.time() - expire = now + max(ttl.total_seconds(), 1.0) - self.core.set(key_str, int(expire * 1e9) if expire is not None else 0) + seconds = ttl.total_seconds() + if seconds == 0: + raise Exception("ttl must be positive") + ttl_ns = int(seconds * 1e9) + self.core.set(key_str, ttl_ns or 0) def set( self, key: Hashable, value: Any, ttl: Optional[timedelta] = None @@ -339,13 +330,14 @@ def set( else: key_str = self.key_gen(key) - expire = None + ttl_ns = None if ttl is not None: - now = time.time() - expire = now + max(ttl.total_seconds(), 1.0) - index, evicted_index, evicted_key = self.core.set( - key_str, int(expire * 1e9) if expire is not None else 0 - ) + seconds = ttl.total_seconds() + if seconds <= 0: + raise InvalidTTL("ttl must be positive") + ttl_ns = int(seconds * 1e9) + # 0 means no ttl + index, evicted_index, evicted_key = self.core.set(key_str, ttl_ns or 0) self._cache[index] = value if evicted_index is not None: self._cache[evicted_index] = sentinel @@ -374,12 +366,15 @@ def _set_clockpro( else: key_str = self.key_gen(key) - expire = None + ttl_ns = None if ttl is not None: - now = time.time() - expire = now + max(ttl.total_seconds(), 1.0) + # min res 1 second + seconds = ttl.total_seconds() + if seconds <= 0: + raise InvalidTTL("ttl must be positive") + ttl_ns = int(seconds * 1e9) index, test_index, evicted_index, evicted_key = self.core.set( - key_str, int(expire * 1e9) if expire is not None else 0 + key_str, ttl_ns or 0 ) self._cache[index] = value if test_index is not None: @@ -417,9 +412,7 @@ def maintenance(self): Remove expired keys. """ while True: - self.core.advance( - time.time_ns(), self._cache, sentinel, self.key_gen.kh, self.key_gen.hk - ) + self.core.advance(self._cache, sentinel, self.key_gen.kh, self.key_gen.hk) time.sleep(0.5) def clear(self):