Skip to content

Commit 998bb47

Browse files
authored
Terminate linearizability check early on large histories (#44444)
Large histories can be problematic and have the linearizability checker occasionally run OOM. As it's very difficult to bound the size of the histories just right, this PR will let it instead run for 10 seconds on large histories and then abort. Closes #44429
1 parent c8ae530 commit 998bb47

File tree

2 files changed

+38
-9
lines changed

2 files changed

+38
-9
lines changed

server/src/test/java/org/elasticsearch/versioning/ConcurrentSeqNoVersioningIT.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import org.elasticsearch.index.engine.VersionConflictEngineException;
3939
import org.elasticsearch.test.ESIntegTestCase;
4040
import org.elasticsearch.test.disruption.ServiceDisruptionScheme;
41+
import org.elasticsearch.threadpool.Scheduler;
42+
import org.elasticsearch.threadpool.ThreadPool;
4143

4244
import java.io.FileInputStream;
4345
import java.io.IOException;
@@ -50,8 +52,10 @@
5052
import java.util.Random;
5153
import java.util.concurrent.BrokenBarrierException;
5254
import java.util.concurrent.CyclicBarrier;
55+
import java.util.concurrent.ScheduledThreadPoolExecutor;
5356
import java.util.concurrent.TimeUnit;
5457
import java.util.concurrent.TimeoutException;
58+
import java.util.concurrent.atomic.AtomicBoolean;
5559
import java.util.concurrent.atomic.AtomicReference;
5660
import java.util.function.Consumer;
5761
import java.util.function.Function;
@@ -437,13 +441,24 @@ private void consumeOutput(HistoryOutput output, int eventId) {
437441
}
438442
}
439443

440-
public boolean isLinearizable() {
444+
public void assertLinearizable() {
441445
logger.info("--> Linearizability checking history of size: {} for key: {} and initialVersion: {}: {}", history.size(),
442446
id, initialVersion, history);
443447
LinearizabilityChecker.SequentialSpec spec = new CASSequentialSpec(initialVersion);
444448
boolean linearizable = false;
445449
try {
446-
linearizable = new LinearizabilityChecker().isLinearizable(spec, history, missingResponseGenerator());
450+
final ScheduledThreadPoolExecutor scheduler = Scheduler.initScheduler(Settings.EMPTY);
451+
final AtomicBoolean abort = new AtomicBoolean();
452+
// Large histories can be problematic and have the linearizability checker run OOM
453+
// Bound the time how long the checker can run on such histories (Values empirically determined)
454+
if (history.size() > 300) {
455+
scheduler.schedule(() -> abort.set(true), 10, TimeUnit.SECONDS);
456+
}
457+
linearizable = new LinearizabilityChecker().isLinearizable(spec, history, missingResponseGenerator(), abort::get);
458+
ThreadPool.terminate(scheduler, 1, TimeUnit.SECONDS);
459+
if (abort.get() && linearizable == false) {
460+
linearizable = true; // let the test pass
461+
}
447462
} finally {
448463
// implicitly test that we can serialize all histories.
449464
String serializedHistory = base64Serialize(history);
@@ -453,11 +468,7 @@ public boolean isLinearizable() {
453468
spec, initialVersion, serializedHistory);
454469
}
455470
}
456-
return linearizable;
457-
}
458-
459-
public void assertLinearizable() {
460-
assertTrue("Must be linearizable", isLinearizable());
471+
assertTrue("Must be linearizable", linearizable);
461472
}
462473
}
463474

test/framework/src/main/java/org/elasticsearch/cluster/coordination/LinearizabilityChecker.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.util.Set;
4040
import java.util.concurrent.ConcurrentLinkedQueue;
4141
import java.util.concurrent.atomic.AtomicInteger;
42+
import java.util.function.BooleanSupplier;
4243
import java.util.function.Consumer;
4344
import java.util.function.Function;
4445

@@ -227,13 +228,27 @@ public String toString() {
227228
* @return true iff the history is linearizable w.r.t. the given spec
228229
*/
229230
public boolean isLinearizable(SequentialSpec spec, History history, Function<Object, Object> missingResponseGenerator) {
231+
return isLinearizable(spec, history, missingResponseGenerator, () -> false);
232+
}
233+
234+
/**
235+
* Checks whether the provided history is linearizable with respect to the given sequential specification
236+
*
237+
* @param spec the sequential specification of the datatype
238+
* @param history the history of events to check for linearizability
239+
* @param missingResponseGenerator used to complete the history with missing responses
240+
* @param terminateEarly a condition upon which to terminate early
241+
* @return true iff the history is linearizable w.r.t. the given spec
242+
*/
243+
public boolean isLinearizable(SequentialSpec spec, History history, Function<Object, Object> missingResponseGenerator,
244+
BooleanSupplier terminateEarly) {
230245
history = history.clone(); // clone history before completing it
231246
history.complete(missingResponseGenerator); // complete history
232247
final Collection<List<Event>> partitions = spec.partition(history.copyEvents());
233-
return partitions.stream().allMatch(h -> isLinearizable(spec, h));
248+
return partitions.stream().allMatch(h -> isLinearizable(spec, h, terminateEarly));
234249
}
235250

236-
private boolean isLinearizable(SequentialSpec spec, List<Event> history) {
251+
private boolean isLinearizable(SequentialSpec spec, List<Event> history, BooleanSupplier terminateEarly) {
237252
logger.debug("Checking history of size: {}: {}", history.size(), history);
238253
Object state = spec.initialState(); // the current state of the datatype
239254
final FixedBitSet linearized = new FixedBitSet(history.size() / 2); // the linearized prefix of the history
@@ -245,6 +260,9 @@ private boolean isLinearizable(SequentialSpec spec, List<Event> history) {
245260
Entry entry = headEntry.next; // current entry
246261

247262
while (headEntry.next != null) {
263+
if (terminateEarly.getAsBoolean()) {
264+
return false;
265+
}
248266
if (entry.match != null) {
249267
final Optional<Object> maybeNextState = spec.nextState(state, entry.event.value, entry.match.event.value);
250268
boolean shouldExploreNextState = false;

0 commit comments

Comments
 (0)