java-topology/defects/opensearch/unit/IndexGraveyardDanglingContains.java

177 lines
6.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}