Skip to content

fix(metrics): sync L2 hit size_bytes measures the served envelope (LAB-3768) - #298

Open
27Bslash6 wants to merge 2 commits into
mainfrom
lab-3768-sync-l2-size-bytes
Open

27Bslash6 wants to merge 2 commits into
mainfrom
lab-3768-sync-l2-size-bytes

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Problem

cache_operation_size_bytes{operation="get",serializer="rust"} was fed a different quantity at the sync L2 hit site than at the sync L1 hit site. The L1 site records len(l1_bytes), the envelope it served (#297 brings the async hit sites to the same quantity). The sync L2 site recorded len(str(value).encode("utf-8")), a repr estimate of the deserialized value, because the sync handler returned only (True, value) with no envelope.

One process mixing L1 and L2 hits, or any @cache.secure caller (ciphertext envelope vs plaintext repr), got a bimodal histogram on the same series. Measured on a trivial {"answer": 42}: the estimate reported 14 bytes, the served envelope is 168.

Fix

  • get_cached_value and get_cached_value_with_freshness return (True, value, size_bytes), the length of the L2 envelope actually served. On the mmap fast path that is handle.view.nbytes, read before close(); on the bytes path len(cached_data). Both are st_size - HEADER_SIZE on the File backend, so the two paths report the same number for the same entry.
  • The sync L2 hit site unpacks the tuple by name (mirroring the async site) and records that length. The estimate computed before log_cache_operation was dead code (that call takes **kwargs and no site passes size_bytes); deleted rather than wired in so the log call stays symmetric with the L1 sites.

Why a length and not the raw bytes like the async variant: the mmap view dangles once its handle closes (#171) and must never leave the read frame, so returning bytes there means a full copy of the payload, which is what the mmap path exists to avoid (tests/performance/test_large_object_memory.py measures peak memory on this exact call). The sync path performs no L1 backfill (#164), so nothing downstream needs the bytes. The async variant keeps carrying raw bytes because it does backfill.

Tests

  • New tests/unit/test_sync_l2_hit_size_bytes.py: a sync L2 hit records size_bytes == len(stored envelope), patched at FeatureOrchestrator.record_cache_operation. Fails on the previous code (14 == 168).
  • test_mmap_read_path.py real-mmap end-to-end now asserts the reported size equals the stored envelope length.
  • Tuple-shape asserts updated in test_decrypt_fail_policy.py, test_swr_decorator.py, test_mmap_read_path.py.

Local: ruff check / ruff format --check clean; tests/unit -m "not slow" 2261 passed; tests/critical -m "not slow" 244 passed; doctest + markdown-docs collection green; File read-path memory-budget perf tests green.

Docs

Internal handler return shape only; docstrings updated in the same diff. docs/api-reference.md and docs/features/prometheus-metrics.md already describe cache_operation_size_bytes as payload size, which is now true at every sync site. No docs.cachekit.io change.

Summary by CodeRabbit

  • Bug Fixes
    • Cache hit results now report the stored entry size accurately.
    • Size reporting is consistent across memory-mapped, byte-based and fallback reads.
    • Freshness-aware cache lookups now include the same size information while preserving freshness details.
    • Synchronous cache operations no longer estimate size from the returned value, improving monitoring and cache usage metrics.
  • Tests
    • Added coverage for size reporting across supported cache read paths and cached data formats.

Summary

This PR fixes a metrics inconsistency where the sync L2 cache hit path was recording an incorrect size_bytes value.

Problem

When a sync cache hit occurred at the L2 level, the size_bytes metric was recording len(str(value).encode()) — a repr-based estimate of the deserialized value — rather than the actual length of the served L2 envelope. For secure caches, this meant comparing ciphertext envelope sizes against plaintext repr sizes, producing a bimodal histogram on a single metric series.

Changes

  • cache_handler.py: Updated the get_cached_value docstring to clarify that the returned size_bytes is the L2 envelope length, aligning it with the L1 hit site which records len(l1_bytes). Removed the now-inaccurate reference to the async path.

  • wrapper.py: Refactored the sync wrapper to unpack the (True, value, size_bytes) tuple into named variables (_found, result, size_bytes) instead of using positional index access (cached_result[1], cached_result[2]), improving readability of the hit-metric recording.

  • test_sync_l2_hit_size_bytes.py: Simplified the test by disabling L1 (l1_enabled=False) instead of relying on L1-clearing fixtures to force the code path to the L2 hit site. Removed the _clear_l1 autouse fixture and the separate recorded fixture, inlining the orchestrator patching directly into the test.

Impact

The sync L2 hit metric now measures the same quantity as the L1 hit site, ensuring consistent size_bytes reporting across cache tiers and avoiding misleading histograms for secure caches.

…B-3768)

Every hit site but one fed cache_operation_size_bytes with the length of the
envelope it served: sync L1 and async L1 use len(l1_bytes), async L2 uses the
raw bytes get_cached_value_async already returns. The sync L2 site recorded
len(str(value).encode()) instead, a repr estimate of the deserialized value,
because the sync handler returned only (True, value). One process mixing sync
and async callers, or any secure cache (ciphertext envelope vs plaintext repr),
got a bimodal histogram on the same series: 14 vs 168 bytes for {"answer": 42}.

get_cached_value and get_cached_value_with_freshness now return
(True, value, size_bytes), the served L2 envelope length. It is a length, not
the envelope itself, because the mmap fast path's view dangles once its handle
closes (#171) and returning bytes there would mean a full copy; the sync path
has no L1 backfill (#164) so nothing needs the bytes. The async variant keeps
carrying raw bytes because it does backfill.

The estimate computed before log_cache_operation was dead code: that call takes
**kwargs and no site passes size_bytes. Deleted rather than wired in, so the
log call stays symmetric with the L1 sites.
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: bfa96813-cf2b-49d7-a6aa-99f58ab8bc59

📥 Commits

Reviewing files that changed from the base of the PR and between 73a0dfa and d76b56e.

📒 Files selected for processing (3)
  • src/cachekit/cache_handler.py
  • src/cachekit/decorators/wrapper.py
  • tests/unit/test_sync_l2_hit_size_bytes.py

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.


Walkthrough

Cache read hits now return the served envelope byte length. Freshness results carry the same value. Sync L2 metrics record this length instead of estimating it from the deserialised value.

Changes

Cache hit size propagation

Layer / File(s) Summary
Cache read result contract
src/cachekit/cache_handler.py
get_cached_value and get_cached_value_with_freshness now return the served envelope length with cache hits for mmap and byte-based reads.
L2 metric size recording
src/cachekit/decorators/wrapper.py
Sync L2 statistics read the envelope length from the cached result instead of recalculating it from the value representation.
Cache size regression coverage
tests/unit/test_decrypt_fail_policy.py, tests/unit/test_mmap_read_path.py, tests/unit/test_swr_decorator.py, tests/unit/test_sync_l2_hit_size_bytes.py
Tests validate size-bearing results for decrypt, mmap, freshness, and synchronous L2 hit paths.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to d76b5

The cache-hit size propagation change has no identified merge-blocking issue.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: correcting sync L2 hit size metrics to measure the served envelope. It is concise and specific.
Description check ✅ Passed The description explains the problem, motivation, implementation, test coverage, documentation impact, and compatibility context. It does not use every template heading or checklist item, but it provi…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch lab-3768-sync-l2-size-bytes

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

@kodus-27b

This comment has been minimized.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 17, 2026
kodus-27b[bot]
kodus-27b Bot previously approved these changes Sep 17, 2026
@codecov

codecov Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

27Bslash6 pushed a commit that referenced this pull request Sep 17, 2026
… (LAB-3765)

The sync L2 site measures the served envelope as of #298; this comment described the state it was written against and would read as a lie once both land. The remaining sentence is about this site only.
…ain (LAB-3768)

The previous commit claimed the async hit sites already record the served envelope length. On this base they do not; #297 brings them there. Docstring, wrapper comment and test docstring now state the fact that holds today: the same quantity the sync L1 site records via len(l1_bytes).

The hit site unpacks the tuple by name, mirroring the async site, instead of reading [1] and [2]. The test runs with l1_enabled=False so the L2 site is reached without an L1 singleton to prime and clear, and the one-use fixture is inlined with its rationale kept as a comment.
@kodus-27b

kodus-27b Bot commented Sep 17, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

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.

1 participant