Skip to content

fix(cache): preserve on_release behavior in TTLCleanupCache.clear()#14180

Merged
lukasmasuch merged 1 commit intodevelopfrom
03-02-fix_cache_preserve_on_release_behavior_in_ttlcleanupcache.clear_
Mar 3, 2026
Merged

fix(cache): preserve on_release behavior in TTLCleanupCache.clear()#14180
lukasmasuch merged 1 commit intodevelopfrom
03-02-fix_cache_preserve_on_release_behavior_in_ttlcleanupcache.clear_

Conversation

@sfc-gh-bnisco
Copy link
Copy Markdown
Collaborator

@sfc-gh-bnisco sfc-gh-bnisco commented Mar 2, 2026

Describe your changes

cachetools 7.0.2 changed TTLCache.clear() to an O(1) implementation that bypasses popitem()/expire(). That skipped TTLCleanupCache release hooks and broke resource cleanup paths that expect on_release to run for each entry.

Override TTLCleanupCache.clear() to evict entries via popitem(), preserving release semantics across cachetools versions. Update related inline comments in cache_resource_api to document the callback/exception behavior and why per-item cleanup is still used.

Screenshot or video (only for visual changes)

GitHub Issue Link (if applicable)

Testing Plan

  • Covered by existing tests

Contribution License Agreement

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.

@sfc-gh-bnisco sfc-gh-bnisco added the change:bugfix PR contains bug fix implementation label Mar 2, 2026 — with Graphite App
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

✅ PR preview is ready!

Name Link
📦 Wheel file https://core-previews.s3-us-west-2.amazonaws.com/pr-14180/streamlit-1.54.0-py3-none-any.whl
📦 @streamlit/component-v2-lib Download from artifacts
🕹️ Preview app pr-14180.streamlit.app (☁️ Deploy here if not accessible)

@sfc-gh-bnisco sfc-gh-bnisco added the impact:internal PR changes only affect internal code label Mar 2, 2026 — with Graphite App
Copy link
Copy Markdown
Collaborator Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@snyk-io
Copy link
Copy Markdown
Contributor

snyk-io bot commented Mar 2, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@sfc-gh-bnisco sfc-gh-bnisco added the ai-review If applied to PR or issue will run AI review workflow label Mar 2, 2026
@sfc-gh-bnisco sfc-gh-bnisco requested a review from Copilot March 2, 2026 21:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR restores resource cleanup semantics for Streamlit’s TTL-based cache by ensuring TTLCleanupCache.clear() triggers on_release for each entry even after cachetools changed TTLCache.clear() internals.

Changes:

  • Override TTLCleanupCache.clear() to evict entries via popitem() so per-entry release hooks run.
  • Update cache_resource_api inline comments to clarify exception and cleanup behavior during cache clearing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
lib/streamlit/runtime/caching/ttl_cleanup_cache.py Adds a clear() override to preserve per-entry on_release behavior across cachetools versions.
lib/streamlit/runtime/caching/cache_resource_api.py Refines comments explaining why cache entries are cleared individually and how callback exceptions are handled/logged.

@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

Summary

This PR fixes a regression introduced by cachetools 7.0.2, which changed TTLCache.clear() to an O(1) implementation that directly clears internal storage, bypassing popitem(). This broke TTLCleanupCache's on_release hook, which relies on popitem() to trigger cleanup callbacks when entries are removed.

The fix adds a clear() override to TTLCleanupCache that iterates via popitem() to preserve on_release semantics. Comment updates in cache_resource_api.py improve documentation clarity around the exception-handling behavior of the per-item clearing loop.

Code Quality

The implementation is clean, minimal, and well-documented:

  • ttl_cleanup_cache.py (lines 78-95): The new clear() override uses @override from typing_extensions, includes a thorough docstring explaining the rationale (cachetools 7.0.2 behavior change), and uses the idiomatic while True / except KeyError pattern that matches how popitem() signals an empty cache.
  • cache_resource_api.py (lines 707-715): The updated comments are clearer than the originals, accurately describing the exception-stopping behavior of TTLCleanupCache.clear() and why per-item cleanup is still used at the ResourceCache level.

One subtle edge case worth noting: if a user-provided on_release callback raises KeyError, the except KeyError in TTLCleanupCache.clear() would catch it and stop clearing prematurely. In practice this is very low risk because (1) the primary clearing path through ResourceCache._clear() catches all exceptions individually, and (2) user callbacks raising KeyError is an unusual pattern. This is not a blocker.

Test Coverage

Test coverage is adequate:

  • ttl_cleanup_cache_test.py::test_clear_calls_on_release (line 95): This existing test directly covers the exact bug by adding 5 items, calling clear(), and asserting all items were released via on_release. Without the new clear() override, this test would fail on cachetools >= 7.0.2.
  • cache_resource_api_test.py::test_on_release_fires (line 235): Tests the higher-level st.cache_resource.clear() path including on_release invocations.
  • cache_resource_api_test.py::test_on_release_fires_when_cleared_with_exceptions (line 256): Tests that all on_release callbacks fire even when some raise exceptions, via the per-item loop in ResourceCache._clear().

The PR description says "Covered by existing tests," which is accurate. The existing tests already validate the expected behavior; the fix ensures they continue to pass with cachetools 7.0.2+.

Backwards Compatibility

