package support; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; /** * MOAD-0005: CWE-362 — A Thundering Herd. * * Defect: a cache lookup uses the pattern get() + null-check + compute + put() * with no lock or synchronization around the compute path. When N threads all * miss the cache simultaneously, each one sees null, each one runs the expensive * computation, and all N results race to be stored. N-1 computes are wasted. * Under high concurrency, this saturates the compute resource (CPU, DB, network) * with redundant work. * * Fix: ConcurrentHashMap.computeIfAbsent() guarantees exactly one compute per * key under concurrent access. All other callers block until the first compute * completes, then receive the cached value. * * Scanner detects: HashMap.get() + null check + compute + HashMap.put() on the * same key, without a surrounding synchronized block or lock acquisition. * * Note: unit tests simulate the concurrent race with a single-threaded model * (all callers check before any put). Integration tests use real threads. */ public class Moad0005Algorithm { /** Counts how many times the "expensive" compute function was called. */ public static final AtomicInteger COMPUTE_CALLS = new AtomicInteger(0); /** * Simulates an expensive computation (DB query, RPC, etc.). * Increments COMPUTE_CALLS so tests can verify how many times it ran. */ public static String expensiveCompute(String key) { COMPUTE_CALLS.incrementAndGet(); return "value-for-" + key; } // ── Defective: get + null-check + compute + put ─────────────────────────── /** * Defective cache backed by unsynchronized HashMap. * * The defect: get() and put() are not atomic. If N callers all invoke * getOrCompute() before any of them completes, they all see null and all * call expensiveCompute(). */ public static final class DefectiveCache { private final HashMap cache = new HashMap<>(); public String getOrCompute(String key) { String value = cache.get(key); // 1. lookup — may be null for N callers simultaneously if (value == null) { // 2. all N see null value = expensiveCompute(key); // 3. all N compute — HERD cache.put(key, value); // 4. last writer wins, N-1 computes wasted } return value; } } /** * Simulates N callers all hitting a cold DefectiveCache simultaneously. * * All callers read before any put — the race window. Returns the number * of times expensiveCompute was called (should be 1, but returns N for defective). */ public static int simulateDefectiveConcurrentMiss(int callerCount, String key) { COMPUTE_CALLS.set(0); DefectiveCache cache = new DefectiveCache(); // Phase 1: all callers read — none sees a cached value yet. String[] seen = new String[callerCount]; for (int i = 0; i < callerCount; i++) { seen[i] = cache.cache.get(key); // all see null } // Phase 2: all callers that saw null compute and put. for (int i = 0; i < callerCount; i++) { if (seen[i] == null) { String v = expensiveCompute(key); cache.cache.put(key, v); } } return COMPUTE_CALLS.get(); // DEFECT: returns callerCount } // ── Fixed: computeIfAbsent ──────────────────────────────────────────────── /** * Fixed cache backed by ConcurrentHashMap.computeIfAbsent(). * * The fix: computeIfAbsent() is atomic per key. The first caller computes; * all subsequent callers block until the value is ready, then receive it. * expensiveCompute() runs exactly once per key regardless of concurrency. */ public static final class FixedCache { private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); public String getOrCompute(String key) { return cache.computeIfAbsent(key, k -> expensiveCompute(k)); } } /** * Simulates N callers all hitting a cold FixedCache. * Returns the number of times expensiveCompute was called (always 1). */ public static int simulateFixedConcurrentMiss(int callerCount, String key) { COMPUTE_CALLS.set(0); FixedCache cache = new FixedCache(); for (int i = 0; i < callerCount; i++) { cache.getOrCompute(key); } return COMPUTE_CALLS.get(); // FIX: always 1 } /** Reset compute counter between test cases. */ public static void resetCounter() { COMPUTE_CALLS.set(0); } }