Skip to content

cacheable - fix: size each key value in deleteMany stats - #1742

Merged
jaredwray merged 2 commits into
mainfrom
claude/great-archimedes-37p526
Oct 18, 2026
Merged

jaredwray merged 2 commits into
mainfrom
claude/great-archimedes-37p526

Conversation

@jaredwray

@jaredwray jaredwray commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Please check if the PR fulfills these requirements

  • Followed the Contributing guidelines and Code of Conduct
  • Tests for the changes have been added (for bug fixes/features) with 100% code coverage.

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)

Bug fix. Fixes #1738.

Problem

When stats are on, Cacheable.deleteMany(keys) had two defects:

  • It sized the full array of values once for each key. Stats.roughSizeOfObject used Array.prototype.includes for visited objects, so each call was O(n²). Together, the call was approximately cubic and blocked the event loop.
  • It changed count, ksize, vsize, and deletes for keys that were not in the cache.

Change

  • packages/cacheable/src/index.ts: deleteManyInternal reads the raw entries with getManyRaw(). It sizes only the value of each entry. It skips keys that are not in the cache. The raw entries let it count a cached undefined value, which get() cannot tell apart from a missing key.
  • packages/utils/src/stats.ts: roughSizeOfObject uses a Set for visited objects. A Set uses the same SameValueZero equality as includes. A WeakSet is not safe here, because a bigint can get to that branch.

Verification

Reproduction script from the issue, before and after the fix:

keys cached values before after
500 100 166 ms 10 ms
1,000 200 1,226 ms 13 ms
2,000 400 8,424 ms 19 ms
4,000 800 not run (approx. 70 s in the issue) 31 ms
14,000 14,000 not run 251 ms

Partial delete, deleteMany(["a", "missing1", "missing2"]) on 3 cached keys:

  • Before: count=0, ksize=0, vsize=0, deletes=3.
  • After: count=2, ksize=4, vsize=14, deletes=1. These are the correct values.

Checks:

  • pnpm test passes in packages/utils, packages/cacheable, packages/memory, and packages/node-cache, with a local Redis.
  • stats.ts has 100% coverage. The line that is not covered in cacheable/src/index.ts (line 422, the sync setter) was not covered before this PR.

Test audit:

  • Rewrote cacheable stats enabled > should get stats on delete many. This test owns the deleteMany stats contract, so I extended it and did not add a near-duplicate.
    • Contract: deleteMany changes the stats only for keys that exist, also when the cached value is undefined. It subtracts the size of each value one time.
    • The test deletes two existing keys (one holds undefined) and one missing key. It compares ksize and vsize with a cache that holds only the remaining key.
    • It fails on main (deletes is 3, expected 2). It fails on the first commit of this PR (deletes is 1, expected 2), because get() skipped the cached undefined value. The vsize check also fails if a change brings back the whole-array sizing, even when all keys exist.
  • No new test for the Set change in roughSizeOfObject. The behavior does not change, and only a timing test can find the regression. A timing test is not stable in CI. The circular-reference tests in packages/utils/test/stats.test.ts cover the visited-object check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T

Before, deleteMany with stats sized the full array of values once for
each key. The time was cubic in the input size, and it blocked the event
loop. It also changed count, ksize, and deletes for keys that were not
in the cache.

Now deleteMany sizes only the value of each key and skips missing keys.
Stats.roughSizeOfObject uses a Set for visited objects, so each call is
linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T23:13:47.160506Z 33af2ab PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (d5e3d88) to head (eed4a05).

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #1742   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           29        29           
  Lines         3612      3615    +3     
  Branches       809       809           
=========================================
+ Hits          3612      3615    +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33af2ab3e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/cacheable/src/index.ts Outdated
Comment thread packages/cacheable/src/index.ts
Comment on lines +1162 to +1163
if (statResult === undefined) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Account for keys that remain in the secondary store

When the primary copy expires before the secondary copy, has(key) still reports that the key exists. This check skips the key, although deleteMany removes the secondary copy, so all four storage statistics remain stale. Check the secondary store before the code classifies the key as absent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

I did not change this in this PR. These are the reasons:

  • deleteInternal (packages/cacheable/src/index.ts:1098) also reads only the primary store. This PR keeps delete and deleteMany the same.
  • The stats do not decrease when a primary entry expires or when the LRU removes it. So the stats do not follow the secondary store in those paths either.
  • A secondary read adds a remote call to each deleteMany when stats are on.
  • A shared secondary store can hold keys that a different instance set. This instance did not count those keys, but the check would decrease the stats for them.

A stats model for two layers is a different issue from #1738. I leave this thread open for the maintainer.


Generated by Claude Code

Before, deleteMany read values with get(). A cached undefined value and
a missing key both gave undefined, so deleteMany did not change the stats
for a cached undefined value. Now deleteMany reads the raw entries with
getManyRaw() and sizes the value of each entry.

Also split the code comment into two instructions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQyS4TFa7JFA5PEcKuLj4T
@jaredwray
jaredwray merged commit dd4562d into main Oct 18, 2026
15 checks passed
@jaredwray
jaredwray deleted the claude/great-archimedes-37p526 branch October 18, 2026 00:31
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.

Cacheable.deleteMany with stats: true is O(keys × objects^2) and blocks the event loop

2 participants