java-topology/defects/drone-0002/test/Drone0002Test.java

200 lines
7.2 KiB
Java

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/**
* Test for drone-0002: MOAD-0005 Thundering Herd in cache/ttl_cache.go TTLCache.Get().
*
* Location: cache/ttl_cache.go TTLCache.Get()
*
* Defect pattern (CWE-362 cache stampede):
* 1. fetch(key) — acquires RLock, returns cache miss
* 2. getter.Find(ctx, key) — called with NO lock held
* 3. mx.Lock(); cache[key] = item; mx.Unlock()
*
* Under concurrent load: C goroutines all see a cache miss simultaneously for
* the same key, all call getter.Find() independently, all issue C database
* round-trips for the same record. A thundering herd of DB queries results.
*
* Fix: wrap getter.Find() in a singleflight group keyed by the cache key.
* Only the first goroutine issues the DB call; all others share its result.
* Backend calls drop from C to 1 for any C concurrent misses on the same key.
*
* Compile and run (no build tool required):
* javac defects/drone-0002/test/Drone0002Test.java -d /tmp/drone-0002
* java -cp /tmp/drone-0002 Drone0002Test
*/
public class Drone0002Test {
private static int passed = 0;
private static int failed = 0;
// --- Fake backend store with call counter ---
static class FakeStore {
final AtomicInteger callCount = new AtomicInteger(0);
final long latencyMs;
FakeStore(long latencyMs) { this.latencyMs = latencyMs; }
String find(String key) throws InterruptedException {
callCount.incrementAndGet();
Thread.sleep(latencyMs);
return "value-for-" + key;
}
}
// --- Defective cache: no singleflight ---
static class CacheDefective {
final Map<String, String> cache = new ConcurrentHashMap<>();
final FakeStore store;
CacheDefective(FakeStore s) { this.store = s; }
String get(String key) throws Exception {
String v = cache.get(key);
if (v != null) return v;
// thundering herd: multiple threads all reach here simultaneously
v = store.find(key);
cache.put(key, v);
return v;
}
}
// --- Fixed cache: singleflight coalesces concurrent misses ---
static class CacheFixed {
final Map<String, String> cache = new ConcurrentHashMap<>();
final FakeStore store;
final ConcurrentHashMap<String, CompletableFuture<String>> inflight = new ConcurrentHashMap<>();
CacheFixed(FakeStore s) { this.store = s; }
String get(String key) throws Exception {
String v = cache.get(key);
if (v != null) return v;
CompletableFuture<String> mine = new CompletableFuture<>();
CompletableFuture<String> existing = inflight.putIfAbsent(key, mine);
if (existing != null) return existing.get(); // wait for in-flight call
try {
v = store.find(key);
cache.put(key, v);
mine.complete(v);
return v;
} catch (Exception e) {
mine.completeExceptionally(e);
throw e;
} finally {
inflight.remove(key);
}
}
}
// --- Helpers ---
static int countBackendCalls(boolean useFixed, int concurrent, long latencyMs)
throws Exception {
FakeStore store = new FakeStore(latencyMs);
Object cache = useFixed ? new CacheFixed(store) : new CacheDefective(store);
CountDownLatch ready = new CountDownLatch(concurrent);
CountDownLatch start = new CountDownLatch(1);
CountDownLatch done = new CountDownLatch(concurrent);
ExecutorService pool = Executors.newFixedThreadPool(concurrent);
for (int i = 0; i < concurrent; i++) {
pool.submit(() -> {
ready.countDown();
try {
start.await();
if (useFixed) ((CacheFixed) cache).get("mykey");
else ((CacheDefective) cache).get("mykey");
} catch (Exception ignored) {
} finally { done.countDown(); }
});
}
ready.await(5, TimeUnit.SECONDS); // all threads waiting at start gate
start.countDown(); // release simultaneously
done.await(10, TimeUnit.SECONDS);
pool.shutdown();
return store.callCount.get();
}
// --- Tests ---
static void testDefectiveTriggersManyBackendCalls() throws Exception {
int concurrent = 20;
int calls = countBackendCalls(false, concurrent, 30);
check("defective cache triggers >1 backend calls (thundering herd confirmed)",
calls > 1);
System.out.printf(" drone-0002 defective backend calls at C=%d: %d (expect ~%d)%n",
concurrent, calls, concurrent);
}
static void testFixedTriggersExactlyOneBackendCall() throws Exception {
int concurrent = 20;
int calls = countBackendCalls(true, concurrent, 30);
check("fixed cache triggers exactly 1 backend call (singleflight working)",
calls == 1);
System.out.printf(" drone-0002 fixed backend calls at C=%d: %d (expect 1)%n",
concurrent, calls);
}
static void testFixedReturnCorrectValue() throws Exception {
FakeStore store = new FakeStore(5);
CacheFixed cache = new CacheFixed(store);
String result = cache.get("mykey");
check("fixed cache returns correct value", "value-for-mykey".equals(result));
}
static void testFixedSecondCallHitsCache() throws Exception {
FakeStore store = new FakeStore(0);
CacheFixed cache = new CacheFixed(store);
cache.get("k1");
int afterFirst = store.callCount.get();
cache.get("k1"); // second call — must hit cache
int afterSecond = store.callCount.get();
check("second Get on same key hits cache (no backend call)",
afterFirst == afterSecond);
}
static void testSpeedupRatioAtConcurrency20() throws Exception {
int concurrent = 20;
int defCalls = countBackendCalls(false, concurrent, 15);
int fixCalls = countBackendCalls(true, concurrent, 15);
double ratio = (double) defCalls / Math.max(1, fixCalls);
check("fixed version reduces backend calls by at least 10x at C=20",
ratio >= 10.0);
System.out.printf(" drone-0002 thundering herd ratio at C=%d: %.1fx fewer backend calls%n",
concurrent, ratio);
}
// --- Harness ---
static void check(String desc, boolean cond) {
if (cond) {
System.out.println(" PASS: " + desc);
passed++;
} else {
System.out.println(" FAIL: " + desc);
failed++;
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== Drone0002Test (MOAD-0005 Thundering Herd in TTLCache.Get) ===\n");
testDefectiveTriggersManyBackendCalls();
testFixedTriggersExactlyOneBackendCall();
testFixedReturnCorrectValue();
testFixedSecondCallHitsCache();
testSpeedupRatioAtConcurrency20();
System.out.println("\n--- " + passed + " passed, " + failed + " failed ---");
if (failed > 0) System.exit(1);
}
}