Katkat

Review an Asynchronous Cache for Races, Memory Growth, and Thread Safety

MercorSoftware Engineer · Technical Screen

Last 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

  1. Defect & Race Condition Analysis: Identify at least three distinct bugs or race conditions in this code. Specifically analyze:
    • What happens when a fetcher call rejects?
    • What happens if invalidate(key) is called while a fetcher call for key is still in flight?
    • How memory growth behaves over time when many unique keys are queried once.
  2. Fixed Implementation: Write a revised version of AsyncCache that 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.
  3. 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?

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?