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,892 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from bson.errors import BSONError
|
|
10
|
+
|
|
11
|
+
from client_query_cache._core.canonical import canonicalize, is_canonicalizable
|
|
12
|
+
from client_query_cache._core.codec import decode_value, encode_value
|
|
13
|
+
from client_query_cache._core.entries import AdmissionOutcome, CacheEntry, LookupResult
|
|
14
|
+
from client_query_cache._core.errors import (
|
|
15
|
+
CacheClosedError,
|
|
16
|
+
CacheConfigurationError,
|
|
17
|
+
UnsupportedCacheRequestError,
|
|
18
|
+
)
|
|
19
|
+
from client_query_cache._core.keys import (
|
|
20
|
+
IdentityCacheKey,
|
|
21
|
+
NamespaceCacheKey,
|
|
22
|
+
canonical_alias_key,
|
|
23
|
+
)
|
|
24
|
+
from client_query_cache._core.lifecycle import CacheLifecycleState
|
|
25
|
+
from client_query_cache._core.locking import LockOrderGuard
|
|
26
|
+
from client_query_cache._core.lru import WeightedLru
|
|
27
|
+
from client_query_cache._core.namespace import IdentityState, NamespaceState
|
|
28
|
+
from client_query_cache._core.order_sensitive_keys import order_sensitive_key
|
|
29
|
+
from client_query_cache._core.snapshots import CacheSnapshot, CacheStatistics
|
|
30
|
+
from client_query_cache._core.stream_cost import (
|
|
31
|
+
DEFAULT_LAG_CAPTURE_WINDOW_CONFIG,
|
|
32
|
+
LagCaptureWindowConfig,
|
|
33
|
+
StreamCostRegistry,
|
|
34
|
+
StreamCostSnapshot,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from collections.abc import Callable, Iterator
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from bson.codec_options import CodecOptions
|
|
42
|
+
|
|
43
|
+
from client_query_cache._core.canonical import Canonical
|
|
44
|
+
from client_query_cache._core.keys import AliasKey, CacheKey, NamespaceId
|
|
45
|
+
|
|
46
|
+
DEFAULT_SHARED_BUDGET_BYTES = 64 * 1024 * 1024
|
|
47
|
+
DEFAULT_MAX_ENTRY_BYTES = 1 * 1024 * 1024
|
|
48
|
+
|
|
49
|
+
_NOT_INDEXED = object()
|
|
50
|
+
_DEFAULT_DATABASE_AVAILABILITY = (True, 0)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class CacheCoreConfig:
|
|
57
|
+
shared_budget_bytes: int = DEFAULT_SHARED_BUDGET_BYTES
|
|
58
|
+
max_entry_bytes: int = DEFAULT_MAX_ENTRY_BYTES
|
|
59
|
+
lag_capture_window_config: LagCaptureWindowConfig = (
|
|
60
|
+
DEFAULT_LAG_CAPTURE_WINDOW_CONFIG
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __post_init__(self) -> None:
|
|
64
|
+
if self.shared_budget_bytes <= 0:
|
|
65
|
+
message = "shared_budget_bytes must be positive"
|
|
66
|
+
raise CacheConfigurationError(message)
|
|
67
|
+
if self.max_entry_bytes <= 0:
|
|
68
|
+
message = "max_entry_bytes must be positive"
|
|
69
|
+
raise CacheConfigurationError(message)
|
|
70
|
+
if self.max_entry_bytes > self.shared_budget_bytes:
|
|
71
|
+
message = "max_entry_bytes must not exceed shared_budget_bytes"
|
|
72
|
+
raise CacheConfigurationError(message)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(slots=True)
|
|
76
|
+
class IdentityCapture:
|
|
77
|
+
namespace: NamespaceId
|
|
78
|
+
identity: Canonical
|
|
79
|
+
generation_key: tuple[int, int]
|
|
80
|
+
availability_generation: int
|
|
81
|
+
released: bool = field(default=False)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(slots=True)
|
|
85
|
+
class NamespaceCapture:
|
|
86
|
+
namespace: NamespaceId
|
|
87
|
+
generation: int
|
|
88
|
+
availability_generation: int
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _maybe_prune_identity_locked(
|
|
92
|
+
state: NamespaceState, identity: Canonical, identity_state: IdentityState
|
|
93
|
+
) -> None:
|
|
94
|
+
if identity_state.is_referenced:
|
|
95
|
+
return
|
|
96
|
+
for alias_key in identity_state.alias_keys:
|
|
97
|
+
state.aliases.pop(alias_key, None)
|
|
98
|
+
state.identities.pop(identity, None)
|
|
99
|
+
state.identity_generation_watermark = max(
|
|
100
|
+
state.identity_generation_watermark, identity_state.generation + 1
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _discard_entry_locked(state: NamespaceState, entry: CacheEntry) -> None:
|
|
105
|
+
was_indexed = state.entry_index.pop(entry, _NOT_INDEXED) is not _NOT_INDEXED
|
|
106
|
+
if was_indexed and entry.identity is not None:
|
|
107
|
+
identity_state = state.identities[entry.identity]
|
|
108
|
+
identity_state.cached_ref_count -= 1
|
|
109
|
+
_maybe_prune_identity_locked(state, entry.identity, identity_state)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _match_identity_state(
|
|
113
|
+
state: NamespaceState,
|
|
114
|
+
identity: Canonical,
|
|
115
|
+
generation_key: tuple[int, int],
|
|
116
|
+
) -> IdentityState | None:
|
|
117
|
+
try:
|
|
118
|
+
identity_state = state.identities[identity]
|
|
119
|
+
except KeyError:
|
|
120
|
+
return None
|
|
121
|
+
if (state.epoch, identity_state.generation) != generation_key:
|
|
122
|
+
return None
|
|
123
|
+
return identity_state
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class _CacheCoreBase:
|
|
127
|
+
__slots__ = (
|
|
128
|
+
"_availability_lock",
|
|
129
|
+
"_database_availability",
|
|
130
|
+
"_database_namespaces",
|
|
131
|
+
"_guard",
|
|
132
|
+
"_lifecycle",
|
|
133
|
+
"_lifecycle_lock",
|
|
134
|
+
"_lru",
|
|
135
|
+
"_namespaces",
|
|
136
|
+
"_namespaces_lock",
|
|
137
|
+
"_statistics",
|
|
138
|
+
"_stream_cost",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def __init__(self, config: CacheCoreConfig | None = None) -> None:
|
|
142
|
+
resolved_config = config if config is not None else CacheCoreConfig()
|
|
143
|
+
self._guard = LockOrderGuard()
|
|
144
|
+
self._lru = WeightedLru(
|
|
145
|
+
shared_budget_bytes=resolved_config.shared_budget_bytes,
|
|
146
|
+
max_entry_bytes=resolved_config.max_entry_bytes,
|
|
147
|
+
guard=self._guard,
|
|
148
|
+
)
|
|
149
|
+
self._namespaces: dict[NamespaceId, NamespaceState] = {}
|
|
150
|
+
self._database_namespaces: dict[str, dict[NamespaceId, None]] = {}
|
|
151
|
+
self._namespaces_lock = threading.Lock()
|
|
152
|
+
self._lifecycle = CacheLifecycleState.ACTIVE
|
|
153
|
+
self._lifecycle_lock = threading.Lock()
|
|
154
|
+
self._statistics = CacheStatistics()
|
|
155
|
+
self._database_availability: dict[str, tuple[bool, int]] = {}
|
|
156
|
+
self._availability_lock = threading.RLock()
|
|
157
|
+
self._stream_cost = StreamCostRegistry(
|
|
158
|
+
resolved_config.lag_capture_window_config
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _is_closed(self) -> bool:
|
|
162
|
+
return self._lifecycle is CacheLifecycleState.CLOSED
|
|
163
|
+
|
|
164
|
+
def _is_database_available(self, database: str) -> bool:
|
|
165
|
+
with self._availability_lock:
|
|
166
|
+
try:
|
|
167
|
+
available, _generation = self._database_availability[database]
|
|
168
|
+
except KeyError:
|
|
169
|
+
available, _generation = _DEFAULT_DATABASE_AVAILABILITY
|
|
170
|
+
return available
|
|
171
|
+
|
|
172
|
+
def _capture_database_availability(self, database: str) -> int:
|
|
173
|
+
with self._availability_lock:
|
|
174
|
+
try:
|
|
175
|
+
_available, generation = self._database_availability[database]
|
|
176
|
+
except KeyError:
|
|
177
|
+
_available, generation = _DEFAULT_DATABASE_AVAILABILITY
|
|
178
|
+
return generation
|
|
179
|
+
|
|
180
|
+
@contextmanager
|
|
181
|
+
def _admission_section(
|
|
182
|
+
self,
|
|
183
|
+
namespace: NamespaceId,
|
|
184
|
+
availability_generation: int,
|
|
185
|
+
weight: int,
|
|
186
|
+
) -> Iterator[AdmissionOutcome | None]:
|
|
187
|
+
with self._availability_lock:
|
|
188
|
+
try:
|
|
189
|
+
available, current_generation = self._database_availability[
|
|
190
|
+
namespace.database
|
|
191
|
+
]
|
|
192
|
+
except KeyError:
|
|
193
|
+
available, current_generation = _DEFAULT_DATABASE_AVAILABILITY
|
|
194
|
+
if not available or availability_generation != current_generation:
|
|
195
|
+
self._statistics.record_bypass()
|
|
196
|
+
yield AdmissionOutcome.DECLINED_UNAVAILABLE
|
|
197
|
+
return
|
|
198
|
+
if self._lru.is_oversize(weight):
|
|
199
|
+
self._statistics.record_oversized_bypass()
|
|
200
|
+
yield AdmissionOutcome.DECLINED_OVERSIZE
|
|
201
|
+
return
|
|
202
|
+
yield None
|
|
203
|
+
|
|
204
|
+
def _ensure_active(self) -> None:
|
|
205
|
+
if self._is_closed():
|
|
206
|
+
message = "cache manager is closed"
|
|
207
|
+
raise CacheClosedError(message)
|
|
208
|
+
|
|
209
|
+
def _namespace(self, namespace: NamespaceId) -> NamespaceState:
|
|
210
|
+
with self._namespaces_lock:
|
|
211
|
+
try:
|
|
212
|
+
state = self._namespaces[namespace]
|
|
213
|
+
except KeyError:
|
|
214
|
+
state = NamespaceState(namespace=namespace)
|
|
215
|
+
if not self._is_closed():
|
|
216
|
+
self._namespaces[namespace] = state
|
|
217
|
+
self._database_namespaces.setdefault(namespace.database, {})[
|
|
218
|
+
namespace
|
|
219
|
+
] = None
|
|
220
|
+
return state
|
|
221
|
+
|
|
222
|
+
@contextmanager
|
|
223
|
+
def _namespace_section(self, state: NamespaceState) -> Iterator[None]:
|
|
224
|
+
with self._guard.namespace_section(), state.lock:
|
|
225
|
+
yield
|
|
226
|
+
|
|
227
|
+
def _reclaim_if_evicted_before_publication(
|
|
228
|
+
self, state: NamespaceState, key: CacheKey, entry: CacheEntry
|
|
229
|
+
) -> bool:
|
|
230
|
+
if self._lru.contains_exact(key, entry):
|
|
231
|
+
return False
|
|
232
|
+
with self._namespace_section(state):
|
|
233
|
+
_discard_entry_locked(state, entry)
|
|
234
|
+
self._lru.remove_exact(key, entry)
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def _process_evicted(self, evicted: list[CacheEntry]) -> None:
|
|
238
|
+
if not evicted:
|
|
239
|
+
return
|
|
240
|
+
self._statistics.record_evictions(len(evicted))
|
|
241
|
+
logger.debug("cache entries evicted", extra={"evicted_count": len(evicted)})
|
|
242
|
+
by_namespace: dict[NamespaceId, list[CacheEntry]] = {}
|
|
243
|
+
for entry in evicted:
|
|
244
|
+
by_namespace.setdefault(entry.namespace, []).append(entry)
|
|
245
|
+
for namespace, entries in by_namespace.items():
|
|
246
|
+
state = self._namespace(namespace)
|
|
247
|
+
with self._namespace_section(state):
|
|
248
|
+
for entry in entries:
|
|
249
|
+
_discard_entry_locked(state, entry)
|
|
250
|
+
|
|
251
|
+
def _finalize_put(
|
|
252
|
+
self,
|
|
253
|
+
state: NamespaceState,
|
|
254
|
+
key: CacheKey,
|
|
255
|
+
entry: CacheEntry,
|
|
256
|
+
*,
|
|
257
|
+
admitted: bool,
|
|
258
|
+
displaced: CacheEntry | None,
|
|
259
|
+
is_still_valid: Callable[[], bool],
|
|
260
|
+
on_admit: Callable[[], None],
|
|
261
|
+
) -> AdmissionOutcome:
|
|
262
|
+
if not admitted:
|
|
263
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
264
|
+
still_resident = self._lru.contains_exact(key, entry)
|
|
265
|
+
rolled_back = False
|
|
266
|
+
with self._namespace_section(state):
|
|
267
|
+
if not still_resident or self._is_closed() or not is_still_valid():
|
|
268
|
+
rolled_back = True
|
|
269
|
+
else:
|
|
270
|
+
state.entry_index[entry] = key
|
|
271
|
+
on_admit()
|
|
272
|
+
if displaced is not None:
|
|
273
|
+
_discard_entry_locked(state, displaced)
|
|
274
|
+
if rolled_back:
|
|
275
|
+
self._lru.remove_exact(key, entry)
|
|
276
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
277
|
+
if self._reclaim_if_evicted_before_publication(state, key, entry):
|
|
278
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
279
|
+
return AdmissionOutcome.ADMITTED
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class _CacheCoreLifecycle(_CacheCoreBase):
|
|
283
|
+
__slots__ = ()
|
|
284
|
+
|
|
285
|
+
def close(self) -> None:
|
|
286
|
+
with self._lifecycle_lock:
|
|
287
|
+
if self._is_closed():
|
|
288
|
+
return
|
|
289
|
+
self._lifecycle = CacheLifecycleState.CLOSED
|
|
290
|
+
self._lru.clear_all()
|
|
291
|
+
with self._namespaces_lock:
|
|
292
|
+
self._namespaces.clear()
|
|
293
|
+
self._database_namespaces.clear()
|
|
294
|
+
logger.info("cache manager closed")
|
|
295
|
+
|
|
296
|
+
def record_bypass(self) -> None:
|
|
297
|
+
self._statistics.record_bypass()
|
|
298
|
+
|
|
299
|
+
def snapshot(self) -> CacheSnapshot:
|
|
300
|
+
used_bytes, entry_count = self._lru.snapshot_usage()
|
|
301
|
+
hits, misses, evictions, bypasses, oversized_bypasses = (
|
|
302
|
+
self._statistics.snapshot()
|
|
303
|
+
)
|
|
304
|
+
return CacheSnapshot(
|
|
305
|
+
lifecycle=self._lifecycle.value,
|
|
306
|
+
used_bytes=used_bytes,
|
|
307
|
+
shared_budget_bytes=self._lru.shared_budget_bytes,
|
|
308
|
+
max_entry_bytes=self._lru.max_entry_bytes,
|
|
309
|
+
entry_count=entry_count,
|
|
310
|
+
hits=hits,
|
|
311
|
+
misses=misses,
|
|
312
|
+
evictions=evictions,
|
|
313
|
+
bypasses=bypasses,
|
|
314
|
+
oversized_bypasses=oversized_bypasses,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class _CacheCoreNamespaceLifecycle(_CacheCoreBase):
|
|
319
|
+
__slots__ = ()
|
|
320
|
+
|
|
321
|
+
def namespaces_for_database(self, database: str) -> list[NamespaceId]:
|
|
322
|
+
with self._namespaces_lock:
|
|
323
|
+
try:
|
|
324
|
+
namespaces = self._database_namespaces[database]
|
|
325
|
+
except KeyError:
|
|
326
|
+
namespaces = {}
|
|
327
|
+
return list(namespaces)
|
|
328
|
+
|
|
329
|
+
def has_namespace(self, namespace: NamespaceId) -> bool:
|
|
330
|
+
with self._namespaces_lock:
|
|
331
|
+
return namespace in self._namespaces
|
|
332
|
+
|
|
333
|
+
def current_epoch(self, namespace: NamespaceId) -> int:
|
|
334
|
+
state = self._namespace(namespace)
|
|
335
|
+
with self._namespace_section(state):
|
|
336
|
+
return state.epoch
|
|
337
|
+
|
|
338
|
+
def current_index_generation(self, namespace: NamespaceId) -> int:
|
|
339
|
+
state = self._namespace(namespace)
|
|
340
|
+
with self._namespace_section(state):
|
|
341
|
+
return state.index_generation
|
|
342
|
+
|
|
343
|
+
def record_index_change(self, namespace: NamespaceId) -> None:
|
|
344
|
+
self._ensure_active()
|
|
345
|
+
state = self._namespace(namespace)
|
|
346
|
+
with self._namespace_section(state):
|
|
347
|
+
state.index_generation += 1
|
|
348
|
+
|
|
349
|
+
def record_write(self, namespace: NamespaceId, identity: object) -> None:
|
|
350
|
+
self._ensure_active()
|
|
351
|
+
identity = order_sensitive_key(identity)
|
|
352
|
+
state = self._namespace(namespace)
|
|
353
|
+
with self._namespace_section(state):
|
|
354
|
+
state.generation += 1
|
|
355
|
+
if not is_canonicalizable(identity):
|
|
356
|
+
return
|
|
357
|
+
try:
|
|
358
|
+
identity_state = state.identities[canonicalize(identity)]
|
|
359
|
+
except KeyError:
|
|
360
|
+
return
|
|
361
|
+
identity_state.generation += 1
|
|
362
|
+
for alias_key in identity_state.alias_keys:
|
|
363
|
+
state.aliases.pop(alias_key, None)
|
|
364
|
+
identity_state.alias_keys.clear()
|
|
365
|
+
|
|
366
|
+
def clear_namespace(self, namespace: NamespaceId) -> None:
|
|
367
|
+
self._ensure_active()
|
|
368
|
+
self._bump_epoch_and_reclaim(namespace)
|
|
369
|
+
|
|
370
|
+
def create_namespace(self, namespace: NamespaceId) -> None:
|
|
371
|
+
self._ensure_active()
|
|
372
|
+
self._bump_epoch_and_reclaim(namespace)
|
|
373
|
+
|
|
374
|
+
def _bump_epoch_and_reclaim(self, namespace: NamespaceId) -> None:
|
|
375
|
+
state = self._namespace(namespace)
|
|
376
|
+
with self._namespace_section(state):
|
|
377
|
+
state.generation += 1
|
|
378
|
+
state.epoch += 1
|
|
379
|
+
state.index_generation += 1
|
|
380
|
+
state.aliases = {}
|
|
381
|
+
reclaimed = list(state.entry_index.items())
|
|
382
|
+
state.entry_index.clear()
|
|
383
|
+
for entry, _key in reclaimed:
|
|
384
|
+
if entry.identity is not None:
|
|
385
|
+
state.identities[entry.identity].cached_ref_count -= 1
|
|
386
|
+
for identity_state in state.identities.values():
|
|
387
|
+
identity_state.alias_keys.clear()
|
|
388
|
+
for identity, identity_state in list(state.identities.items()):
|
|
389
|
+
if not identity_state.is_referenced:
|
|
390
|
+
del state.identities[identity]
|
|
391
|
+
for entry, key in reclaimed:
|
|
392
|
+
self._lru.remove_exact(key, entry)
|
|
393
|
+
logger.debug(
|
|
394
|
+
"cache namespace reclaimed",
|
|
395
|
+
extra={
|
|
396
|
+
"database": namespace.database,
|
|
397
|
+
"collection": namespace.collection,
|
|
398
|
+
"reclaimed_entries": len(reclaimed),
|
|
399
|
+
},
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class _CacheCoreDatabaseAvailability(_CacheCoreBase):
|
|
404
|
+
__slots__ = ()
|
|
405
|
+
|
|
406
|
+
def is_database_available(self, database: str) -> bool:
|
|
407
|
+
return self._is_database_available(database)
|
|
408
|
+
|
|
409
|
+
def set_database_available(self, database: str, *, available: bool) -> None:
|
|
410
|
+
with self._availability_lock:
|
|
411
|
+
try:
|
|
412
|
+
current_available, generation = self._database_availability[database]
|
|
413
|
+
except KeyError:
|
|
414
|
+
current_available, generation = _DEFAULT_DATABASE_AVAILABILITY
|
|
415
|
+
if available != current_available:
|
|
416
|
+
self._database_availability[database] = (available, generation + 1)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class _CacheCoreIdentityAdmission(_CacheCoreBase):
|
|
420
|
+
__slots__ = ()
|
|
421
|
+
|
|
422
|
+
def begin_identity_admission(
|
|
423
|
+
self, namespace: NamespaceId, identity: object
|
|
424
|
+
) -> IdentityCapture:
|
|
425
|
+
self._ensure_active()
|
|
426
|
+
canonical_identity = canonicalize(order_sensitive_key(identity))
|
|
427
|
+
if canonical_identity is None:
|
|
428
|
+
message = "identity must not be None"
|
|
429
|
+
raise UnsupportedCacheRequestError(message)
|
|
430
|
+
availability_generation = self._capture_database_availability(
|
|
431
|
+
namespace.database
|
|
432
|
+
)
|
|
433
|
+
state = self._namespace(namespace)
|
|
434
|
+
with self._namespace_section(state):
|
|
435
|
+
try:
|
|
436
|
+
identity_state = state.identities[canonical_identity]
|
|
437
|
+
except KeyError:
|
|
438
|
+
identity_state = IdentityState(
|
|
439
|
+
generation=state.identity_generation_watermark
|
|
440
|
+
)
|
|
441
|
+
state.identity_generation_watermark += 1
|
|
442
|
+
state.identities[canonical_identity] = identity_state
|
|
443
|
+
identity_state.inflight_ref_count += 1
|
|
444
|
+
generation_key = (state.epoch, identity_state.generation)
|
|
445
|
+
return IdentityCapture(
|
|
446
|
+
namespace=namespace,
|
|
447
|
+
identity=canonical_identity,
|
|
448
|
+
generation_key=generation_key,
|
|
449
|
+
availability_generation=availability_generation,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
def discard_identity_admission(self, capture: IdentityCapture) -> None:
|
|
453
|
+
self._release_identity_capture(capture)
|
|
454
|
+
|
|
455
|
+
def admit_identity(
|
|
456
|
+
self,
|
|
457
|
+
capture: IdentityCapture,
|
|
458
|
+
read_shape: object,
|
|
459
|
+
value: object,
|
|
460
|
+
*,
|
|
461
|
+
alias: AliasKey | None = None,
|
|
462
|
+
codec_options: CodecOptions[Any] | None = None,
|
|
463
|
+
) -> AdmissionOutcome:
|
|
464
|
+
self._ensure_active()
|
|
465
|
+
try:
|
|
466
|
+
canonical_shape = canonicalize(read_shape)
|
|
467
|
+
try:
|
|
468
|
+
encoded = encode_value(value, codec_options)
|
|
469
|
+
except BSONError, OverflowError:
|
|
470
|
+
return AdmissionOutcome.DECLINED_UNENCODABLE
|
|
471
|
+
weight = len(encoded)
|
|
472
|
+
with self._admission_section(
|
|
473
|
+
capture.namespace, capture.availability_generation, weight
|
|
474
|
+
) as rejected:
|
|
475
|
+
if rejected is not None:
|
|
476
|
+
return rejected
|
|
477
|
+
key = IdentityCacheKey(
|
|
478
|
+
capture.namespace, capture.identity, canonical_shape
|
|
479
|
+
)
|
|
480
|
+
state = self._namespace(capture.namespace)
|
|
481
|
+
with self._namespace_section(state):
|
|
482
|
+
identity_state = _match_identity_state(
|
|
483
|
+
state, capture.identity, capture.generation_key
|
|
484
|
+
)
|
|
485
|
+
if self._is_closed() or identity_state is None:
|
|
486
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
487
|
+
if alias is not None:
|
|
488
|
+
_publish_alias_locked(state, alias, capture.identity)
|
|
489
|
+
entry = CacheEntry(
|
|
490
|
+
generation_key=capture.generation_key,
|
|
491
|
+
weight=weight,
|
|
492
|
+
value=encoded,
|
|
493
|
+
namespace=capture.namespace,
|
|
494
|
+
identity=capture.identity,
|
|
495
|
+
)
|
|
496
|
+
admitted, displaced, evicted = self._lru.conditional_put(key, entry)
|
|
497
|
+
self._process_evicted(evicted)
|
|
498
|
+
|
|
499
|
+
def _is_still_valid() -> bool:
|
|
500
|
+
return (
|
|
501
|
+
_match_identity_state(
|
|
502
|
+
state, capture.identity, capture.generation_key
|
|
503
|
+
)
|
|
504
|
+
is not None
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def _on_admit() -> None:
|
|
508
|
+
state.identities[capture.identity].cached_ref_count += 1
|
|
509
|
+
|
|
510
|
+
return self._finalize_put(
|
|
511
|
+
state,
|
|
512
|
+
key,
|
|
513
|
+
entry,
|
|
514
|
+
admitted=admitted,
|
|
515
|
+
displaced=displaced,
|
|
516
|
+
is_still_valid=_is_still_valid,
|
|
517
|
+
on_admit=_on_admit,
|
|
518
|
+
)
|
|
519
|
+
finally:
|
|
520
|
+
self._release_identity_capture(capture)
|
|
521
|
+
|
|
522
|
+
def _release_identity_capture(self, capture: IdentityCapture) -> None:
|
|
523
|
+
if capture.released:
|
|
524
|
+
return
|
|
525
|
+
capture.released = True
|
|
526
|
+
state = self._namespace(capture.namespace)
|
|
527
|
+
with self._namespace_section(state):
|
|
528
|
+
try:
|
|
529
|
+
identity_state = state.identities[capture.identity]
|
|
530
|
+
except KeyError:
|
|
531
|
+
return
|
|
532
|
+
identity_state.inflight_ref_count -= 1
|
|
533
|
+
_maybe_prune_identity_locked(state, capture.identity, identity_state)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _publish_alias_locked(
|
|
537
|
+
state: NamespaceState, alias: AliasKey, identity: Canonical
|
|
538
|
+
) -> None:
|
|
539
|
+
try:
|
|
540
|
+
previous_identity = state.aliases[alias]
|
|
541
|
+
except KeyError:
|
|
542
|
+
previous_identity = None
|
|
543
|
+
if previous_identity is not None and previous_identity != identity:
|
|
544
|
+
state.identities[previous_identity].alias_keys.discard(alias)
|
|
545
|
+
state.aliases[alias] = identity
|
|
546
|
+
state.identities[identity].alias_keys.add(alias)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _discard_alias_locked(
|
|
550
|
+
state: NamespaceState, alias: AliasKey, expected_identity: Canonical
|
|
551
|
+
) -> None:
|
|
552
|
+
try:
|
|
553
|
+
current_identity = state.aliases[alias]
|
|
554
|
+
except KeyError:
|
|
555
|
+
return
|
|
556
|
+
if current_identity != expected_identity:
|
|
557
|
+
return
|
|
558
|
+
del state.aliases[alias]
|
|
559
|
+
identity_state = state.identities[expected_identity]
|
|
560
|
+
identity_state.alias_keys.discard(alias)
|
|
561
|
+
_maybe_prune_identity_locked(state, expected_identity, identity_state)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
class _CacheCoreNamespaceAdmission(_CacheCoreBase):
|
|
565
|
+
__slots__ = ()
|
|
566
|
+
|
|
567
|
+
def capture_namespace_generation(self, namespace: NamespaceId) -> NamespaceCapture:
|
|
568
|
+
self._ensure_active()
|
|
569
|
+
availability_generation = self._capture_database_availability(
|
|
570
|
+
namespace.database
|
|
571
|
+
)
|
|
572
|
+
state = self._namespace(namespace)
|
|
573
|
+
with self._namespace_section(state):
|
|
574
|
+
return NamespaceCapture(
|
|
575
|
+
namespace=namespace,
|
|
576
|
+
generation=state.generation,
|
|
577
|
+
availability_generation=availability_generation,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
def admit_namespace(
|
|
581
|
+
self,
|
|
582
|
+
capture: NamespaceCapture,
|
|
583
|
+
discriminator: object,
|
|
584
|
+
value: object,
|
|
585
|
+
*,
|
|
586
|
+
codec_options: CodecOptions[Any] | None = None,
|
|
587
|
+
) -> AdmissionOutcome:
|
|
588
|
+
self._ensure_active()
|
|
589
|
+
canonical_discriminator = canonicalize(discriminator)
|
|
590
|
+
try:
|
|
591
|
+
encoded = encode_value(value, codec_options)
|
|
592
|
+
except BSONError, OverflowError:
|
|
593
|
+
return AdmissionOutcome.DECLINED_UNENCODABLE
|
|
594
|
+
weight = len(encoded)
|
|
595
|
+
with self._admission_section(
|
|
596
|
+
capture.namespace, capture.availability_generation, weight
|
|
597
|
+
) as rejected:
|
|
598
|
+
if rejected is not None:
|
|
599
|
+
return rejected
|
|
600
|
+
key = NamespaceCacheKey(capture.namespace, canonical_discriminator)
|
|
601
|
+
state = self._namespace(capture.namespace)
|
|
602
|
+
with self._namespace_section(state):
|
|
603
|
+
if self._is_closed() or state.generation != capture.generation:
|
|
604
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
605
|
+
entry = CacheEntry(
|
|
606
|
+
generation_key=(capture.generation,),
|
|
607
|
+
weight=weight,
|
|
608
|
+
value=encoded,
|
|
609
|
+
namespace=capture.namespace,
|
|
610
|
+
identity=None,
|
|
611
|
+
)
|
|
612
|
+
admitted, displaced, evicted = self._lru.conditional_put(key, entry)
|
|
613
|
+
self._process_evicted(evicted)
|
|
614
|
+
return self._finalize_put(
|
|
615
|
+
state,
|
|
616
|
+
key,
|
|
617
|
+
entry,
|
|
618
|
+
admitted=admitted,
|
|
619
|
+
displaced=displaced,
|
|
620
|
+
is_still_valid=lambda: state.generation == capture.generation,
|
|
621
|
+
on_admit=lambda: None,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class _CacheCoreUniqueKeyAdmission(_CacheCoreBase):
|
|
626
|
+
__slots__ = ()
|
|
627
|
+
|
|
628
|
+
def discard_stale_alias(
|
|
629
|
+
self, namespace: NamespaceId, alias: AliasKey, expected_identity: Canonical
|
|
630
|
+
) -> None:
|
|
631
|
+
self._ensure_active()
|
|
632
|
+
state = self._namespace(namespace)
|
|
633
|
+
with self._namespace_section(state):
|
|
634
|
+
_discard_alias_locked(state, alias, expected_identity)
|
|
635
|
+
|
|
636
|
+
def discard_namespace_entry(
|
|
637
|
+
self, namespace: NamespaceId, discriminator: object, generation: int
|
|
638
|
+
) -> None:
|
|
639
|
+
self._ensure_active()
|
|
640
|
+
key = NamespaceCacheKey(namespace, canonicalize(discriminator))
|
|
641
|
+
entry = self._lru.peek(key)
|
|
642
|
+
if entry is None or entry.generation_key != (generation,):
|
|
643
|
+
return
|
|
644
|
+
state = self._namespace(namespace)
|
|
645
|
+
with self._namespace_section(state):
|
|
646
|
+
_discard_entry_locked(state, entry)
|
|
647
|
+
self._lru.remove_exact(key, entry)
|
|
648
|
+
|
|
649
|
+
def admit_unique_key_match(
|
|
650
|
+
self,
|
|
651
|
+
namespace_capture: NamespaceCapture,
|
|
652
|
+
discriminator: object,
|
|
653
|
+
identity: object,
|
|
654
|
+
read_shape: object,
|
|
655
|
+
value: object,
|
|
656
|
+
*,
|
|
657
|
+
alias: AliasKey,
|
|
658
|
+
codec_options: CodecOptions[Any] | None = None,
|
|
659
|
+
) -> AdmissionOutcome:
|
|
660
|
+
self._ensure_active()
|
|
661
|
+
namespace = namespace_capture.namespace
|
|
662
|
+
canonical_discriminator = canonicalize(discriminator)
|
|
663
|
+
canonical_identity = canonicalize(order_sensitive_key(identity))
|
|
664
|
+
if canonical_identity is None:
|
|
665
|
+
message = "identity must not be None"
|
|
666
|
+
raise UnsupportedCacheRequestError(message)
|
|
667
|
+
canonical_shape = canonicalize(read_shape)
|
|
668
|
+
try:
|
|
669
|
+
encoded = encode_value(value, codec_options)
|
|
670
|
+
except BSONError, OverflowError:
|
|
671
|
+
return AdmissionOutcome.DECLINED_UNENCODABLE
|
|
672
|
+
weight = len(encoded)
|
|
673
|
+
with self._admission_section(
|
|
674
|
+
namespace, namespace_capture.availability_generation, weight
|
|
675
|
+
) as rejected:
|
|
676
|
+
if rejected is not None:
|
|
677
|
+
return rejected
|
|
678
|
+
namespace_key = NamespaceCacheKey(namespace, canonical_discriminator)
|
|
679
|
+
identity_key = IdentityCacheKey(
|
|
680
|
+
namespace, canonical_identity, canonical_shape
|
|
681
|
+
)
|
|
682
|
+
state = self._namespace(namespace)
|
|
683
|
+
with self._namespace_section(state):
|
|
684
|
+
if (
|
|
685
|
+
self._is_closed()
|
|
686
|
+
or state.generation != namespace_capture.generation
|
|
687
|
+
):
|
|
688
|
+
return AdmissionOutcome.DECLINED_STALE
|
|
689
|
+
try:
|
|
690
|
+
identity_state = state.identities[canonical_identity]
|
|
691
|
+
except KeyError:
|
|
692
|
+
identity_state = IdentityState(
|
|
693
|
+
generation=state.identity_generation_watermark
|
|
694
|
+
)
|
|
695
|
+
state.identity_generation_watermark += 1
|
|
696
|
+
state.identities[canonical_identity] = identity_state
|
|
697
|
+
identity_generation_key = (state.epoch, identity_state.generation)
|
|
698
|
+
_publish_alias_locked(state, alias, canonical_identity)
|
|
699
|
+
namespace_entry = CacheEntry(
|
|
700
|
+
generation_key=(namespace_capture.generation,),
|
|
701
|
+
weight=weight,
|
|
702
|
+
value=encoded,
|
|
703
|
+
namespace=namespace,
|
|
704
|
+
identity=None,
|
|
705
|
+
)
|
|
706
|
+
identity_entry = CacheEntry(
|
|
707
|
+
generation_key=identity_generation_key,
|
|
708
|
+
weight=weight,
|
|
709
|
+
value=encoded,
|
|
710
|
+
namespace=namespace,
|
|
711
|
+
identity=canonical_identity,
|
|
712
|
+
)
|
|
713
|
+
ns_admitted, ns_displaced, ns_evicted = self._lru.conditional_put(
|
|
714
|
+
namespace_key, namespace_entry
|
|
715
|
+
)
|
|
716
|
+
id_admitted, id_displaced, id_evicted = self._lru.conditional_put(
|
|
717
|
+
identity_key, identity_entry
|
|
718
|
+
)
|
|
719
|
+
self._process_evicted(ns_evicted)
|
|
720
|
+
self._process_evicted(id_evicted)
|
|
721
|
+
namespace_outcome = self._finalize_put(
|
|
722
|
+
state,
|
|
723
|
+
namespace_key,
|
|
724
|
+
namespace_entry,
|
|
725
|
+
admitted=ns_admitted,
|
|
726
|
+
displaced=ns_displaced,
|
|
727
|
+
is_still_valid=lambda: state.generation == namespace_capture.generation,
|
|
728
|
+
on_admit=lambda: None,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def _identity_still_valid() -> bool:
|
|
732
|
+
return (
|
|
733
|
+
_match_identity_state(
|
|
734
|
+
state, canonical_identity, identity_generation_key
|
|
735
|
+
)
|
|
736
|
+
is not None
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def _on_identity_admit() -> None:
|
|
740
|
+
state.identities[canonical_identity].cached_ref_count += 1
|
|
741
|
+
|
|
742
|
+
identity_outcome = self._finalize_put(
|
|
743
|
+
state,
|
|
744
|
+
identity_key,
|
|
745
|
+
identity_entry,
|
|
746
|
+
admitted=id_admitted,
|
|
747
|
+
displaced=id_displaced,
|
|
748
|
+
is_still_valid=_identity_still_valid,
|
|
749
|
+
on_admit=_on_identity_admit,
|
|
750
|
+
)
|
|
751
|
+
if identity_outcome is not AdmissionOutcome.ADMITTED:
|
|
752
|
+
with self._namespace_section(state):
|
|
753
|
+
try:
|
|
754
|
+
current_identity_state = state.identities[canonical_identity]
|
|
755
|
+
except KeyError:
|
|
756
|
+
current_identity_state = None
|
|
757
|
+
if (
|
|
758
|
+
current_identity_state is None
|
|
759
|
+
or not current_identity_state.is_referenced
|
|
760
|
+
):
|
|
761
|
+
_discard_alias_locked(state, alias, canonical_identity)
|
|
762
|
+
if namespace_outcome is AdmissionOutcome.ADMITTED:
|
|
763
|
+
return identity_outcome
|
|
764
|
+
return namespace_outcome
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
class _CacheCoreLookup(_CacheCoreBase):
|
|
768
|
+
__slots__ = ()
|
|
769
|
+
|
|
770
|
+
def lookup_identity(
|
|
771
|
+
self,
|
|
772
|
+
namespace: NamespaceId,
|
|
773
|
+
identity: object,
|
|
774
|
+
read_shape: object,
|
|
775
|
+
*,
|
|
776
|
+
codec_options: CodecOptions[Any] | None = None,
|
|
777
|
+
) -> LookupResult:
|
|
778
|
+
self._ensure_active()
|
|
779
|
+
if not self._is_database_available(namespace.database):
|
|
780
|
+
self._statistics.record_bypass()
|
|
781
|
+
return LookupResult(hit=False)
|
|
782
|
+
canonical_identity = canonicalize(order_sensitive_key(identity))
|
|
783
|
+
key = IdentityCacheKey(namespace, canonical_identity, canonicalize(read_shape))
|
|
784
|
+
entry = self._lru.peek(key)
|
|
785
|
+
if entry is None:
|
|
786
|
+
self._statistics.record_miss()
|
|
787
|
+
return LookupResult(hit=False)
|
|
788
|
+
state = self._namespace(namespace)
|
|
789
|
+
entry_generation_key = (entry.generation_key[0], entry.generation_key[1])
|
|
790
|
+
with self._namespace_section(state):
|
|
791
|
+
matched = _match_identity_state(
|
|
792
|
+
state, canonical_identity, entry_generation_key
|
|
793
|
+
)
|
|
794
|
+
if matched is None:
|
|
795
|
+
self._statistics.record_miss()
|
|
796
|
+
return LookupResult(hit=False)
|
|
797
|
+
self._lru.touch(key)
|
|
798
|
+
self._statistics.record_hit()
|
|
799
|
+
return LookupResult(hit=True, value=decode_value(entry.value, codec_options))
|
|
800
|
+
|
|
801
|
+
def lookup_namespace(
|
|
802
|
+
self,
|
|
803
|
+
namespace: NamespaceId,
|
|
804
|
+
discriminator: object,
|
|
805
|
+
*,
|
|
806
|
+
codec_options: CodecOptions[Any] | None = None,
|
|
807
|
+
) -> LookupResult:
|
|
808
|
+
self._ensure_active()
|
|
809
|
+
if not self._is_database_available(namespace.database):
|
|
810
|
+
self._statistics.record_bypass()
|
|
811
|
+
return LookupResult(hit=False)
|
|
812
|
+
canonical_discriminator = canonicalize(discriminator)
|
|
813
|
+
key = NamespaceCacheKey(namespace, canonical_discriminator)
|
|
814
|
+
entry = self._lru.peek(key)
|
|
815
|
+
if entry is None:
|
|
816
|
+
self._statistics.record_miss()
|
|
817
|
+
return LookupResult(hit=False)
|
|
818
|
+
state = self._namespace(namespace)
|
|
819
|
+
with self._namespace_section(state):
|
|
820
|
+
valid = (state.generation,) == entry.generation_key
|
|
821
|
+
if not valid:
|
|
822
|
+
self._statistics.record_miss()
|
|
823
|
+
return LookupResult(hit=False)
|
|
824
|
+
self._lru.touch(key)
|
|
825
|
+
self._statistics.record_hit()
|
|
826
|
+
return LookupResult(hit=True, value=decode_value(entry.value, codec_options))
|
|
827
|
+
|
|
828
|
+
def resolve_alias(
|
|
829
|
+
self,
|
|
830
|
+
namespace: NamespaceId,
|
|
831
|
+
definition: object,
|
|
832
|
+
value: object,
|
|
833
|
+
collation: object,
|
|
834
|
+
) -> Canonical | None:
|
|
835
|
+
self._ensure_active()
|
|
836
|
+
if not self._is_database_available(namespace.database):
|
|
837
|
+
self._statistics.record_bypass()
|
|
838
|
+
return None
|
|
839
|
+
alias_key = canonical_alias_key(definition, value, collation)
|
|
840
|
+
state = self._namespace(namespace)
|
|
841
|
+
with self._namespace_section(state):
|
|
842
|
+
try:
|
|
843
|
+
return state.aliases[alias_key]
|
|
844
|
+
except KeyError:
|
|
845
|
+
return None
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
class _CacheCoreStreamCostTelemetry(_CacheCoreBase):
|
|
849
|
+
__slots__ = ()
|
|
850
|
+
|
|
851
|
+
def record_stream_poll(self, database: str) -> None:
|
|
852
|
+
self._stream_cost.record_poll(database)
|
|
853
|
+
|
|
854
|
+
def record_logical_event_bytes(self, database: str, count: int) -> None:
|
|
855
|
+
self._stream_cost.record_logical_event_bytes(database, count)
|
|
856
|
+
|
|
857
|
+
def record_invalidation_applied(
|
|
858
|
+
self,
|
|
859
|
+
database: str,
|
|
860
|
+
raw_lag_seconds: float,
|
|
861
|
+
wall_seconds: float,
|
|
862
|
+
monotonic_seconds: float,
|
|
863
|
+
) -> None:
|
|
864
|
+
self._stream_cost.record_invalidation(
|
|
865
|
+
database, raw_lag_seconds, wall_seconds, monotonic_seconds
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def reset_stream_cost_statistics(self, database: str | None = None) -> None:
|
|
869
|
+
if database is None:
|
|
870
|
+
self._stream_cost.reset_all()
|
|
871
|
+
else:
|
|
872
|
+
self._stream_cost.reset(database)
|
|
873
|
+
|
|
874
|
+
def stream_cost_snapshot(self, database: str) -> StreamCostSnapshot:
|
|
875
|
+
used_bytes, _entry_count = self._lru.snapshot_usage()
|
|
876
|
+
return self._stream_cost.snapshot(database, resident_bytes=used_bytes)
|
|
877
|
+
|
|
878
|
+
def active_stream_cost_databases(self) -> list[str]:
|
|
879
|
+
return self._stream_cost.active_databases()
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
class CacheCore(
|
|
883
|
+
_CacheCoreLifecycle,
|
|
884
|
+
_CacheCoreNamespaceLifecycle,
|
|
885
|
+
_CacheCoreDatabaseAvailability,
|
|
886
|
+
_CacheCoreIdentityAdmission,
|
|
887
|
+
_CacheCoreNamespaceAdmission,
|
|
888
|
+
_CacheCoreUniqueKeyAdmission,
|
|
889
|
+
_CacheCoreLookup,
|
|
890
|
+
_CacheCoreStreamCostTelemetry,
|
|
891
|
+
):
|
|
892
|
+
__slots__ = ()
|