Skip to content

feat(cache): stale-on-error via LRU touch on bgFlights failure#1132

Open
igoramf wants to merge 1 commit intomainfrom
feat/stale-on-error-lru-touch
Open

feat(cache): stale-on-error via LRU touch on bgFlights failure#1132
igoramf wants to merge 1 commit intomainfrom
feat/stale-on-error-lru-touch

Conversation

@igoramf
Copy link
Copy Markdown
Contributor

@igoramf igoramf commented Mar 19, 2026

Summary

  • Adds touch() method to LRU cache that extends TTL without modifying the underlying data or expires header
  • Stores item size as LRU value (was true) so touch can re-set the correct size when extending TTL
  • Adds ERROR_STALE_TTL env var (default 1h) to control the extension window
  • Calls touch in loader.ts bgFlights .catch after logging the error

Motivation

When bgFlights fails due to origin outage (e.g. VTEX down), the stale cache entry expires after STALE_TTL_PERIOD (30s) and the next request becomes a hard MISS — returning an error to the user.

With this change, on failure the LRU TTL is extended by ERROR_STALE_TTL (1h), so stale content keeps being served while the origin recovers. Once the origin comes back, the next successful bgFlights calls cache.put which resets the TTL back to normal (90s).

This preserves the normal 30s STALE_TTL_PERIOD for healthy traffic — avoiding the memory pressure of a globally extended stale window, which would cause LRU slot exhaustion in high-cache-hit scenarios.

Test plan

  • Simulate origin failure and verify stale content is served beyond the 30s window
  • Verify normal HIT/STALE/MISS flow is unchanged when origin is healthy
  • Check ERROR_STALE_TTL env var overrides default 1h

Summary by cubic

Implements stale-on-error: when bgFlights revalidation fails, the LRU entry’s TTL is extended (default 1h) so stale content keeps serving instead of returning errors. Healthy traffic is unchanged; TTL resets on the next successful revalidation.

  • New Features
    • Added LRU touch() to extend TTL without changing data or the expires header.
    • Now store item size as the LRU value (was true) so touch preserves correct size; updated dispose signature accordingly.
    • Introduced ERROR_STALE_TTL env var (default 1h) to control the extension window.
    • Call cache.touch in bgFlights failure path in loader.ts, with error logging.

Written for commit eb0371e. Summary will update on new commits.

Summary by CodeRabbit

  • Performance Improvements

    • Cache entries now remain available longer when background data refreshes encounter errors, improving service resilience during temporary outages.
  • Bug Fixes

    • Enhanced error handling for failed background data refresh operations with automatic cache lifetime extension and improved error logging.

@github-actions
Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.180.1 update
  • 🎉 for Minor 1.181.0 update
  • 🚀 for Major 2.0.0 update

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

When background revalidation fails, the system now extends the cached entry's lifetime using an ERROR_STALE_TTL configuration instead of immediately invalidating it. A new touch method on the LRU cache resets the entry's TTL without modifying the underlying response data.

Changes

Cohort / File(s) Summary
Error-aware background revalidation
blocks/loader.ts
Added error handling for failed background revalidation that calls cache.touch?.(request) to extend LRU entry lifetime via the new ERROR_STALE_TTL configuration, with nested error logging.
Stale cache extension mechanism
runtime/caches/lrucache.ts
Introduced ERROR_STALE_TTL environment-configurable constant (default 3600000 ms) and new touch method to extend cache entry lifetime. Updated dispose callback parameter type from boolean to number and adjusted cached value storage to track Content-Length.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • hugo-ccabral

Poem

🐰 When requests fail their noble quest,
We touch the cache to let it rest,
With ERROR_STALE to save the day—
Stale data keeps the users' way! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: introducing a stale-on-error caching mechanism via LRU touch method triggered on bgFlights failure.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stale-on-error-lru-touch
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="blocks/loader.ts">

<violation number="1" location="blocks/loader.ts:345">
P1: The optional `touch` call is followed by a non-optional `.catch`, which can throw when `touch` is not implemented.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +345 to +346
.touch?.(request)
.catch((e) => logger.error(`loader touch error ${e}`));
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 19, 2026

