Review an Asynchronous Cache for Races, Memory Growth, and Thread Safety
MercorSoftware Engineer · Technical ScreenLast updated
Tests understanding of asynchronous race conditions, memory leaks, and eviction in single-threaded event loops versus multithreaded synchronization in languages like C++.
Scenario
You are reviewing a teammate's implementation of an in-memory asynchronous cache written in TypeScript/JavaScript. The cache is intended to sit in front of an expensive remote data store, coalesce concurrent in-flight requests for the same key to prevent cache stampedes, support TTL-based expiration, and allow manual invalidation.
Here is the current implementation:
interface CacheEntry {
value: T;
expiresAt: number;
}
class AsyncCache {
private store = new Map>();
private inFlight = new Map>();
constructor(private ttlMs: number) {}
async get(key: string, fetcher: (key: string) => Promise): Promise {
const entry = this.store.get(key);
const now = Date.now();
if (entry && entry.expiresAt > now) {
return entry.value;
}
if (this.inFlight.has(key)) {
return this.inFlight.get(key)!;
}
const promise = fetcher(key).then((value) => {
this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
this.inFlight.delete(key);
return value;
});
this.inFlight.set(key, promise);
return promise;
}
invalidate(key: string): void {
this.store.delete(key);
}
}
Tasks
- Defect & Race Condition Analysis: Identify at least three distinct bugs or race conditions in this code. Specifically analyze:
- What happens when a
fetchercall rejects? - What happens if
invalidate(key)is called while afetchercall forkeyis still in flight? - How memory growth behaves over time when many unique keys are queried once.
- What happens when a
- Fixed Implementation: Write a revised version of
AsyncCachethat addresses these defects. Ensure that failed requests do not poison the cache and that invalidation correctly prevents stale in-flight results from being committed to the store. - Event Loop vs. Thread Safety Contrast: Compare the concurrency model of this JavaScript cache with a multithreaded C++ implementation:
- Why does JavaScript not require locks around
this.store.set()? - What concurrency primitives (
std::shared_mutex, double-checked locking, condition variables) would be required to port this cache safely to C++20 without blocking cache reads during long network fetches?
- Why does JavaScript not require locks around
Hints
Hint 1
Trace the promise settlement path: if `fetcher(key)` throws an unhandled rejection, check whether `this.inFlight` ever removes the key or propagates the error cleanly.
Hint 2
Consider the order of operations when `invalidate(key)` runs between the invocation of `fetcher(key)` and its `.then(...)` callback resolution.
Hint 3
Notice that expired entries are only removed if accessed again, and even then they are replaced rather than deleted, causing unbounded memory growth if unique keys are queried.
Follow-up questions
- How would you integrate request cancellation (e.g., using `AbortController`) so that if all callers waiting on an in-flight promise abort, the underlying fetch is aborted immediately?
- How could you use JavaScript `WeakRef` and `FinalizationRegistry` to allow the V8 garbage collector to reclaim cache values under memory pressure while keeping hot values fast?
- In C++, how would you structure the in-flight deduplication map using `std::shared_mutex` to ensure that threads waiting on the same key do not hold lock contention on threads reading unrelated cached keys?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.