No breaking changes. The fix is purely defensive:

  • The clear() override preserves the exact same semantics that existed before cachetools 7.0.2 (items are evicted one-by-one via popitem(), triggering on_release for each).
  • The cachetools version constraint (>=5.5,<8) already allows 7.0.2, so this fix is necessary for compatibility across the supported range.
  • No public API changes.

Security & Risk

No security concerns. The change is limited to internal cache cleanup logic with no impact on authentication, networking, or data handling. The regression risk is very low given the existing test coverage.

External test recommendation

  • Recommend external_test: No
  • Triggered categories: None
  • Evidence:
    • lib/streamlit/runtime/caching/ttl_cleanup_cache.py: Internal cache cleanup logic only, no routing, auth, websocket, embedding, asset serving, CORS, storage, or security header changes.
    • lib/streamlit/runtime/caching/cache_resource_api.py: Comment-only changes, no behavioral modifications.
  • Suggested external_test focus areas: N/A
  • Confidence: High
  • Assumptions and gaps: None. The changes are entirely internal to the caching subsystem with no external-facing behavior changes.

Accessibility

No frontend changes; accessibility is not applicable.

Recommendations

No blocking issues. One optional suggestion:

  1. Optional — guard against KeyError from on_release: The except KeyError in TTLCleanupCache.clear() could theoretically catch a KeyError raised by the _on_release callback rather than by popitem() on an empty cache. A more defensive approach would be to check len(self) > 0 instead of relying on KeyError:
    def clear(self) -> None:
        while len(self) > 0:
            self.popitem()
    This is a very minor edge case and not a blocker, especially since the primary clearing path in ResourceCache._clear() already handles exceptions individually.

Verdict

APPROVED: Clean, minimal fix that correctly preserves on_release semantics against a cachetools 7.0.2 regression, with adequate existing test coverage and no backwards compatibility concerns.


This is an automated AI review by opus-4.6-thinking.

@sfc-gh-bnisco sfc-gh-bnisco force-pushed the 03-02-fix_cache_preserve_on_release_behavior_in_ttlcleanupcache.clear_ branch from ea9d58f to 9c54abb Compare March 2, 2026 22:11
@sfc-gh-bnisco sfc-gh-bnisco requested a review from Copilot March 2, 2026 22:11
@sfc-gh-bnisco sfc-gh-bnisco added the ai-review If applied to PR or issue will run AI review workflow label Mar 2, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@github-actions github-actions bot removed the ai-review If applied to PR or issue will run AI review workflow label Mar 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

⚠️ AI Review Failed

The AI review job failed to complete. Please check the workflow run for details.

You can retry by adding the 'ai-review' label again or manually triggering the workflow.

1 similar comment
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 2, 2026

⚠️ AI Review Failed

The AI review job failed to complete. Please check the workflow run for details.

You can retry by adding the 'ai-review' label again or manually triggering the workflow.

@sfc-gh-bnisco sfc-gh-bnisco marked this pull request as ready for review March 2, 2026 22:25

# Invoke on_release outside the try/except so callback exceptions,
# including KeyError, propagate to the caller.
self._on_release(value)
Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch Mar 2, 2026

Choose a reason for hiding this comment

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

question: An exception in _on_release would break the clear loop. Is that a problem or different to the current behaviour?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for this comment. Technically what was written before was 99% the same, but changed up a rough edge in the existing implementation. In order to keep this narrowly scoped, I ended up switching this to a simpler implementation. This one exactly matches the behavior seen in <= 7.0.1 in an effort to reduce any potential differences across versions, rather than attempting to fix some long-standing behavior.

Copy link
Copy Markdown
Collaborator

@lukasmasuch lukasmasuch left a comment

Choose a reason for hiding this comment

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

LGTM 👍 One potential issue to double check

cachetools 7.0.2 changed TTLCache.clear() to an O(1) implementation that
bypasses popitem()/expire(). That skipped TTLCleanupCache release hooks and
broke resource cleanup paths that expect on_release to run for each entry.

Override TTLCleanupCache.clear() to evict entries via popitem(), preserving
release semantics across cachetools versions.
@sfc-gh-bnisco sfc-gh-bnisco force-pushed the 03-02-fix_cache_preserve_on_release_behavior_in_ttlcleanupcache.clear_ branch from 9c54abb to 632a037 Compare March 3, 2026 03:25
@lukasmasuch
Copy link
Copy Markdown
Collaborator

I'm going to merge this to get the CI unblocked

@lukasmasuch lukasmasuch merged commit f915c38 into develop Mar 3, 2026
43 checks passed
@lukasmasuch lukasmasuch deleted the 03-02-fix_cache_preserve_on_release_behavior_in_ttlcleanupcache.clear_ branch March 3, 2026 09:20
github-actions bot pushed a commit that referenced this pull request Mar 3, 2026
…14180)

## Describe your changes

cachetools 7.0.2 changed TTLCache.clear() to an O(1) implementation that
bypasses `popitem()`/`expire()`. That skipped `TTLCleanupCache` release
hooks and broke resource cleanup paths that expect on_release to run for
each entry.

Override `TTLCleanupCache.clear()` to evict entries via `popitem()`,
preserving release semantics across `cachetools` versions. Update
related inline comments in `cache_resource_api` to document the
callback/exception behavior and why per-item cleanup is still used.

## Screenshot or video (only for visual changes)

## GitHub Issue Link (if applicable)

## Testing Plan

- [x] Covered by existing tests

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:bugfix PR contains bug fix implementation impact:internal PR changes only affect internal code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants