Skip to content

Commit e3369ea

Browse files
authored
docs(cache): document cache coordination flow (JhaSourav07#3186)
## Description Fixes JhaSourav07#2246 ## Summary * Added JSDoc for `DistributedCache.getOrSet()` * Documented local Promise deduplication flow * Added explanations for Redis lock coordination and fallback behavior * Improved readability without changing functionality ## Pillar * [ ] 🎨 Pillar 1 — New Theme Design * [ ] 📐 Pillar 2 — Geometric SVG Improvement * [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [x] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview N/A (documentation-only change) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. * [ ] I have tested these changes locally. * [ ] I have run formatting/lint checks locally. - [x] My commits follow the Conventional Commits format. * [ ] I updated README if required. - [ ] I have only one commit in this PR.
2 parents cd1e1ea + 404be1a commit e3369ea

1 file changed

Lines changed: 25 additions & 15 deletions

File tree

lib/cache.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -437,29 +437,35 @@ return c`;
437437
}
438438

439439
/**
440-
* Gets a value from the cache, or executes the load function if missing or stale.
441-
* Employs both an in-memory Promise lock (L1) and a Redis Mutex (L2) to prevent Cache Stampedes.
440+
* Returns cached data when available, otherwise loads and stores fresh data.
441+
*
442+
* Uses a two-layer coordination strategy to reduce cache stampedes:
443+
* 1. Local Promise deduplication (L1) prevents duplicate fetches within the same instance.
444+
* 2. Redis mutex locking (L2) prevents duplicate fetches across distributed instances.
445+
*
446+
* `loadFn` receives the current cached value (or null) so callers can implement
447+
* stale refresh logic when needed.
442448
*
443449
* @param key - Cache key.
444-
* @param loadFn - Async function to fetch the data. Receives the stale cached value if one exists.
445-
* @param ttlMs - Time to live in milliseconds.
446-
* @param shouldFetch - Optional predicate to force fetching even if a cache value exists (e.g. for stale delta sync).
450+
* @param loadFn - Async function used to load fresh data.
451+
* @param ttlMs - Cache expiration time in milliseconds.
452+
* @param shouldFetch - Optional predicate that forces refresh even on cache hits.
447453
*/
448454
async getOrSet(
449455
key: string,
450456
loadFn: (cached: T | null) => Promise<T>,
451457
ttlMs: number,
452458
shouldFetch?: (cached: T) => boolean
453459
): Promise<T> {
454-
// 1. L1 & L2 Cache Check
460+
// Attempt to retrieve an existing value before triggering a refresh.
455461
const cached = await this.get(key);
456462

457-
// If we have a cache hit and we don't need to force a refresh, return it early.
458463
if (cached !== null && (!shouldFetch || !shouldFetch(cached))) {
459464
return cached;
460465
}
461466

462-
// 2. L1 Promise Deduping (Local Lock)
467+
// Join an existing in-flight request instead of creating duplicate fetches
468+
// within the same runtime instance.
463469
const pendingLocal = this.localLocks.get(key);
464470
if (pendingLocal) return pendingLocal;
465471

@@ -478,7 +484,8 @@ return c`;
478484

479485
while (Date.now() - start < maxPollTime) {
480486
try {
481-
// Attempt to acquire Redis Mutex
487+
// NX: acquire only if lock doesn't already exist.
488+
// PX 10000: auto-expire lock after 10 seconds to avoid deadlocks.
482489
const lockRes = await fetch(`${this.redisUrl}/`, {
483490
method: 'POST',
484491
headers: {
@@ -491,12 +498,12 @@ return c`;
491498
if (lockRes.ok) {
492499
const lockData = await lockRes.json();
493500
if (lockData.result === 'OK') {
494-
// Lock acquired! Execute loadFn.
495501
try {
496502
const freshData = await loadFn(cached);
497503
await this.set(key, freshData, ttlMs);
498504

499-
// Release lock early
505+
// Release immediately after caching data instead of waiting
506+
// for lock expiration so other instances can continue sooner.
500507
await fetch(`${this.redisUrl}/`, {
501508
method: 'POST',
502509
headers: {
@@ -508,7 +515,8 @@ return c`;
508515

509516
return freshData;
510517
} catch (err) {
511-
// Release lock on error so others can retry
518+
// Remove lock even on failure so other instances don't wait
519+
// for the full lock timeout period.
512520
await fetch(`${this.redisUrl}/`, {
513521
method: 'POST',
514522
headers: {
@@ -529,10 +537,11 @@ return c`;
529537
return fallbackData;
530538
}
531539

532-
// Lock not acquired. Wait and poll L2 cache.
540+
// Wait briefly before checking whether another instance populated the cache.
533541
await new Promise((resolve) => setTimeout(resolve, pollInterval));
534542
const doubleCheck = await this.get(key);
535-
// If doubleCheck satisfies the condition, return it
543+
544+
// Another instance may have already populated the cache while waiting.
536545
if (doubleCheck !== null && (!shouldFetch || !shouldFetch(doubleCheck))) {
537546
return doubleCheck;
538547
}
@@ -544,10 +553,11 @@ return c`;
544553
return finalFallback;
545554
};
546555

547-
// Execute with local Promise lock
556+
// Ensure local lock cleanup even if request execution fails.
548557
const promise = executeAndLock().finally(() => {
549558
this.localLocks.delete(key);
550559
});
560+
551561
this.localLocks.set(key, promise);
552562

553563
return promise;

0 commit comments

Comments
 (0)