feat(registry): implement client-side LRU cache with TTL and ID validation #28

Closed
opened 2026-06-05 17:34:25 +00:00 by CoreRasurae · 2 comments
Member

Metadata

Commit Message: feat(registry): implement RegistryCache with LRU eviction and TTL
Branch: feature/m1-registry-client-cache

Background and context

The Package Registry Standard v1.0.0 (§10.3) requires clients to cache retrieved packages locally, validate cached content by recomputing SHA-1, and refresh based on TTL. This reduces network roundtrips and ensures content integrity.

Part of Epic: Package Registry Client — Support Package Registry Standard v1.0.0

Current behavior

No caching mechanism exists.

Expected behavior

  • RegistryCache with LRU eviction and configurable max size
  • TTL-based expiration: entries expire after configurable duration
  • Content validation: cached entries validated via SHA-1 recomputation
  • Tamper detection: mismatched SHA-1 triggers re-fetch
  • Thread-safe via asyncio locks
  • Integrates with RegistryClient as transparent caching layer

Acceptance criteria

  • Cache hit returns stored content without HTTP call
  • Stale entries (beyond TTL) trigger re-fetch
  • Tampered cache entries detected → re-fetch
  • LRU eviction respects size limit
  • Cache statistics exposed for observability
  • Concurrent access does not corrupt cache state

Subtasks

  • Create src/cleveractors/registry/cache.py with RegistryCache class
  • Implement LRU eviction policy with configurable max size
  • Implement TTL-based expiration with configurable duration
  • Implement content validation via SHA-1 recomputation
  • Integrate with RegistryClient as transparent caching layer
  • Expose cache statistics (hits, misses, evictions)
  • Tests (Behave): features/registry_cache.feature — hit/miss, TTL expiry, tamper detection, LRU eviction
  • Verify coverage >=97% via nox -s coverage_report
  • Run nox (all default sessions), fix any errors

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line matches the Commit Message in Metadata exactly.
  • The commit is pushed to the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a PR to master, reviewed, and merged.
## Metadata Commit Message: feat(registry): implement RegistryCache with LRU eviction and TTL Branch: feature/m1-registry-client-cache ## Background and context The Package Registry Standard v1.0.0 (§10.3) requires clients to cache retrieved packages locally, validate cached content by recomputing SHA-1, and refresh based on TTL. This reduces network roundtrips and ensures content integrity. Part of Epic: Package Registry Client — Support Package Registry Standard v1.0.0 ## Current behavior No caching mechanism exists. ## Expected behavior - `RegistryCache` with LRU eviction and configurable max size - TTL-based expiration: entries expire after configurable duration - Content validation: cached entries validated via SHA-1 recomputation - Tamper detection: mismatched SHA-1 triggers re-fetch - Thread-safe via asyncio locks - Integrates with `RegistryClient` as transparent caching layer ## Acceptance criteria - Cache hit returns stored content without HTTP call - Stale entries (beyond TTL) trigger re-fetch - Tampered cache entries detected → re-fetch - LRU eviction respects size limit - Cache statistics exposed for observability - Concurrent access does not corrupt cache state ## Subtasks - [ ] Create `src/cleveractors/registry/cache.py` with `RegistryCache` class - [ ] Implement LRU eviction policy with configurable max size - [ ] Implement TTL-based expiration with configurable duration - [ ] Implement content validation via SHA-1 recomputation - [ ] Integrate with `RegistryClient` as transparent caching layer - [ ] Expose cache statistics (hits, misses, evictions) - [ ] Tests (Behave): features/registry_cache.feature — hit/miss, TTL expiry, tamper detection, LRU eviction - [ ] Verify coverage >=97% via nox -s coverage_report - [ ] Run nox (all default sessions), fix any errors ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the first line matches the Commit Message in Metadata exactly. - The commit is pushed to the branch matching the Branch in Metadata exactly. - The commit is submitted as a PR to master, reviewed, and merged.
CoreRasurae added this to the v2.1.0 milestone 2026-06-05 17:34:25 +00:00
CoreRasurae changed title from Client-side caching — LRU cache with TTL and ID validation to feat(registry): implement client-side LRU cache with TTL and ID validation 2026-06-05 17:47:52 +00:00
Author
Member

