client-query-cache 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- client_query_cache/__init__.py +15 -0
- client_query_cache/_core/__init__.py +44 -0
- client_query_cache/_core/canonical.py +63 -0
- client_query_cache/_core/codec.py +55 -0
- client_query_cache/_core/collation.py +22 -0
- client_query_cache/_core/collection_metadata.py +65 -0
- client_query_cache/_core/entries.py +32 -0
- client_query_cache/_core/errors.py +25 -0
- client_query_cache/_core/identity_reads.py +64 -0
- client_query_cache/_core/keys.py +43 -0
- client_query_cache/_core/lifecycle.py +8 -0
- client_query_cache/_core/locking.py +40 -0
- client_query_cache/_core/lru.py +109 -0
- client_query_cache/_core/manager.py +892 -0
- client_query_cache/_core/namespace.py +35 -0
- client_query_cache/_core/order_sensitive_keys.py +69 -0
- client_query_cache/_core/projection.py +59 -0
- client_query_cache/_core/read_validation.py +89 -0
- client_query_cache/_core/snapshots.py +67 -0
- client_query_cache/_core/stream_cost.py +242 -0
- client_query_cache/_core/stream_events.py +157 -0
- client_query_cache/_core/stream_health.py +47 -0
- client_query_cache/_core/stream_options.py +14 -0
- client_query_cache/_core/unique_keys.py +119 -0
- client_query_cache/asynchronous/__init__.py +16 -0
- client_query_cache/asynchronous/collection.py +682 -0
- client_query_cache/asynchronous/database.py +59 -0
- client_query_cache/asynchronous/manager.py +118 -0
- client_query_cache/asynchronous/streams.py +300 -0
- client_query_cache/otel.py +206 -0
- client_query_cache/py.typed +0 -0
- client_query_cache/synchronous/__init__.py +16 -0
- client_query_cache/synchronous/collection.py +678 -0
- client_query_cache/synchronous/database.py +53 -0
- client_query_cache/synchronous/manager.py +118 -0
- client_query_cache/synchronous/streams.py +297 -0
- client_query_cache-0.1.0.dist-info/METADATA +114 -0
- client_query_cache-0.1.0.dist-info/RECORD +39 -0
- client_query_cache-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from client_query_cache._core.entries import AdmissionOutcome, LookupResult
|
|
2
|
+
from client_query_cache._core.errors import (
|
|
3
|
+
CacheClosedError,
|
|
4
|
+
CacheConfigurationError,
|
|
5
|
+
CacheError,
|
|
6
|
+
StreamLifecycleError,
|
|
7
|
+
StreamStartupError,
|
|
8
|
+
UnsupportedCacheRequestError,
|
|
9
|
+
)
|
|
10
|
+
from client_query_cache._core.keys import NamespaceId
|
|
11
|
+
from client_query_cache._core.lifecycle import CacheLifecycleState
|
|
12
|
+
from client_query_cache._core.manager import (
|
|
13
|
+
CacheCore,
|
|
14
|
+
CacheCoreConfig,
|
|
15
|
+
IdentityCapture,
|
|
16
|
+
NamespaceCapture,
|
|
17
|
+
)
|
|
18
|
+
from client_query_cache._core.snapshots import CacheSnapshot
|
|
19
|
+
from client_query_cache._core.stream_cost import (
|
|
20
|
+
LagCaptureWindowConfig,
|
|
21
|
+
StreamCostSnapshot,
|
|
22
|
+
)
|
|
23
|
+
from client_query_cache._core.stream_health import StreamHealth
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AdmissionOutcome",
|
|
27
|
+
"CacheClosedError",
|
|
28
|
+
"CacheConfigurationError",
|
|
29
|
+
"CacheCore",
|
|
30
|
+
"CacheCoreConfig",
|
|
31
|
+
"CacheError",
|
|
32
|
+
"CacheLifecycleState",
|
|
33
|
+
"CacheSnapshot",
|
|
34
|
+
"IdentityCapture",
|
|
35
|
+
"LagCaptureWindowConfig",
|
|
36
|
+
"LookupResult",
|
|
37
|
+
"NamespaceCapture",
|
|
38
|
+
"NamespaceId",
|
|
39
|
+
"StreamCostSnapshot",
|
|
40
|
+
"StreamHealth",
|
|
41
|
+
"StreamLifecycleError",
|
|
42
|
+
"StreamStartupError",
|
|
43
|
+
"UnsupportedCacheRequestError",
|
|
44
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Hashable, Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from client_query_cache._core.errors import UnsupportedCacheRequestError
|
|
6
|
+
|
|
7
|
+
type Canonical = Hashable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _CanonicalTag:
|
|
11
|
+
__slots__ = ("_name",)
|
|
12
|
+
|
|
13
|
+
def __init__(self, name: str) -> None:
|
|
14
|
+
self._name = name
|
|
15
|
+
|
|
16
|
+
def __repr__(self) -> str:
|
|
17
|
+
return f"<canonical:{self._name}>"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_MAPPING_TAG = _CanonicalTag("map")
|
|
21
|
+
_SEQUENCE_TAG = _CanonicalTag("seq")
|
|
22
|
+
_BOOL_TAG = _CanonicalTag("bool")
|
|
23
|
+
_OWN_TAGS = (_MAPPING_TAG, _SEQUENCE_TAG, _BOOL_TAG)
|
|
24
|
+
_TAGGED_TUPLE_SIZE = 2
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def canonicalize(value: object) -> Canonical:
|
|
28
|
+
if (
|
|
29
|
+
isinstance(value, tuple)
|
|
30
|
+
and len(value) == _TAGGED_TUPLE_SIZE
|
|
31
|
+
and any(value[0] is tag for tag in _OWN_TAGS)
|
|
32
|
+
):
|
|
33
|
+
return value
|
|
34
|
+
if isinstance(value, Mapping):
|
|
35
|
+
pairs = [(canonicalize(key), canonicalize(item)) for key, item in value.items()]
|
|
36
|
+
items = tuple(sorted(pairs, key=lambda pair: repr(pair[0])))
|
|
37
|
+
return (_MAPPING_TAG, items)
|
|
38
|
+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
|
|
39
|
+
return (_SEQUENCE_TAG, tuple(canonicalize(item) for item in value))
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return (_BOOL_TAG, value)
|
|
42
|
+
try:
|
|
43
|
+
hash(value)
|
|
44
|
+
except TypeError:
|
|
45
|
+
type_name = type(value).__name__
|
|
46
|
+
message = f"cannot canonicalize value of type {type_name!r} for a cache key"
|
|
47
|
+
raise UnsupportedCacheRequestError(message) from None
|
|
48
|
+
if value != value: # noqa: PLR0124 (deliberate self-inequality check for NaN-like values)
|
|
49
|
+
type_name = type(value).__name__
|
|
50
|
+
message = (
|
|
51
|
+
f"cannot canonicalize a non-reflexive value of type {type_name!r} "
|
|
52
|
+
"for a cache key"
|
|
53
|
+
)
|
|
54
|
+
raise UnsupportedCacheRequestError(message)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_canonicalizable(value: object) -> bool:
|
|
59
|
+
try:
|
|
60
|
+
canonicalize(value)
|
|
61
|
+
except UnsupportedCacheRequestError:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
import bson
|
|
6
|
+
from bson.codec_options import CodecOptions
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
|
|
11
|
+
from bson.codec_options import TypeRegistry
|
|
12
|
+
|
|
13
|
+
_ENVELOPE_FIELD = "v"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _TypeRegistryIdentity:
|
|
17
|
+
__slots__ = ("_registry",)
|
|
18
|
+
|
|
19
|
+
def __init__(self, registry: TypeRegistry) -> None:
|
|
20
|
+
self._registry = registry
|
|
21
|
+
|
|
22
|
+
def __eq__(self, other: object) -> bool:
|
|
23
|
+
return (
|
|
24
|
+
isinstance(other, _TypeRegistryIdentity)
|
|
25
|
+
and self._registry is other._registry
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __hash__(self) -> int:
|
|
29
|
+
return id(self._registry)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def encode_value(
|
|
33
|
+
value: object, codec_options: CodecOptions[Mapping[str, Any]] | None = None
|
|
34
|
+
) -> bytes:
|
|
35
|
+
return bson.encode(
|
|
36
|
+
{_ENVELOPE_FIELD: value}, codec_options=codec_options or CodecOptions()
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def decode_value(
|
|
41
|
+
encoded: bytes, codec_options: CodecOptions[Mapping[str, Any]] | None = None
|
|
42
|
+
) -> object:
|
|
43
|
+
return bson.decode(encoded, codec_options=codec_options)[_ENVELOPE_FIELD]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def codec_fingerprint(codec_options: CodecOptions[Mapping[str, Any]]) -> object:
|
|
47
|
+
return (
|
|
48
|
+
codec_options.document_class,
|
|
49
|
+
codec_options.tz_aware,
|
|
50
|
+
codec_options.uuid_representation,
|
|
51
|
+
codec_options.unicode_decode_error_handler,
|
|
52
|
+
codec_options.tzinfo,
|
|
53
|
+
codec_options.datetime_conversion,
|
|
54
|
+
_TypeRegistryIdentity(codec_options.type_registry),
|
|
55
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
_SIMPLE_LOCALE = "simple"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def normalize_collation(
|
|
12
|
+
collation: Mapping[str, Any] | None,
|
|
13
|
+
) -> Mapping[str, Any] | None:
|
|
14
|
+
if collation is None:
|
|
15
|
+
return None
|
|
16
|
+
try:
|
|
17
|
+
locale = collation["locale"]
|
|
18
|
+
except KeyError:
|
|
19
|
+
locale = None
|
|
20
|
+
if locale == _SIMPLE_LOCALE:
|
|
21
|
+
return None
|
|
22
|
+
return collation
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from client_query_cache._core.collation import normalize_collation
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
|
|
12
|
+
from client_query_cache._core.keys import NamespaceId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class CollectionMetadata:
|
|
17
|
+
checked_epoch: int
|
|
18
|
+
is_cacheable: bool
|
|
19
|
+
default_collation: Mapping[str, Any] | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CollectionMetadataCache:
|
|
23
|
+
__slots__ = ("_entries", "_lock")
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._entries: dict[NamespaceId, CollectionMetadata] = {}
|
|
27
|
+
self._lock = threading.Lock()
|
|
28
|
+
|
|
29
|
+
def get(self, namespace: NamespaceId) -> CollectionMetadata | None:
|
|
30
|
+
with self._lock:
|
|
31
|
+
try:
|
|
32
|
+
return self._entries[namespace]
|
|
33
|
+
except KeyError:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def put(self, namespace: NamespaceId, metadata: CollectionMetadata) -> None:
|
|
37
|
+
with self._lock:
|
|
38
|
+
self._entries[namespace] = metadata
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class CollectionProbeResult:
|
|
43
|
+
is_cacheable: bool
|
|
44
|
+
default_collation: Mapping[str, Any] | None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def interpret_list_collections_entry(
|
|
48
|
+
entry: Mapping[str, Any] | None,
|
|
49
|
+
) -> CollectionProbeResult | None:
|
|
50
|
+
if entry is None:
|
|
51
|
+
return None
|
|
52
|
+
collection_type = entry["type"]
|
|
53
|
+
try:
|
|
54
|
+
options = entry["options"]
|
|
55
|
+
except KeyError:
|
|
56
|
+
options = {}
|
|
57
|
+
try:
|
|
58
|
+
collation = options["collation"]
|
|
59
|
+
except KeyError:
|
|
60
|
+
collation = None
|
|
61
|
+
default_collation = normalize_collation(collation)
|
|
62
|
+
return CollectionProbeResult(
|
|
63
|
+
is_cacheable=collection_type == "collection",
|
|
64
|
+
default_collation=default_collation,
|
|
65
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from client_query_cache._core.canonical import Canonical
|
|
9
|
+
from client_query_cache._core.keys import NamespaceId
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdmissionOutcome(enum.Enum):
|
|
13
|
+
ADMITTED = "admitted"
|
|
14
|
+
DECLINED_OVERSIZE = "declined_oversize"
|
|
15
|
+
DECLINED_STALE = "declined_stale"
|
|
16
|
+
DECLINED_UNAVAILABLE = "declined_unavailable"
|
|
17
|
+
DECLINED_UNENCODABLE = "declined_unencodable"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(eq=False, slots=True)
|
|
21
|
+
class CacheEntry:
|
|
22
|
+
generation_key: tuple[int, ...]
|
|
23
|
+
weight: int
|
|
24
|
+
value: bytes
|
|
25
|
+
namespace: NamespaceId
|
|
26
|
+
identity: Canonical | None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class LookupResult:
|
|
31
|
+
hit: bool
|
|
32
|
+
value: Any = None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CacheError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheConfigurationError(CacheError, ValueError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedCacheRequestError(CacheError, TypeError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CacheClosedError(CacheError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StreamStartupError(CacheError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StreamLifecycleError(CacheError):
|
|
25
|
+
pass
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from bson.errors import BSONError
|
|
8
|
+
from bson.regex import Regex
|
|
9
|
+
|
|
10
|
+
from client_query_cache._core.codec import decode_value, encode_value
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from bson.codec_options import CodecOptions
|
|
14
|
+
|
|
15
|
+
_ID_FIELD = "_id"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _NoIdentity:
|
|
19
|
+
__slots__ = ()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
NO_IDENTITY = _NoIdentity()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_query_operator_mapping(value: Mapping[str, Any]) -> bool:
|
|
26
|
+
return any(key.startswith("$") for key in value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _is_regex(value: object) -> bool:
|
|
30
|
+
return isinstance(value, re.Pattern | Regex)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_equality_value(value: object) -> object:
|
|
34
|
+
if value is None:
|
|
35
|
+
return NO_IDENTITY
|
|
36
|
+
if isinstance(value, Mapping) and _is_query_operator_mapping(value):
|
|
37
|
+
return NO_IDENTITY
|
|
38
|
+
if _is_regex(value):
|
|
39
|
+
return NO_IDENTITY
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_id_identity(filter_query: object) -> object:
|
|
44
|
+
if filter_query is None:
|
|
45
|
+
return NO_IDENTITY
|
|
46
|
+
if not isinstance(filter_query, Mapping):
|
|
47
|
+
if _is_regex(filter_query):
|
|
48
|
+
return NO_IDENTITY
|
|
49
|
+
return filter_query
|
|
50
|
+
if set(filter_query) != {_ID_FIELD}:
|
|
51
|
+
return NO_IDENTITY
|
|
52
|
+
return extract_equality_value(filter_query[_ID_FIELD])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_identity_for_cache_key(
|
|
56
|
+
identity: object,
|
|
57
|
+
read_codec_options: CodecOptions[Any],
|
|
58
|
+
stream_codec_options: CodecOptions[Any],
|
|
59
|
+
) -> object:
|
|
60
|
+
try:
|
|
61
|
+
encoded = encode_value(identity, read_codec_options)
|
|
62
|
+
return decode_value(encoded, stream_codec_options)
|
|
63
|
+
except BSONError:
|
|
64
|
+
return identity
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
5
|
+
|
|
6
|
+
from client_query_cache._core.canonical import canonicalize
|
|
7
|
+
from client_query_cache._core.order_sensitive_keys import order_sensitive_key
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from client_query_cache._core.canonical import Canonical
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NamespaceId(NamedTuple):
|
|
14
|
+
database: str
|
|
15
|
+
collection: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class IdentityCacheKey:
|
|
20
|
+
namespace: NamespaceId
|
|
21
|
+
identity: Canonical
|
|
22
|
+
read_shape: Canonical
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class NamespaceCacheKey:
|
|
27
|
+
namespace: NamespaceId
|
|
28
|
+
discriminator: Canonical
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
type CacheKey = IdentityCacheKey | NamespaceCacheKey
|
|
32
|
+
|
|
33
|
+
type AliasKey = tuple[Canonical, Canonical, Canonical]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def canonical_alias_key(
|
|
37
|
+
definition: object, value: object, collation: object
|
|
38
|
+
) -> AliasKey:
|
|
39
|
+
return (
|
|
40
|
+
canonicalize(definition),
|
|
41
|
+
canonicalize(order_sensitive_key(value)),
|
|
42
|
+
canonicalize(collation),
|
|
43
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LockOrderViolationError(RuntimeError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LockOrderGuard:
|
|
16
|
+
__slots__ = ("_state",)
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._state = threading.local()
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def namespace_section(self) -> Iterator[None]:
|
|
23
|
+
with self._section(entering="in_namespace", forbidden="in_lru"):
|
|
24
|
+
yield
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def lru_section(self) -> Iterator[None]:
|
|
28
|
+
with self._section(entering="in_lru", forbidden="in_namespace"):
|
|
29
|
+
yield
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def _section(self, *, entering: str, forbidden: str) -> Iterator[None]:
|
|
33
|
+
if getattr(self._state, forbidden, False):
|
|
34
|
+
message = f"cannot enter {entering!r} section while holding {forbidden!r}"
|
|
35
|
+
raise LockOrderViolationError(message)
|
|
36
|
+
setattr(self._state, entering, True)
|
|
37
|
+
try:
|
|
38
|
+
yield
|
|
39
|
+
finally:
|
|
40
|
+
setattr(self._state, entering, False)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from client_query_cache._core.entries import CacheEntry
|
|
9
|
+
from client_query_cache._core.keys import CacheKey
|
|
10
|
+
from client_query_cache._core.locking import LockOrderGuard
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WeightedLru:
|
|
14
|
+
__slots__ = (
|
|
15
|
+
"_guard",
|
|
16
|
+
"_lock",
|
|
17
|
+
"_max_entry_bytes",
|
|
18
|
+
"_order",
|
|
19
|
+
"_shared_budget_bytes",
|
|
20
|
+
"_used_bytes",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
shared_budget_bytes: int,
|
|
27
|
+
max_entry_bytes: int,
|
|
28
|
+
guard: LockOrderGuard,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._shared_budget_bytes = shared_budget_bytes
|
|
31
|
+
self._max_entry_bytes = max_entry_bytes
|
|
32
|
+
self._guard = guard
|
|
33
|
+
self._order: OrderedDict[CacheKey, CacheEntry] = OrderedDict()
|
|
34
|
+
self._used_bytes = 0
|
|
35
|
+
self._lock = threading.Lock()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def max_entry_bytes(self) -> int:
|
|
39
|
+
return self._max_entry_bytes
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def shared_budget_bytes(self) -> int:
|
|
43
|
+
return self._shared_budget_bytes
|
|
44
|
+
|
|
45
|
+
def is_oversize(self, weight: int) -> bool:
|
|
46
|
+
return weight > self._max_entry_bytes
|
|
47
|
+
|
|
48
|
+
def snapshot_usage(self) -> tuple[int, int]:
|
|
49
|
+
with self._guard.lru_section(), self._lock:
|
|
50
|
+
return self._used_bytes, len(self._order)
|
|
51
|
+
|
|
52
|
+
def peek(self, key: CacheKey) -> CacheEntry | None:
|
|
53
|
+
with self._guard.lru_section(), self._lock:
|
|
54
|
+
try:
|
|
55
|
+
return self._order[key]
|
|
56
|
+
except KeyError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def touch(self, key: CacheKey) -> None:
|
|
60
|
+
with self._guard.lru_section(), self._lock:
|
|
61
|
+
if key in self._order:
|
|
62
|
+
self._order.move_to_end(key)
|
|
63
|
+
|
|
64
|
+
def conditional_put(
|
|
65
|
+
self, key: CacheKey, entry: CacheEntry
|
|
66
|
+
) -> tuple[bool, CacheEntry | None, list[CacheEntry]]:
|
|
67
|
+
evicted: list[CacheEntry] = []
|
|
68
|
+
with self._guard.lru_section(), self._lock:
|
|
69
|
+
try:
|
|
70
|
+
current = self._order[key]
|
|
71
|
+
except KeyError:
|
|
72
|
+
current = None
|
|
73
|
+
if current is not None and current.generation_key >= entry.generation_key:
|
|
74
|
+
return False, None, evicted
|
|
75
|
+
if current is not None:
|
|
76
|
+
self._used_bytes -= current.weight
|
|
77
|
+
del self._order[key]
|
|
78
|
+
self._order[key] = entry
|
|
79
|
+
self._used_bytes += entry.weight
|
|
80
|
+
while self._used_bytes > self._shared_budget_bytes:
|
|
81
|
+
oldest_key, oldest_entry = next(iter(self._order.items()))
|
|
82
|
+
del self._order[oldest_key]
|
|
83
|
+
self._used_bytes -= oldest_entry.weight
|
|
84
|
+
evicted.append(oldest_entry)
|
|
85
|
+
return True, current, evicted
|
|
86
|
+
|
|
87
|
+
def contains_exact(self, key: CacheKey, entry: CacheEntry) -> bool:
|
|
88
|
+
with self._guard.lru_section(), self._lock:
|
|
89
|
+
try:
|
|
90
|
+
return self._order[key] is entry
|
|
91
|
+
except KeyError:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def remove_exact(self, key: CacheKey, entry: CacheEntry) -> bool:
|
|
95
|
+
with self._guard.lru_section(), self._lock:
|
|
96
|
+
try:
|
|
97
|
+
current = self._order[key]
|
|
98
|
+
except KeyError:
|
|
99
|
+
return False
|
|
100
|
+
if current is entry:
|
|
101
|
+
del self._order[key]
|
|
102
|
+
self._used_bytes -= entry.weight
|
|
103
|
+
return True
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
def clear_all(self) -> None:
|
|
107
|
+
with self._guard.lru_section(), self._lock:
|
|
108
|
+
self._order.clear()
|
|
109
|
+
self._used_bytes = 0
|