package unit; import java.util.*; import java.nio.ByteBuffer; /** * Cassandra CWE-407 unit tests — standalone, no JUnit. * * cassandra-0001 DEAD_STATES/SILENT_SHUTDOWN_STATES List.contains() per endpoint * src/java/org/apache/cassandra/gms/Gossiper.java:147,1334,1343 */ public class CassandraTest { // ----------------------------------------------------------------------- // cassandra-0001: isDeadState() — List.contains() vs Set.contains() // // Models the per-gossip-tick loop: for each of N endpoints, check whether // the endpoint's status string is in the dead-states collection. // Slow: List (ArrayList / Arrays.asList) — O(|states|) per lookup. // Fast: HashSet — O(1) per lookup. // ----------------------------------------------------------------------- static final String[] STATUS_STRINGS = { "REMOVING_TOKEN", "REMOVED_TOKEN", "STATUS_LEFT", "HIBERNATE", "NORMAL", "BOOTSTRAPPING", "JOINING", "LEAVING" }; static final String[] DEAD_STATES_ARR = { "REMOVING_TOKEN", "REMOVED_TOKEN", "STATUS_LEFT", "HIBERNATE" }; /** * Slow: DEAD_STATES is an ArrayList; membership test is O(|DEAD_STATES|). * Called once per endpoint per gossip round. * Returns total comparison operations performed. */ static long deadStateSlowOps(int numEndpoints) { List deadStates = new ArrayList<>(Arrays.asList(DEAD_STATES_ARR)); // Simulate SILENT_SHUTDOWN_STATES built as ArrayList too List silentStates = new ArrayList<>(deadStates); long ops = 0; Random rng = new Random(42); for (int ep = 0; ep < numEndpoints; ep++) { // Each endpoint has a status — pick one at random String status = STATUS_STRINGS[rng.nextInt(STATUS_STRINGS.length)]; // isDeadState: scan deadStates list for (int i = 0; i < deadStates.size(); i++) { ops++; if (deadStates.get(i).equals(status)) break; } // isSilentShutdownState: scan silentStates list for (int i = 0; i < silentStates.size(); i++) { ops++; if (silentStates.get(i).equals(status)) break; } } return ops; } /** * Fast: DEAD_STATES and SILENT_SHUTDOWN_STATES are HashSet; O(1) lookup. * Returns total comparison operations performed (one per endpoint per check). */ static long deadStateFastOps(int numEndpoints) { Set deadStates = new HashSet<>(Arrays.asList(DEAD_STATES_ARR)); Set silentStates = new HashSet<>(deadStates); long ops = 0; Random rng = new Random(42); for (int ep = 0; ep < numEndpoints; ep++) { String status = STATUS_STRINGS[rng.nextInt(STATUS_STRINGS.length)]; // isDeadState: O(1) hash lookup — count as 1 op ops++; deadStates.contains(status); // isSilentShutdownState: O(1) hash lookup — count as 1 op ops++; silentStates.contains(status); } return ops; } // ----------------------------------------------------------------------- // cassandra-0005: Lists.Discarder.execute() — List.contains() per cell // // CQL: DELETE items[item_val] FROM t WHERE pk = x // → for each cell in complexData, checks toDiscard.contains(cell.buffer()) // → toDiscard is a plain ArrayList // → O(|existingList| * |toDiscard|) — quadratic when both sides are large. // // Slow: List — O(D) per cell lookup // Fast: HashSet — O(1) per cell lookup // // src/java/org/apache/cassandra/cql3/terms/Lists.java line 524-528 // ----------------------------------------------------------------------- /** * Build a byte-buffer list of D distinct "discard" values, then scan E existing cells * checking each against the discard collection. Returns total comparisons performed. */ static long discardSlowOps(int existingCells, int discardValues) { // Simulate toDiscard = value.getElements() — a plain ArrayList List toDiscard = new ArrayList<>(discardValues); for (int i = 0; i < discardValues; i++) { toDiscard.add(ByteBuffer.wrap(new byte[]{(byte)(i & 0xff), (byte)((i >> 8) & 0xff)})); } long ops = 0; // Simulate complexData — cells with values drawn from a wider range // (some will match toDiscard, most won't) for (int i = 0; i < existingCells; i++) { ByteBuffer cellBuf = ByteBuffer.wrap( new byte[]{(byte)(i & 0xff), (byte)((i >> 8) & 0xff)}); // List.contains(): scan toDiscard until found or exhausted boolean found = false; for (int j = 0; j < toDiscard.size(); j++) { ops++; if (toDiscard.get(j).equals(cellBuf)) { found = true; break; } } } return ops; } static long discardFastOps(int existingCells, int discardValues) { // Simulate fix: HashSet built from value.getElements() before the loop Set toDiscard = new HashSet<>(discardValues); for (int i = 0; i < discardValues; i++) { toDiscard.add(ByteBuffer.wrap(new byte[]{(byte)(i & 0xff), (byte)((i >> 8) & 0xff)})); } long ops = 0; for (int i = 0; i < existingCells; i++) { ByteBuffer cellBuf = ByteBuffer.wrap( new byte[]{(byte)(i & 0xff), (byte)((i >> 8) & 0xff)}); ops++; // O(1) hash lookup — count as single op toDiscard.contains(cellBuf); } return ops; } // ----------------------------------------------------------------------- // Test runner // ----------------------------------------------------------------------- public static void main(String[] args) { int passed = 0; int failed = 0; // cassandra-0001 at various cluster sizes int[] sizes = {100, 500, 1000, 2000}; for (int n : sizes) { long slow = deadStateSlowOps(n); long fast = deadStateFastOps(n); // Slow should be > 2x fast: List scan vs O(1) set // At minimum 2x because DEAD_STATES has 4 entries and ~half statuses // won't be found until partway through the list. boolean pass = slow > fast * 2; System.out.printf("cassandra-0001 N=%-5d slow=%6d fast=%6d ratio=%.1fx %s%n", n, slow, fast, (double) slow / fast, pass ? "PASS" : "FAIL"); if (pass) passed++; else failed++; } // cassandra-0005: Discarder.execute() — existingCells x discardValues // Use D=100 discard values (not unreasonable for a batch DELETE) and growing E int discardValues = 100; int[] cellCounts = {500, 1000, 2000, 5000}; for (int e : cellCounts) { long slow = discardSlowOps(e, discardValues); long fast = discardFastOps(e, discardValues); // Ratio should be ~D/2 on average (linear scan finds match halfway through) boolean pass = slow >= fast * 5; System.out.printf( "cassandra-0005 E=%-5d D=%-4d slow=%8d fast=%8d ratio=%.1fx %s%n", e, discardValues, slow, fast, (double) slow / fast, pass ? "PASS" : "FAIL"); if (pass) passed++; else failed++; } System.out.printf("%nTotal: %d/%d PASS%n", passed, passed + failed); if (failed > 0) System.exit(1); } }