Skip to content

Commit 2fb5e36

Browse files
docs(cache): document cache coordination flow
1 parent 5e76adf commit 2fb5e36

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

lib/cache.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -376,29 +376,35 @@ export class DistributedCache<T> {
376376
}
377377

378378
/**
379-
* Gets a value from the cache, or executes the load function if missing or stale.
380-
* Employs both an in-memory Promise lock (L1) and a Redis Mutex (L2) to prevent Cache Stampedes.
379+
* Returns cached data when available, otherwise loads and stores fresh data.
380+
*
381+
* Uses a two-layer coordination strategy to reduce cache stampedes:
382+
* 1. Local Promise deduplication (L1) prevents duplicate fetches within the same instance.
383+
* 2. Redis mutex locking (L2) prevents duplicate fetches across distributed instances.
384+
*
385+
* `loadFn` receives the current cached value (or null) so callers can implement
386+
* stale refresh logic when needed.
381387
*
382388
* @param key - Cache key.
383-
* @param loadFn - Async function to fetch the data. Receives the stale cached value if one exists.
384-
* @param ttlMs - Time to live in milliseconds.
385-
* @param shouldFetch - Optional predicate to force fetching even if a cache value exists (e.g. for stale delta sync).
389+
* @param loadFn - Async function used to load fresh data.
390+
* @param ttlMs - Cache expiration time in milliseconds.
391+
* @param shouldFetch - Optional predicate that forces refresh even on cache hits.
386392
*/
387393
async getOrSet(
388394
key: string,
389395
loadFn: (cached: T | null) => Promise<T>,
390396
ttlMs: number,
391397
shouldFetch?: (cached: T) => boolean
392398
): Promise<T> {
393-
// 1. L1 & L2 Cache Check
399+
// Attempt to retrieve an existing value before triggering a refresh.
394400
const cached = await this.get(key);
395401

396-
// If we have a cache hit and we don't need to force a refresh, return it early.
397402
if (cached !== null && (!shouldFetch || !shouldFetch(cached))) {
398403
return cached;
399404
}
400405

401-
// 2. L1 Promise Deduping (Local Lock)
406+
// Join an existing in-flight request instead of creating duplicate fetches
407+
// within the same runtime instance.
402408
const pendingLocal = this.localLocks.get(key);
403409
if (pendingLocal) return pendingLocal;
404410

@@ -417,7 +423,8 @@ export class DistributedCache<T> {
417423

418424
while (Date.now() - start < maxPollTime) {
419425
try {
420-
// Attempt to acquire Redis Mutex
426+
// NX: acquire only if lock doesn't already exist.
427+
// PX 10000: auto-expire lock after 10 seconds to avoid deadlocks.
421428
const lockRes = await fetch(`${this.redisUrl}/`, {
422429
method: 'POST',
423430
headers: {
@@ -430,12 +437,12 @@ export class DistributedCache<T> {
430437
if (lockRes.ok) {
431438
const lockData = await lockRes.json();
432439
if (lockData.result === 'OK') {
433-
// Lock acquired! Execute loadFn.
434440
try {
435441
const freshData = await loadFn(cached);
436442
await this.set(key, freshData, ttlMs);
437443

438-
// Release lock early
444+
// Release immediately after caching data instead of waiting
445+
// for lock expiration so other instances can continue sooner.
439446
await fetch(`${this.redisUrl}/`, {
440447
method: 'POST',
441448
headers: {
@@ -447,7 +454,8 @@ export class DistributedCache<T> {
447454

448455
return freshData;
449456
} catch (err) {
450-
// Release lock on error so others can retry
457+
// Remove lock even on failure so other instances don't wait
458+
// for the full lock timeout period.
451459
await fetch(`${this.redisUrl}/`, {
452460
method: 'POST',
453461
headers: {
@@ -471,7 +479,8 @@ export class DistributedCache<T> {
471479
// Lock not acquired. Wait and poll L2 cache.
472480
await new Promise((resolve) => setTimeout(resolve, pollInterval));
473481
const doubleCheck = await this.get(key);
474-
// If doubleCheck satisfies the condition, return it
482+
483+
// Another instance may have already populated the cache while waiting.
475484
if (doubleCheck !== null && (!shouldFetch || !shouldFetch(doubleCheck))) {
476485
return doubleCheck;
477486
}
@@ -483,10 +492,11 @@ export class DistributedCache<T> {
483492
return finalFallback;
484493
};
485494

486-
// Execute with local Promise lock
495+
// Ensure local lock cleanup even if request execution fails.
487496
const promise = executeAndLock().finally(() => {
488497
this.localLocks.delete(key);
489498
});
499+
490500
this.localLocks.set(key, promise);
491501

492502
return promise;

0 commit comments

Comments
 (0)