mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Create a really good async cache
This commit is contained in:
parent
b552ca7c51
commit
5f787bedfe
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.10
|
FROM python:3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY ./requirements.txt .
|
COPY ./requirements.txt .
|
||||||
|
119
scanner/scanner/cache.py
Normal file
119
scanner/scanner/cache.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import asyncio
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Optional, Tuple
|
||||||
|
from providers.utils import ProviderError
|
||||||
|
|
||||||
|
type Cache = dict[Any, Tuple[Optional[asyncio.Event], Optional[datetime], Any]]
|
||||||
|
|
||||||
|
def cache(ttl: timedelta, cache: Cache={}, typed=False):
|
||||||
|
"""
|
||||||
|
A cache decorator for async methods. If the same method is called twice with
|
||||||
|
the same args, the underlying method will only be called once and the first
|
||||||
|
result will be cached.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
typed: same as functools.lru_cache
|
||||||
|
ttl: how many time should the cached value be considered valid?
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def wrap(f):
|
||||||
|
@wraps(f)
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
key = make_key(args, kwargs, typed)
|
||||||
|
|
||||||
|
ret = cache.get(key, (None, None, None))
|
||||||
|
# First check if the same method is already running and wait for it.
|
||||||
|
if ret[0] is not None:
|
||||||
|
await ret[0].wait()
|
||||||
|
ret = cache.get(key, (None, None, None))
|
||||||
|
if ret[2] is None:
|
||||||
|
# ret[2] can be None if the cached method failed. if that is the case, run again.
|
||||||
|
return wrapper(*args, **kwargs)
|
||||||
|
return ret[2]
|
||||||
|
# Return the cached result if it exits and is not expired
|
||||||
|
if (
|
||||||
|
ret[2] is not None
|
||||||
|
and ret[1] is not None
|
||||||
|
and ret[1] - datetime.now() < ttl
|
||||||
|
):
|
||||||
|
return ret[2]
|
||||||
|
|
||||||
|
return await exec_as_cache(cache, key, lambda: f(*args, **kwargs))
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
async def exec_as_cache(cache: Cache, key, f):
|
||||||
|
event = asyncio.Event()
|
||||||
|
cache[key] = (event, None, None)
|
||||||
|
try:
|
||||||
|
result = await f()
|
||||||
|
except:
|
||||||
|
del cache[key]
|
||||||
|
event.set()
|
||||||
|
raise
|
||||||
|
|
||||||
|
event.set()
|
||||||
|
cache[key] = (None, datetime.now(), result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Code bellow was stolen from https://github.com/python/cpython/blob/3.12/Lib/functools.py#L432
|
||||||
|
|
||||||
|
|
||||||
|
class _HashedSeq(list):
|
||||||
|
"""This class guarantees that hash() will be called no more than once
|
||||||
|
per element. This is important because the lru_cache() will hash
|
||||||
|
the key multiple times on a cache miss.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = "hashvalue"
|
||||||
|
|
||||||
|
def __init__(self, tup, hash=hash):
|
||||||
|
self[:] = tup
|
||||||
|
self.hashvalue = hash(tup)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return self.hashvalue
|
||||||
|
|
||||||
|
|
||||||
|
def make_key(
|
||||||
|
args,
|
||||||
|
kwds={},
|
||||||
|
typed=False,
|
||||||
|
kwd_mark=(object(),),
|
||||||
|
fasttypes={int, str},
|
||||||
|
tuple=tuple,
|
||||||
|
type=type,
|
||||||
|
len=len,
|
||||||
|
):
|
||||||
|
"""Make a cache key from optionally typed positional and keyword arguments
|
||||||
|
|
||||||
|
The key is constructed in a way that is flat as possible rather than
|
||||||
|
as a nested structure that would take more memory.
|
||||||
|
|
||||||
|
If there is only a single argument and its data type is known to cache
|
||||||
|
its hash value, then that argument is returned without a wrapper. This
|
||||||
|
saves space and improves lookup speed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# All of code below relies on kwds preserving the order input by the user.
|
||||||
|
# Formerly, we sorted() the kwds before looping. The new way is *much*
|
||||||
|
# faster; however, it means that f(x=1, y=2) will now be treated as a
|
||||||
|
# distinct call from f(y=2, x=1) which will be cached separately.
|
||||||
|
key = args
|
||||||
|
if kwds:
|
||||||
|
key += kwd_mark
|
||||||
|
for item in kwds.items():
|
||||||
|
key += item
|
||||||
|
if typed:
|
||||||
|
key += tuple(type(v) for v in args)
|
||||||
|
if kwds:
|
||||||
|
key += tuple(type(v) for v in kwds.values())
|
||||||
|
elif len(key) == 1 and type(key[0]) in fasttypes:
|
||||||
|
return key[0]
|
||||||
|
return _HashedSeq(key)
|
@ -6,6 +6,7 @@
|
|||||||
sdk_7_0
|
sdk_7_0
|
||||||
aspnetcore_7_0
|
aspnetcore_7_0
|
||||||
];
|
];
|
||||||
|
python = pkgs.python312;
|
||||||
in
|
in
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
@ -14,8 +15,8 @@ in
|
|||||||
nodePackages.eas-cli
|
nodePackages.eas-cli
|
||||||
nodePackages.expo-cli
|
nodePackages.expo-cli
|
||||||
dotnet
|
dotnet
|
||||||
python3
|
python
|
||||||
python3Packages.pip
|
python312Packages.pip
|
||||||
cargo
|
cargo
|
||||||
cargo-watch
|
cargo-watch
|
||||||
rustfmt
|
rustfmt
|
||||||
@ -37,7 +38,7 @@ in
|
|||||||
# Install python modules
|
# Install python modules
|
||||||
SOURCE_DATE_EPOCH=$(date +%s)
|
SOURCE_DATE_EPOCH=$(date +%s)
|
||||||
if [ ! -d "${venvDir}" ]; then
|
if [ ! -d "${venvDir}" ]; then
|
||||||
${pkgs.python3}/bin/python3 -m venv ${toString ./.}/${venvDir}
|
${python}/bin/python3 -m venv ${toString ./.}/${venvDir}
|
||||||
source ${venvDir}/bin/activate
|
source ${venvDir}/bin/activate
|
||||||
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
export PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
pip install -r ${pythonPkgs} >&2
|
pip install -r ${pythonPkgs} >&2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user