java-topology/defects/varnish/unit/VarnishBanReloadAlgorithm.java

209 lines
8.3 KiB
Java

package unit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* varnish-0002: BAN_Reload O(B²) dedup scan.
*
* During Varnish restart / ban persistence reload, BAN_Reload() calls ban_reload()
* once per persisted ban. ban_reload() itself does TWO linear walks over the
* existing ban_head list calling ban_equal() (memcmp) on each entry:
*
* 1. VTAILQ_FOREACH_FROM to detect duplicates at the same timestamp bracket.
* 2. A second for-loop that walks ALL older bans to mark them completed.
*
* Combined: O(B²) comparisons. The code even has an explicit XXX comment:
* "This can be optimized by traversing the live ban list together with
* the reload list (combining the loops in BAN_Reload and ban_reload)."
*
* SLOW path: simulates the defect — linear ban_equal scan per loaded ban.
* FAST path: simulates the fix — hash pre-filter so only hash-matching bans
* need full comparison (typically 0 collisions = 0 full compares).
*
* Correctness: both paths must produce the same duplicate detection result.
* Performance: slow ops >> fast ops, ratio >= 5x at B=500.
*/
public class VarnishBanReloadAlgorithm {
// ---- simulated ban spec -------------------------------------------------
static class BanSpec {
final int id; // unique id standing in for full ban content
final long hash; // XXH64-equivalent: id*2654435761L
boolean completed;
BanSpec(int id) {
this.id = id;
this.hash = id * 2654435761L; // Knuth multiplicative hash
this.completed = false;
}
/** ban_equal equivalent: O(1) here, models memcmp on full spec bytes */
boolean equalSpec(BanSpec other) {
return this.id == other.id;
}
}
// ---- Result container ---------------------------------------------------
static class Result {
long ops; // comparison operations performed
int duplicatesFound;
Result(long ops, int dups) { this.ops = ops; this.duplicatesFound = dups; }
}
// -----------------------------------------------------------------------
// SLOW: ban_reload defect — O(B) linear scan per loaded ban
// Simulates the "hunt down older duplicates" for-loop in ban_reload()
// -----------------------------------------------------------------------
static Result slow(int B) {
List<BanSpec> banHead = new ArrayList<>(B);
long ops = 0;
int dups = 0;
// Simulate BAN_Reload: load B bans, each doing a full linear dedup scan
for (int i = 0; i < B; i++) {
BanSpec incoming = new BanSpec(i);
// "Hunt down older duplicates" — scan all existing bans for ban_equal
for (BanSpec existing : banHead) {
ops++; // models one ban_equal call
if (existing.equalSpec(incoming)) {
existing.completed = true; // mark_completed
dups++;
}
}
banHead.add(incoming);
}
return new Result(ops, dups);
}
// -----------------------------------------------------------------------
// FAST: hash pre-filter fix — O(1) hash check before ban_equal
// Simulates CWE-407 fix: spec_hash field added to struct ban
// Each incoming ban gets hash checked; full memcmp only on hash collision
// -----------------------------------------------------------------------
static Result fast(int B) {
// Map from spec_hash -> list of bans with that hash (collision chain)
Map<Long, List<BanSpec>> hashIndex = new HashMap<>(B * 2);
long ops = 0;
int dups = 0;
for (int i = 0; i < B; i++) {
BanSpec incoming = new BanSpec(i);
// O(1) hash lookup — finds only candidates with matching hash
List<BanSpec> candidates = hashIndex.get(incoming.hash);
if (candidates != null) {
for (BanSpec candidate : candidates) {
ops++; // models one ban_equal call (after hash match)
if (candidate.equalSpec(incoming)) {
candidate.completed = true;
dups++;
}
}
}
// CWE-407 fix: ops for the hash index lookup itself counted as 1
ops++; // hash lookup O(1)
// Insert into hash index
hashIndex.computeIfAbsent(incoming.hash, k -> new ArrayList<>()).add(incoming);
}
return new Result(ops, dups);
}
// -----------------------------------------------------------------------
public static void main(String[] args) {
int[] sizes = {100, 500, 1000};
int failures = 0;
int tests = 0;
for (int B : sizes) {
Result s = slow(B);
Result f = fast(B);
// Correctness: both paths should find same number of duplicates
// (With unique IDs: 0 duplicates in both cases)
if (s.duplicatesFound != f.duplicatesFound) {
System.out.printf("FAIL [B=%d] correctness: slow_dups=%d fast_dups=%d%n",
B, s.duplicatesFound, f.duplicatesFound);
failures++;
}
long ratio = s.ops / Math.max(f.ops, 1);
// At B=500: slow does ~B²/2 = 124,750 ops, fast does ~B = 500 ops
// Expect ratio >= 5x
boolean pass = s.ops >= f.ops * 5 && s.duplicatesFound == f.duplicatesFound;
tests++;
if (!pass) failures++;
System.out.printf("varnish-0002 B=%-6d slow=%-10d fast=%-8d ratio=%4dx %s%n",
B, s.ops, f.ops, ratio, pass ? "PASS" : "FAIL");
}
// Additional test: correctness with actual duplicates
// Simulate reloading same ban twice (duplicate scenario)
{
int B = 200;
// Create 200 bans, but first 10 are duplicates of last 10
List<BanSpec> banHead = new ArrayList<>();
Map<Long, List<BanSpec>> hashIndex = new HashMap<>();
long slowOps = 0, fastOps = 0;
int slowDups = 0, fastDups = 0;
// Inject 190 unique bans first
for (int i = 0; i < 190; i++) {
BanSpec b = new BanSpec(i);
// slow: just add
banHead.add(b);
// fast: index
hashIndex.computeIfAbsent(b.hash, k -> new ArrayList<>()).add(b);
}
// Now reload 10 bans that duplicate IDs 0..9
for (int i = 0; i < 10; i++) {
BanSpec incoming = new BanSpec(i); // duplicate of existing ban i
// SLOW: linear scan all 190+i bans
for (BanSpec existing : banHead) {
slowOps++;
if (existing.equalSpec(incoming)) {
existing.completed = true;
slowDups++;
}
}
banHead.add(incoming);
// FAST: hash lookup
List<BanSpec> candidates = hashIndex.get(incoming.hash);
if (candidates != null) {
for (BanSpec c : candidates) {
fastOps++;
if (c.equalSpec(incoming)) {
c.completed = true;
fastDups++;
}
}
}
fastOps++;
hashIndex.computeIfAbsent(incoming.hash, k -> new ArrayList<>()).add(incoming);
}
boolean correctness = (slowDups == fastDups && slowDups == 10);
boolean perf = slowOps > fastOps * 5;
boolean pass = correctness && perf;
tests++;
if (!pass) failures++;
System.out.printf("varnish-0002 dedup_correctness slow_dups=%d fast_dups=%d " +
"slow_ops=%d fast_ops=%d %s%n",
slowDups, fastDups, slowOps, fastOps, pass ? "PASS" : "FAIL");
}
System.out.printf("%n%d/%d PASS%n", tests - failures, tests);
if (failures > 0) System.exit(1);
}
}