|
82 | 82 | import logging |
83 | 83 | import re |
84 | 84 | import threading |
| 85 | +import time |
85 | 86 | from collections.abc import Callable |
86 | 87 | from typing import TYPE_CHECKING, Any |
87 | 88 |
|
|
96 | 97 | if TYPE_CHECKING: |
97 | 98 | from psycopg_pool import ConnectionPool |
98 | 99 |
|
| 100 | + from adcp.audit_sink import AuditSink |
99 | 101 | from adcp.decisioning.registry_cache import CachingBuyerAgentRegistry |
100 | 102 |
|
101 | 103 | logger = logging.getLogger(__name__) |
@@ -417,6 +419,73 @@ def with_caching( |
417 | 419 | self.add_mutation_observer(lambda _op, _agent_url: cache.clear_sync()) |
418 | 420 | return cache |
419 | 421 |
|
| 422 | + def with_full_stack( |
| 423 | + self, |
| 424 | + *, |
| 425 | + ttl_seconds: float = 60.0, |
| 426 | + max_entries: int = 4096, |
| 427 | + hit_callback: Callable[[str], None] | None = None, |
| 428 | + rps_per_tenant: float = 100.0, |
| 429 | + burst: float | None = None, |
| 430 | + audit_sink: AuditSink | None = None, |
| 431 | + sink_timeout_seconds: float = 5.0, |
| 432 | + time_source: Callable[[], float] = time.monotonic, |
| 433 | + ) -> CachingBuyerAgentRegistry: |
| 434 | + """Return the canonical production registry wrapper stack. |
| 435 | +
|
| 436 | + Builds and returns ``Caching(RateLimited(Auditing(self)))``: |
| 437 | +
|
| 438 | + * cache is outermost so cached hits skip rate-limit accounting |
| 439 | + and DB work; |
| 440 | + * rate limiting applies only to cache misses that need inner |
| 441 | + resolution; |
| 442 | + * auditing wraps the SQL-backed store so DB ``resolved`` / |
| 443 | + ``miss`` outcomes are recorded. |
| 444 | +
|
| 445 | + ``audit_sink`` and ``sink_timeout_seconds`` are threaded through |
| 446 | + all three layers, so cache hits/misses, rate-limit rejects, and |
| 447 | + terminal DB outcomes can all land in the same audit trail. |
| 448 | + ``time_source`` is shared by the cache and rate limiter for |
| 449 | + deterministic tests. |
| 450 | +
|
| 451 | + Mutations through this :class:`PgBuyerAgentRegistry` instance |
| 452 | + clear the returned cache via the same observer wiring as |
| 453 | + :meth:`with_caching`. Adopters needing a different layer order |
| 454 | + should compose :class:`CachingBuyerAgentRegistry`, |
| 455 | + :class:`RateLimitedBuyerAgentRegistry`, and |
| 456 | + :class:`AuditingBuyerAgentRegistry` manually. |
| 457 | + """ |
| 458 | + from adcp.decisioning.registry_cache import ( |
| 459 | + AuditingBuyerAgentRegistry, |
| 460 | + CachingBuyerAgentRegistry, |
| 461 | + RateLimitedBuyerAgentRegistry, |
| 462 | + ) |
| 463 | + |
| 464 | + audited = AuditingBuyerAgentRegistry( |
| 465 | + self, |
| 466 | + audit_sink=audit_sink, |
| 467 | + sink_timeout_seconds=sink_timeout_seconds, |
| 468 | + ) |
| 469 | + rate_limited = RateLimitedBuyerAgentRegistry( |
| 470 | + audited, |
| 471 | + rps_per_tenant=rps_per_tenant, |
| 472 | + burst=burst, |
| 473 | + audit_sink=audit_sink, |
| 474 | + sink_timeout_seconds=sink_timeout_seconds, |
| 475 | + time_source=time_source, |
| 476 | + ) |
| 477 | + cache = CachingBuyerAgentRegistry( |
| 478 | + rate_limited, |
| 479 | + ttl_seconds=ttl_seconds, |
| 480 | + max_entries=max_entries, |
| 481 | + hit_callback=hit_callback, |
| 482 | + audit_sink=audit_sink, |
| 483 | + sink_timeout_seconds=sink_timeout_seconds, |
| 484 | + time_source=time_source, |
| 485 | + ) |
| 486 | + self.add_mutation_observer(lambda _op, _agent_url: cache.clear_sync()) |
| 487 | + return cache |
| 488 | + |
420 | 489 | def _notify_mutation(self, op: str, agent_url: str) -> None: |
421 | 490 | """Fire registered observers; log and swallow exceptions.""" |
422 | 491 | with self._mutation_observers_lock: |
|
0 commit comments