Implemented by PR #42 (#42)

Implemented by PR #42 (https://git.cleverthis.com/cleveragents/cleveractors-core/pulls/42)
Author
Member

Integration Directives — RegistryCache ↔ PackageContentResolver

This comment documents the integration architecture that was followed when wiring the RegistryCache into the PackageContentResolver, consistent with Package Registry Standard v1.0.0 §10.3 and the project's SOLID principles.


Integration Directives Applied

Directive 1 — Two-tier caching architecture

The PackageContentResolver now operates with two distinct cache layers serving different purposes:

Tier Store Key Purpose
Resolution OrderedDict (LRU, max 128) reference:type Fast reference → enriched content dicts with _original_reference, _server, _namespace, _name, _version metadata
Content RegistryCache (LRU+TTL+SHA-1) package_id Raw package content caching per §10.3; one cache per server URL

Resolution cache handles the fast path for resolve() / aresolve() callers. Content cache handles the content-integrity path for get_package() callers and is warmed automatically by resolution.

Directive 2 — CacheFactory (Factory Method + Dependency Injection)

CacheFactory centralises cache creation so that:

  • Cache configuration (max_size, ttl, validate_content) is specified once at the factory level
  • PackageContentResolver depends on the factory abstraction, not on RegistryCache directly
  • Future alternative implementations (disk-backed, distributed) can be selected by changing the factory without touching consumer code
# Dependency Injection — factory passed as constructor argument
resolver = PackageContentResolver(
    api_key="...",
    cache_factory=CacheFactory(max_size=256, ttl=300.0),
)

When cache_factory=None, the content cache tier is disabled and only the resolution cache is used — full backward compatibility.

Directive 3 — Content warming on resolution

When _resolve_registry_async() fetches content via RegistryClient.resolve_package(), the package_id from the response is used to warm the per-server content cache via RegistryCache.put(). Subsequent get_package() calls for the same package_id through the same server hit the local content cache instead of the network. Warming failures are logged at debug level and never block resolution.

Directive 4 — Resource lifecycle consistency

close_all() now closes content caches FIRST (under the async lock) before closing raw clients, so that in-flight cache operations complete cleanly. aclear_cache() clears both resolution and content tiers in one async operation.

Directive 5 — Single-flight is preserved

RegistryCache._singleflight_fetch coalesces concurrent misses into a single upstream fetch. This behaviour is preserved by the integration — when get_package_cached() delegates to RegistryCache.get_package(), concurrent callers for the same cold key share a single upstream request.


Patterns Used

Pattern Where Why
Factory Method CacheFactory.create() Decouples cache construction from consumption
Dependency Injection PackageContentResolver(cache_factory=...) Testable, configurable, swappable
Strategy RegistryCache is the content-cache strategy when cache_factory is not None Clean fallback when disabled
Adapter get_package_cached(package_id, server) Bridges resolution domain (server URLs) and content domain (package IDs)
SOLID: SRP CacheFactory (creation), RegistryCache (caching), PackageContentResolver (resolution) Each class has one reason to change
SOLID: DIP PackageContentResolver depends on CacheFactory, not RegistryCache High-level module not coupled to low-level cache implementation

Files Changed

File Change
src/cleveractors/registry/cache.py Added CacheFactory class and RegistryCache.put() method
src/cleveractors/registry/reference_resolver.py PackageContentResolver now accepts cache_factory, manages per-server content caches, warms cache on resolution, closes caches on shutdown, exposes get_package_cached() and aclear_cache()
src/cleveractors/registry/__init__.py Exported CacheFactory

Integration documented by Luis

