177 lines
6.5 KiB
Java
177 lines
6.5 KiB
Java
package unit;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.HashSet;
|
||
import java.util.List;
|
||
import java.util.Set;
|
||
import java.util.Objects;
|
||
|
||
/**
|
||
* CWE-407 unit test: opensearch-005
|
||
* IndexGraveyard.containsIndex — List<Tombstone> linear scan called per-index
|
||
* inside DanglingIndicesState loop → O(I × T).
|
||
*
|
||
* Mirrors elasticsearch-004 — OpenSearch is a fork with the identical defect.
|
||
*
|
||
* Slow path: for (Tombstone t : tombstones) { if t.equals(index) ... } — O(T) per call.
|
||
* Fast path: HashSet<Index>.contains() — O(1) per call.
|
||
*
|
||
* Compile: javac -d . IndexGraveyardDanglingContains.java
|
||
* Run: java -ea unit.IndexGraveyardDanglingContains
|
||
*/
|
||
public class IndexGraveyardDanglingContains {
|
||
|
||
static final class Index {
|
||
final String name;
|
||
final String uuid;
|
||
|
||
Index(String name, String uuid) {
|
||
this.name = name;
|
||
this.uuid = uuid;
|
||
}
|
||
|
||
@Override
|
||
public boolean equals(Object o) {
|
||
if (!(o instanceof Index)) return false;
|
||
Index other = (Index) o;
|
||
return name.equals(other.name) && uuid.equals(other.uuid);
|
||
}
|
||
|
||
@Override
|
||
public int hashCode() {
|
||
return Objects.hash(name, uuid);
|
||
}
|
||
}
|
||
|
||
static final class Tombstone {
|
||
final Index index;
|
||
Tombstone(Index index) { this.index = index; }
|
||
Index getIndex() { return index; }
|
||
}
|
||
|
||
// ---- Slow: O(T) per call ----
|
||
|
||
static boolean slowContainsIndex(List<Tombstone> tombstones, Index index) {
|
||
for (Tombstone tombstone : tombstones) {
|
||
if (tombstone.getIndex().equals(index)) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static List<Index> slowFindDangling(List<Index> diskIndices, List<Tombstone> tombstones) {
|
||
List<Index> dangling = new ArrayList<>();
|
||
for (Index index : diskIndices) {
|
||
if (!slowContainsIndex(tombstones, index)) dangling.add(index);
|
||
}
|
||
return dangling;
|
||
}
|
||
|
||
// ---- Fast: O(1) per call after O(T) setup ----
|
||
|
||
static List<Index> fastFindDangling(List<Index> diskIndices, List<Tombstone> tombstones) {
|
||
Set<Index> graveyardSet = new HashSet<>(tombstones.size() * 2);
|
||
for (Tombstone t : tombstones) graveyardSet.add(t.getIndex());
|
||
List<Index> dangling = new ArrayList<>();
|
||
for (Index index : diskIndices) {
|
||
if (!graveyardSet.contains(index)) dangling.add(index);
|
||
}
|
||
return dangling;
|
||
}
|
||
|
||
static long countSlowOps(List<Index> diskIndices, List<Tombstone> tombstones) {
|
||
long ops = 0;
|
||
for (Index index : diskIndices) {
|
||
for (Tombstone t : tombstones) {
|
||
ops++;
|
||
if (t.getIndex().equals(index)) break;
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static long countFastOps(List<Index> diskIndices, List<Tombstone> tombstones) {
|
||
long ops = 0;
|
||
for (Tombstone t : tombstones) ops++; // build set: O(T)
|
||
for (Index ignored : diskIndices) ops++; // lookup: O(1) each
|
||
return ops;
|
||
}
|
||
|
||
public static void main(String[] args) {
|
||
int passed = 0;
|
||
|
||
// Test 1: correctness
|
||
{
|
||
List<Tombstone> tombstones = new ArrayList<>();
|
||
List<Index> diskIndices = new ArrayList<>();
|
||
for (int i = 0; i < 15; i++) {
|
||
tombstones.add(new Tombstone(new Index("idx-" + i, "u" + i)));
|
||
}
|
||
for (int i = 0; i < 25; i++) {
|
||
diskIndices.add(new Index("idx-" + i, "u" + i));
|
||
}
|
||
List<Index> slow = slowFindDangling(diskIndices, tombstones);
|
||
List<Index> fast = fastFindDangling(diskIndices, tombstones);
|
||
assert slow.size() == fast.size()
|
||
: "FAIL: mismatch slow=" + slow.size() + " fast=" + fast.size();
|
||
System.out.println("PASS test1: correctness — " + fast.size() + " dangling");
|
||
passed++;
|
||
}
|
||
|
||
// Test 2: all tombstoned
|
||
{
|
||
List<Tombstone> tombstones = new ArrayList<>();
|
||
List<Index> diskIndices = new ArrayList<>();
|
||
for (int i = 0; i < 30; i++) {
|
||
tombstones.add(new Tombstone(new Index("idx-" + i, "u" + i)));
|
||
diskIndices.add(new Index("idx-" + i, "u" + i));
|
||
}
|
||
assert slowFindDangling(diskIndices, tombstones).size() == 0 : "FAIL slow";
|
||
assert fastFindDangling(diskIndices, tombstones).size() == 0 : "FAIL fast";
|
||
System.out.println("PASS test2: all tombstoned");
|
||
passed++;
|
||
}
|
||
|
||
// Test 3: operation count ratio >= 10x
|
||
{
|
||
int I = 500, T = 500;
|
||
List<Tombstone> tombstones = new ArrayList<>();
|
||
List<Index> diskIndices = new ArrayList<>();
|
||
for (int i = 0; i < T; i++) tombstones.add(new Tombstone(new Index("t" + i, "ut" + i)));
|
||
for (int i = 0; i < I; i++) diskIndices.add(new Index("d" + i, "ud" + i));
|
||
|
||
long slowOps = countSlowOps(diskIndices, tombstones);
|
||
long fastOps = countFastOps(diskIndices, tombstones);
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf("PASS test3: I=%d T=%d slow=%d fast=%d ratio=%.1fx%n",
|
||
I, T, slowOps, fastOps, ratio);
|
||
assert ratio >= 10.0 : "FAIL: ratio " + ratio + " < 10x";
|
||
passed++;
|
||
}
|
||
|
||
// Test 4: timing benchmark
|
||
{
|
||
int I = 2000, T = 500;
|
||
List<Tombstone> tombstones = new ArrayList<>();
|
||
List<Index> diskIndices = new ArrayList<>();
|
||
for (int i = 0; i < T; i++) tombstones.add(new Tombstone(new Index("t" + i, "ut" + i)));
|
||
for (int i = 0; i < I; i++) diskIndices.add(new Index("d" + i, "ud" + i));
|
||
|
||
int reps = 200;
|
||
long t0 = System.nanoTime();
|
||
for (int r = 0; r < reps; r++) slowFindDangling(diskIndices, tombstones);
|
||
long slowNs = System.nanoTime() - t0;
|
||
|
||
long t1 = System.nanoTime();
|
||
for (int r = 0; r < reps; r++) fastFindDangling(diskIndices, tombstones);
|
||
long fastNs = System.nanoTime() - t1;
|
||
|
||
double ratio = (double) slowNs / fastNs;
|
||
System.out.printf("PASS test4: timing I=%d T=%d slow=%.1fms fast=%.1fms ratio=%.1fx%n",
|
||
I, T, slowNs / 1e6 / reps, fastNs / 1e6 / reps, ratio);
|
||
assert ratio >= 10.0 : "FAIL: timing ratio " + ratio + " < 10x";
|
||
passed++;
|
||
}
|
||
|
||
System.out.println(passed + "/" + passed + " PASS");
|
||
}
|
||
}
|