Follow-up to #380 / #692.
Context
PgBuyerAgentRegistry.with_caching(**kwargs) shipped in #692 collapses cache wiring into one call. But the canonical production stack documented in registry_cache.py is three layers:
Caching(RateLimited(Auditing(inner)))
Adopters who want the full stack still hand-compose:
pg = PgBuyerAgentRegistry(pool=pool)
audited = AuditingBuyerAgentRegistry(pg, audit_sink=sink)
limited = RateLimitedBuyerAgentRegistry(audited, rps_per_tenant=100, audit_sink=sink)
cache = CachingBuyerAgentRegistry(limited, ttl_seconds=60, audit_sink=sink)
pg.add_mutation_observer(lambda _op, _url: cache.clear_sync())
serve(buyer_agent_registry=cache, ...)
The mutation-observer wiring at the end is the part most adopters will forget.
Proposed
registry = PgBuyerAgentRegistry(pool=pool).with_full_stack(
ttl_seconds=60,
max_entries=10_000,
rps_per_tenant=100,
audit_sink=sink,
)
serve(buyer_agent_registry=registry, ...)
Returns the outermost CachingBuyerAgentRegistry, pre-wired with the mutation observer, with audit_sink threaded through every layer that accepts one.
Tradeoffs
- More opinionated than
with_caching — fixes a layer order (`Cache -> RateLimit -> Audit -> inner`). Adopters who need a different order still go manual.
- Worth doing because the current manual path is the most-foot-shootable section of the registry composition story.
Alternative considered
Stacking method-chained factories (pg.with_caching(...).with_rate_limiting(...).with_auditing(...)) reads nicely but inverts the layer order — Auditing would end up outermost, which is wrong for this use case. The single-call factory dodges that confusion.
Acceptance
with_full_stack(**kwargs) on PgBuyerAgentRegistry
audit_sink parameter threaded to all three wrapper layers (so cache/rate-limit/resolve events all emit)
- Conformance test: mutation through
pg invalidates cache; rate-limit fires on burst; audit events emitted at each layer
- Docstring documents layer order and points to manual composition for non-default orderings
Follow-up to #380 / #692.
Context
PgBuyerAgentRegistry.with_caching(**kwargs)shipped in #692 collapses cache wiring into one call. But the canonical production stack documented inregistry_cache.pyis three layers:Adopters who want the full stack still hand-compose:
The mutation-observer wiring at the end is the part most adopters will forget.
Proposed
Returns the outermost
CachingBuyerAgentRegistry, pre-wired with the mutation observer, withaudit_sinkthreaded through every layer that accepts one.Tradeoffs
with_caching— fixes a layer order (`Cache -> RateLimit -> Audit -> inner`). Adopters who need a different order still go manual.Alternative considered
Stacking method-chained factories (
pg.with_caching(...).with_rate_limiting(...).with_auditing(...)) reads nicely but inverts the layer order —Auditingwould end up outermost, which is wrong for this use case. The single-call factory dodges that confusion.Acceptance
with_full_stack(**kwargs)onPgBuyerAgentRegistryaudit_sinkparameter threaded to all three wrapper layers (so cache/rate-limit/resolve events all emit)pginvalidates cache; rate-limit fires on burst; audit events emitted at each layer