Skip to content

feat(cache): content-based cache keys with SHA-256 signatures (1/2) - #106

Merged
yacosta738 merged 2 commits into
mainfrom
read-cache
Jun 5, 2026
Merged

yacosta738 merged 2 commits into
mainfrom
read-cache

Conversation

@yacosta738

Copy link
Copy Markdown
Contributor

Issue

Closes #50 (Part 1 of 2)

Summary

Implements content-based caching foundation with SHA-256 signatures, LRU eviction, and stats tracking.

⚠️ Breaking Change: CacheKey now includes signature: String field (documented in proposal)

What Changed

Phase 1: Foundation

  • ✅ Added signature: String field to CacheKey (breaking change)
  • ✅ Implemented Display trait for CacheKey (shows first 8 chars of signature)
  • ✅ Added CacheStats struct with hit_rate() and utilization() methods
  • ✅ Added sha2 and hex dependencies to rook-core
  • ✅ Implemented SHA-256 content hashing in CompletionRequest::cache_key()

Phase 2: Infrastructure

  • ✅ Added LRU tracking with DashMap<CacheKey, Instant> for last_accessed timestamps
  • ✅ Added AtomicU64 counters (hits, misses, evictions) to InMemoryCache
  • ✅ Updated InMemoryCache::new() constructor to accept max_entries: Option<usize>
  • ✅ Implemented LRU eviction in set() when capacity reached
  • ✅ Updated get() to track hits/misses and update last_accessed
  • ✅ Updated clear() to reset all stats
  • ✅ Implemented stats() method returning CacheStats

Phase 8: Unit Tests

  • ✅ 16 comprehensive unit tests covering:
    • Hash determinism (100 iterations)
    • Field inclusion/exclusion
    • LRU eviction (4 tests: at capacity, access order, below capacity, unlimited)
    • Stats accuracy (hits/misses, evictions, entries)
    • Concurrent access safety
    • CacheStats methods (hit_rate, utilization)

Verification

✅ 443 tests passed (16 new cache tests)
✅ cargo clippy: 0 warnings
✅ cargo fmt: formatted
✅ cargo doc: generated
✅ cargo audit: 0 vulnerabilities

Files Changed

  • crates/domain/shared-kernel/src/lib.rs — CacheKey signature field + Display + test helper
  • crates/domain/rook-core/src/model.rs — CacheStats struct + SHA-256 hashing + 11 unit tests
  • crates/infrastructure/cache-memory/src/lib.rs — LRU + stats counters + 12 unit tests
  • crates/domain/rook-core/Cargo.toml — sha2 + hex dependencies
  • apps/rook/src/di.rs — updated InMemoryCache constructor call

Total: ~657 lines (higher than estimated due to comprehensive tests)

Next Steps (PR 2)

  • Phase 3: Ports (CachePort::stats method)
  • Phase 4: Configuration (max_entries field, TTL validation)
  • Phase 5: Application (stats tracking in RouteRequest)
  • Phase 6: Transport (HTTP cache management endpoints)
  • Phase 7: Observability (Prometheus metrics)
  • Phase 9: Integration tests
  • Phase 10: Final verification

SDD Artifacts

Full SDD cycle completed:

  • ✅ Exploration
  • ✅ Proposal
  • ✅ Spec (7 requirements, 32 scenarios)
  • ✅ Design (architecture decisions, component design)
  • ✅ Tasks (47 tasks across 10 phases)

Artifacts available in: openspec/changes/read-cache/

Implement content-based caching for issue #50:
- Add signature field to CacheKey (SHA-256 hex string)
- Update CompletionRequest.cache_key() to hash (model + messages + params)
- Add CacheStats struct with hit_rate() and utilization() methods
- Add LRU eviction to InMemoryCache with max_entries support
- Add AtomicU64 counters (hits, misses, evictions)
- Implement stats() method in InMemoryCache
- Add comprehensive unit tests for hashing, LRU, and stats

Breaking change: CacheKey now requires signature field (documented in proposal)
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@yacosta738, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 33 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7473ba3d-07e6-4ee4-b0d0-463513f87592

📥 Commits

Reviewing files that changed from the base of the PR and between 1aea5ee and aa96345.

📒 Files selected for processing (3)
  • crates/infrastructure/cache-memory/src/lib.rs
  • openspec/changes/read-cache/specs/cache/spec.md
  • openspec/changes/read-cache/tasks.md
📝 Walkthrough