Choose a reason for hiding this comment

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

P1: The optional touch call is followed by a non-optional .catch, which can throw when touch is not implemented.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At blocks/loader.ts, line 345:

<comment>The optional `touch` call is followed by a non-optional `.catch`, which can throw when `touch` is not implemented.</comment>

<file context>
@@ -337,7 +337,14 @@ const wrapLoader = (
+                // Origin failed — extend LRU TTL so stale content keeps being served
+                // while origin recovers, instead of evicting and returning a hard error.
+                (cache as Cache & { touch?: (r: RequestInfo | URL) => Promise<void> })
+                  .touch?.(request)
+                  .catch((e) => logger.error(`loader touch error ${e}`));
+              });
</file context>
Suggested change
.touch?.(request)
.catch((e) => logger.error(`loader touch error ${e}`));
.touch?.(request)?.catch((e) => logger.error(`loader touch error ${e}`));
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@blocks/loader.ts`:
- Around line 340-347: The file has formatting differences causing CI to fail;
run the formatter (deno fmt) on the file containing the loader error handling
block (the code using logger.error(...) and the cache.touch?.(request) call) to
normalize spacing and line breaks so the catch block matches project style;
after formatting, re-run the formatter check to ensure deno fmt --check passes.
- Around line 340-347: The current code calls (cache as Cache & { touch?: (r:
RequestInfo | URL) => Promise<void> }).touch?.(request).catch(...), which throws
when touch is undefined because .catch is invoked on undefined; change the call
so the optional chaining also applies to .catch — i.e. call touch via (cache
...).touch?.(request)?.catch((e) => logger.error(`loader touch error ${e}`)) —
or alternatively guard with an if (typeof touch === 'function') before invoking
— referencing the cache variable, its touch? method, request and logger to
locate and update the loader error handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87577151-0254-4bd7-a7a5-dba432343c67

📥 Commits

Reviewing files that changed from the base of the PR and between ac02ba0 and eb0371e.

📒 Files selected for processing (2)
  • blocks/loader.ts
  • runtime/caches/lrucache.ts

Comment on lines +340 to +347
.catch((error) => {
logger.error(`loader error ${error}`);
// Origin failed — extend LRU TTL so stale content keeps being served
// while origin recovers, instead of evicting and returning a hard error.
(cache as Cache & { touch?: (r: RequestInfo | URL) => Promise<void> })
.touch?.(request)
.catch((e) => logger.error(`loader touch error ${e}`));
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix formatting to pass CI.

The pipeline reports deno fmt --check found formatting differences. Run deno fmt on this file to resolve the CI failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocks/loader.ts` around lines 340 - 347, The file has formatting differences
causing CI to fail; run the formatter (deno fmt) on the file containing the
loader error handling block (the code using logger.error(...) and the
cache.touch?.(request) call) to normalize spacing and line breaks so the catch
block matches project style; after formatting, re-run the formatter check to
ensure deno fmt --check passes.

⚠️ Potential issue | 🔴 Critical

TypeError when touch is undefined: optional chaining doesn't extend to .catch().

If the cache implementation doesn't have a touch method (e.g., non-LRU caches), touch?.(request) returns undefined, and then .catch(...) is invoked on undefined, throwing a TypeError.

🐛 Proposed fix: extend optional chaining to the catch call
                 (cache as Cache & { touch?: (r: RequestInfo | URL) => Promise<void> })
                   .touch?.(request)
-                  .catch((e) => logger.error(`loader touch error ${e}`));
+                  ?.catch((e) => logger.error(`loader touch error ${e}`));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blocks/loader.ts` around lines 340 - 347, The current code calls (cache as
Cache & { touch?: (r: RequestInfo | URL) => Promise<void>
}).touch?.(request).catch(...), which throws when touch is undefined because
.catch is invoked on undefined; change the call so the optional chaining also
applies to .catch — i.e. call touch via (cache ...).touch?.(request)?.catch((e)
=> logger.error(`loader touch error ${e}`)) — or alternatively guard with an if
(typeof touch === 'function') before invoking — referencing the cache variable,
its touch? method, request and logger to locate and update the loader error
handling.

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.

2 participants