## Integration Directives — RegistryCache ↔ PackageContentResolver This comment documents the integration architecture that was followed when wiring the `RegistryCache` into the `PackageContentResolver`, consistent with Package Registry Standard v1.0.0 §10.3 and the project's SOLID principles. --- ### Integration Directives Applied **Directive 1 — Two-tier caching architecture** The `PackageContentResolver` now operates with two distinct cache layers serving different purposes: | Tier | Store | Key | Purpose | |------|-------|-----|---------| | Resolution | `OrderedDict` (LRU, max 128) | `reference:type` | Fast reference → enriched content dicts with `_original_reference`, `_server`, `_namespace`, `_name`, `_version` metadata | | Content | `RegistryCache` (LRU+TTL+SHA-1) | `package_id` | Raw package content caching per §10.3; one cache per server URL | Resolution cache handles the fast path for `resolve()` / `aresolve()` callers. Content cache handles the content-integrity path for `get_package()` callers and is warmed automatically by resolution. **Directive 2 — CacheFactory (Factory Method + Dependency Injection)** `CacheFactory` centralises cache creation so that: - Cache configuration (`max_size`, `ttl`, `validate_content`) is specified once at the factory level - `PackageContentResolver` depends on the factory abstraction, not on `RegistryCache` directly - Future alternative implementations (disk-backed, distributed) can be selected by changing the factory without touching consumer code ```python # Dependency Injection — factory passed as constructor argument resolver = PackageContentResolver( api_key="...", cache_factory=CacheFactory(max_size=256, ttl=300.0), ) ``` When `cache_factory=None`, the content cache tier is disabled and only the resolution cache is used — full backward compatibility. **Directive 3 — Content warming on resolution** When `_resolve_registry_async()` fetches content via `RegistryClient.resolve_package()`, the `package_id` from the response is used to warm the per-server content cache via `RegistryCache.put()`. Subsequent `get_package()` calls for the same `package_id` through the same server hit the local content cache instead of the network. Warming failures are logged at debug level and never block resolution. **Directive 4 — Resource lifecycle consistency** `close_all()` now closes content caches FIRST (under the async lock) before closing raw clients, so that in-flight cache operations complete cleanly. `aclear_cache()` clears both resolution and content tiers in one async operation. **Directive 5 — Single-flight is preserved** `RegistryCache._singleflight_fetch` coalesces concurrent misses into a single upstream fetch. This behaviour is preserved by the integration — when `get_package_cached()` delegates to `RegistryCache.get_package()`, concurrent callers for the same cold key share a single upstream request. --- ### Patterns Used | Pattern | Where | Why | |---------|-------|-----| | **Factory Method** | `CacheFactory.create()` | Decouples cache construction from consumption | | **Dependency Injection** | `PackageContentResolver(cache_factory=...)` | Testable, configurable, swappable | | **Strategy** | `RegistryCache` is the content-cache strategy when `cache_factory is not None` | Clean fallback when disabled | | **Adapter** | `get_package_cached(package_id, server)` | Bridges resolution domain (server URLs) and content domain (package IDs) | | **SOLID: SRP** | `CacheFactory` (creation), `RegistryCache` (caching), `PackageContentResolver` (resolution) | Each class has one reason to change | | **SOLID: DIP** | `PackageContentResolver` depends on `CacheFactory`, not `RegistryCache` | High-level module not coupled to low-level cache implementation | --- ### Files Changed | File | Change | |------|--------| | `src/cleveractors/registry/cache.py` | Added `CacheFactory` class and `RegistryCache.put()` method | | `src/cleveractors/registry/reference_resolver.py` | `PackageContentResolver` now accepts `cache_factory`, manages per-server content caches, warms cache on resolution, closes caches on shutdown, exposes `get_package_cached()` and `aclear_cache()` | | `src/cleveractors/registry/__init__.py` | Exported `CacheFactory` | --- *Integration documented by Luis*
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveractors-core#28
No description provided.