Walkthrough

This PR implements a semantic response cache using content-based signatures derived from SHA-256 hashing of request semantic fields (model, messages, parameters). The implementation spans domain contract extensions, infrastructure storage with LRU eviction and atomic statistics tracking, dependency injection wiring, and comprehensive design documentation defining the architecture and acceptance criteria.

Changes

Semantic Read Cache

Layer / File(s) Summary
CacheKey extension with signature field
crates/domain/shared-kernel/src/lib.rs
CacheKey struct adds a signature: String field to hold SHA-256 content hashes. Conversions initialize signature to empty, a test_key() helper creates keys with explicit signatures, and Display formatting now shows request_id:signature_preview.
Cache key computation and statistics types
crates/domain/rook-core/Cargo.toml, crates/domain/rook-core/src/model.rs
sha2 and hex dependencies added. CompletionRequest::cache_key() computes deterministic SHA-256 hashes over canonical JSON of semantic fields (model, messages, max_tokens, temperature, tools, tool_choice), producing 64-character hex signatures. New CacheStats type tracks hits, misses, evictions, and entries with hit_rate() and utilization() helper methods; unit tests validate key determinism, field inclusion/exclusion, and stats edge cases.
InMemoryCache LRU eviction and statistics
crates/infrastructure/cache-memory/src/lib.rs
InMemoryCache now tracks last-access timestamps per key for LRU eviction, accepts optional max_entries capacity, and maintains atomic counters for hits, misses, and evictions. get() updates last-access and increments counters; set() evicts oldest entry before insertion; clear() resets statistics. New stats() method exposes cache metrics. Tests updated to use signature-based keys; new test coverage includes LRU behavior (capacity eviction, access-order tracking, no eviction when unlimited), stats accuracy, and concurrent access safety.
Dependency injection configuration
apps/rook/src/di.rs
InMemoryCache instantiation updated to pass None for the new max_entries parameter in enabled-cache code path.
Design specification and implementation planning
openspec/changes/read-cache/design.md, openspec/changes/read-cache/exploration.md, openspec/changes/read-cache/proposal.md, openspec/changes/read-cache/specs/cache/spec.md, openspec/changes/read-cache/state.yaml, openspec/changes/read-cache/tasks.md
Comprehensive planning documents define semantic content hashing strategy, LRU eviction with last-access tracking, atomic statistics counters, HTTP cache management endpoints (GET /api/cache/stats, DELETE /api/cache, DELETE /api/cache/:signature), TTL validation (24-hour max), metrics integration, configuration validation, testing strategy, and phased implementation tasks with acceptance criteria and rollback procedures.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A rabbit hops through cache lines with glee,
Hashing requests down to their core decree,
SHA-256 signatures dance, LRU sway,
Evicting old entries to make room for the day,
Statistics emerge—hits and misses take flight,
Response caching now shines ever bright! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly identifies the main change: content-based cache keys with SHA-256 signatures and notes this is part 1 of 2.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, detailing all implemented features, verification results, and next steps.
Linked Issues check ✅ Passed The PR implements core foundation requirements from #50: SHA-256 content hashing [#50], LRU eviction structure [#50], stats tracking [#50], CacheKey signature field [#50], and comprehensive unit tests validating these implementations.
Out of Scope Changes check ✅ Passed All changes are in-scope for the stated Phase 1/2 foundation work: caching infrastructure (CacheKey, CacheStats, InMemoryCache), domain hashing (CompletionRequest), dependencies, and DI wiring. Documentation changes provide necessary design context without code scope creep.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch read-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/infrastructure/cache-memory/src/lib.rs (1)

96-112: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor inefficiency: eviction triggered even when overwriting existing key.

When set() overwrites an existing key, evict_if_needed() is still called. If the cache is at capacity, this unnecessarily evicts an entry even though no new slot is needed.

This is not a correctness bug—the cache still works correctly—but it may cause premature evictions during update-heavy workloads.

