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.
Files changed (39) hide show
  1. client_query_cache/__init__.py +15 -0
  2. client_query_cache/_core/__init__.py +44 -0
  3. client_query_cache/_core/canonical.py +63 -0
  4. client_query_cache/_core/codec.py +55 -0
  5. client_query_cache/_core/collation.py +22 -0
  6. client_query_cache/_core/collection_metadata.py +65 -0
  7. client_query_cache/_core/entries.py +32 -0
  8. client_query_cache/_core/errors.py +25 -0
  9. client_query_cache/_core/identity_reads.py +64 -0
  10. client_query_cache/_core/keys.py +43 -0
  11. client_query_cache/_core/lifecycle.py +8 -0
  12. client_query_cache/_core/locking.py +40 -0
  13. client_query_cache/_core/lru.py +109 -0
  14. client_query_cache/_core/manager.py +892 -0
  15. client_query_cache/_core/namespace.py +35 -0
  16. client_query_cache/_core/order_sensitive_keys.py +69 -0
  17. client_query_cache/_core/projection.py +59 -0
  18. client_query_cache/_core/read_validation.py +89 -0
  19. client_query_cache/_core/snapshots.py +67 -0
  20. client_query_cache/_core/stream_cost.py +242 -0
  21. client_query_cache/_core/stream_events.py +157 -0
  22. client_query_cache/_core/stream_health.py +47 -0
  23. client_query_cache/_core/stream_options.py +14 -0
  24. client_query_cache/_core/unique_keys.py +119 -0
  25. client_query_cache/asynchronous/__init__.py +16 -0
  26. client_query_cache/asynchronous/collection.py +682 -0
  27. client_query_cache/asynchronous/database.py +59 -0
  28. client_query_cache/asynchronous/manager.py +118 -0
  29. client_query_cache/asynchronous/streams.py +300 -0
  30. client_query_cache/otel.py +206 -0
  31. client_query_cache/py.typed +0 -0
  32. client_query_cache/synchronous/__init__.py +16 -0
  33. client_query_cache/synchronous/collection.py +678 -0
  34. client_query_cache/synchronous/database.py +53 -0
  35. client_query_cache/synchronous/manager.py +118 -0
  36. client_query_cache/synchronous/streams.py +297 -0
  37. client_query_cache-0.1.0.dist-info/METADATA +114 -0
  38. client_query_cache-0.1.0.dist-info/RECORD +39 -0
  39. 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__ = ()