♻️ Optional fix to skip eviction on overwrites
     async fn set(
         &self,
         key: &CacheKey,
         value: &CompletionResponse,
         ttl: Duration,
     ) -> CortexResult<()> {
-        // Evict oldest entry if at capacity
-        self.evict_if_needed();
+        // Evict oldest entry if at capacity and not overwriting
+        if !self.store.contains_key(key) {
+            self.evict_if_needed();
+        }

         self.store.insert(key.clone(), value.clone());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/infrastructure/cache-memory/src/lib.rs` around lines 96 - 112, The set
method calls evict_if_needed unconditionally which can evict when overwriting an
existing key; change set in cache-memory::lib.rs to check if key already exists
in self.store (e.g., via self.store.contains_key(key) or retrieval) and only
call evict_if_needed when inserting a new key (not when updating), then proceed
to insert/update expiry and last_accessed as before; reference functions/fields:
set, evict_if_needed, self.store, self.expiry, self.last_accessed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/infrastructure/cache-memory/src/lib.rs`:
- Around line 52-71: Add a short documentation note to evict_if_needed (and/or
the Cache type) stating that LRU eviction is approximate under concurrency:
explain that evict_if_needed finds the oldest key by iterating last_accessed and
may race with other threads (an entry could be accessed between selection and
removal), so strict LRU is not guaranteed without heavier locking; reference the
evict_if_needed method, last_accessed map, and the eviction counter (evictions)
so maintainers know where the approximation occurs and why it's acceptable for
performance.

In `@openspec/changes/read-cache/specs/cache/spec.md`:
- Around line 186-196: The spec currently conflicts with the code's
CacheConfig.max_entries (Option<usize>) and InMemoryCache semantics — update the
spec to clarify which interpretation you choose: either (A) treat 0/None as
"unlimited" by allowing max_entries = 0 (or prefer explicit None) and change the
"Reject zero" scenario to only reject negative values removed and instead
validate only when max_entries.is_some(), or (B) enforce non-zero capacity by
changing the config type to Option<NonZeroUsize> (and adjust tasks/architecture
and InMemoryCache accordingly) and update the scenarios to reject zero; also
remove the impossible "negative" case since usize cannot be negative. Ensure you
reference CacheConfig.max_entries and InMemoryCache in the spec so the behavior
matches the implementation and issue `#50` objectives.

In `@openspec/changes/read-cache/tasks.md`:
- Line 54: Clarify and implement explicit validation in CacheConfig::validate()
(apps/rook/src/config.rs): keep the existing ttl_secs check (reject > 86400) and
add a deterministic rule for max_entries such that if max_entries is Some(n) it
must be n > 0 (reject zero or negative), and if max_entries is None allow it
(unlimited capacity); return an error when the max_entries Some constraint fails
and otherwise pass validation.

---

Outside diff comments:
In `@crates/infrastructure/cache-memory/src/lib.rs`:
- Around line 96-112: The set method calls evict_if_needed unconditionally which
can evict when overwriting an existing key; change set in cache-memory::lib.rs
to check if key already exists in self.store (e.g., via
self.store.contains_key(key) or retrieval) and only call evict_if_needed when
inserting a new key (not when updating), then proceed to insert/update expiry
and last_accessed as before; reference functions/fields: set, evict_if_needed,
self.store, self.expiry, self.last_accessed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d676158b-a563-48b5-8cf4-1ea84df72d6e

📥 Commits

Reviewing files that changed from the base of the PR and between ff49b7a and 1aea5ee.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • apps/rook/src/di.rs
  • crates/domain/rook-core/Cargo.toml
  • crates/domain/rook-core/src/model.rs
  • crates/domain/shared-kernel/src/lib.rs
  • crates/infrastructure/cache-memory/src/lib.rs
  • openspec/changes/read-cache/design.md
  • openspec/changes/read-cache/exploration.md
  • openspec/changes/read-cache/proposal.md
  • openspec/changes/read-cache/specs/cache/spec.md
  • openspec/changes/read-cache/state.yaml
  • openspec/changes/read-cache/tasks.md

Comment thread crates/infrastructure/cache-memory/src/lib.rs Outdated
Comment thread openspec/changes/read-cache/specs/cache/spec.md Outdated
Comment thread openspec/changes/read-cache/tasks.md Outdated
- Add documentation for approximate LRU under concurrency
- Fix evict_if_needed to only trigger on new keys (not overwrites)
- Add test for no-eviction-on-overwrite behavior
- Clarify spec: None=unlimited, Some(0)=rejected (usize cannot be negative)
- Update tasks to specify max_entries validation rules
@yacosta738
yacosta738 merged commit 5c25ed3 into main Jun 5, 2026
10 of 11 checks passed
@yacosta738
yacosta738 deleted the read-cache branch June 5, 2026 05:46
@dallay-bot dallay-bot Bot mentioned this pull request Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read Cache (Response Caching)

1 participant