Skip to content

Commit 8b22aba

Browse files
duncdrumcursoragent
andcommitted
[test] Fix DeadlockIT infinite loop in testRemoved mode
Correct RemoveDocumentTask document ID calculation to match StoreTask naming, cap remove retries, and propagate worker failures to the test thread so CI fails fast instead of hanging. Share document naming between store and remove tasks, wait for store completion before removals, use per-test failure tracking, and retry when concurrent removes race on the same resource. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8436a40 commit 8b22aba

1 file changed

Lines changed: 72 additions & 14 deletions

File tree

exist-core/src/test/java/org/exist/storage/lock/DeadlockIT.java

Lines changed: 72 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626
import java.io.IOException;
2727
import java.nio.file.Path;
2828
import java.util.*;
29+
import java.util.concurrent.CountDownLatch;
2930
import java.util.concurrent.ExecutorService;
3031
import java.util.concurrent.Executors;
3132
import java.util.concurrent.TimeUnit;
33+
import java.util.concurrent.atomic.AtomicReference;
3234

3335
import org.apache.logging.log4j.LogManager;
3436
import org.apache.logging.log4j.Logger;
@@ -57,6 +59,7 @@
5759
import org.xml.sax.SAXException;
5860
import org.xmldb.api.DatabaseManager;
5961
import org.xmldb.api.base.Database;
62+
import org.xmldb.api.base.ErrorCodes;
6063
import org.xmldb.api.base.ResourceSet;
6164
import org.xmldb.api.base.XMLDBException;
6265
import org.xmldb.api.base.Resource;
@@ -91,6 +94,38 @@ public class DeadlockIT {
9194
/** Max time to wait for executor to finish (fail fast instead of hanging CI). */
9295
private static final int AWAIT_TERMINATION_MINUTES = 5;
9396

97+
/** Max attempts to find and remove an existing document before failing. */
98+
private static final int MAX_REMOVE_ATTEMPTS = 100;
99+
100+
private final AtomicReference<Throwable> taskFailure = new AtomicReference<>();
101+
102+
private void recordTaskFailure(final Throwable t) {
103+
taskFailure.compareAndSet(null, t);
104+
}
105+
106+
private void rethrowTaskFailure() {
107+
final Throwable failure = taskFailure.get();
108+
if (failure != null) {
109+
if (failure instanceof RuntimeException re) {
110+
throw re;
111+
}
112+
if (failure instanceof Error err) {
113+
throw err;
114+
}
115+
throw new AssertionError(failure.getMessage(), failure);
116+
}
117+
}
118+
119+
/** Matches {@link StoreTask} global document numbering. */
120+
private static String documentName(final int collectionId, final int indexInCollection) {
121+
return "test" + (collectionId * DOC_COUNT + indexInCollection) + ".xml";
122+
}
123+
124+
private static boolean isConcurrentRemoveRace(final XMLDBException e) {
125+
return e.errorCode == ErrorCodes.INVALID_RESOURCE
126+
|| e.errorCode == ErrorCodes.NO_SUCH_RESOURCE;
127+
}
128+
94129
/** Use 4 test runs, querying different collections */
95130
@Parameters(name = "{0}")
96131
public static java.util.Collection<Object[]> data() {
@@ -185,8 +220,10 @@ public void clearDB() throws XMLDBException {
185220

186221
@Test(timeout = (AWAIT_TERMINATION_MINUTES + 1) * 60 * 1000)
187222
public void runTasks() {
223+
taskFailure.set(null);
188224
final ExecutorService executor = Executors.newFixedThreadPool(N_THREADS);
189-
executor.submit(new StoreTask("store", COLL_COUNT, DOC_COUNT));
225+
final CountDownLatch storeComplete = new CountDownLatch(1);
226+
executor.submit(new StoreTask("store", COLL_COUNT, DOC_COUNT, storeComplete));
190227
synchronized (this) {
191228
try {
192229
wait(DELAY);
@@ -200,6 +237,15 @@ public void runTasks() {
200237
executor.submit(new QueryTask(COLL_COUNT));
201238
}
202239
if (mode == TEST_REMOVE) {
240+
try {
241+
assertTrue("Store task did not finish before document removals started",
242+
storeComplete.await(AWAIT_TERMINATION_MINUTES, TimeUnit.MINUTES));
243+
} catch (InterruptedException e) {
244+
Thread.currentThread().interrupt();
245+
LOG.error(e.getMessage(), e);
246+
fail(e.getMessage());
247+
}
248+
rethrowTaskFailure();
203249
for (int i = 0; i < REMOVE_COUNT; i++) {
204250
executor.submit(new RemoveDocumentTask(COLL_COUNT, DOC_COUNT));
205251
}
@@ -217,19 +263,23 @@ public void runTasks() {
217263
executor.shutdownNow();
218264
assertTrue("Executor did not terminate within " + AWAIT_TERMINATION_MINUTES + " minutes; possible deadlock or hang", terminated);
219265
}
266+
rethrowTaskFailure();
220267
}
221268

222-
private static class StoreTask implements Runnable {
269+
private class StoreTask implements Runnable {
223270

224271
@SuppressWarnings("unused")
225272
private final String id;
226273
private final int docCount;
227274
private final int collectionCount;
275+
private final CountDownLatch storeComplete;
228276

229-
public StoreTask(final String id, final int collectionCount, final int docCount) {
277+
public StoreTask(final String id, final int collectionCount, final int docCount,
278+
final CountDownLatch storeComplete) {
230279
this.id = id;
231280
this.collectionCount = collectionCount;
232281
this.docCount = docCount;
282+
this.storeComplete = storeComplete;
233283
}
234284

235285
@Override
@@ -240,7 +290,6 @@ public void run() {
240290

241291
final TestDataGenerator generator = new TestDataGenerator("xdb", docCount);
242292
Collection coll;
243-
int fileCount = 0;
244293
for (int i = 0; i < collectionCount; i++) {
245294
try(final Txn transaction = transact.beginTransaction()) {
246295
coll = broker.getOrCreateCollection(transaction,
@@ -252,21 +301,23 @@ public void run() {
252301
}
253302

254303
final Path[] files = generator.generate(broker, coll, generateXQ);
255-
for (int j = 0; j < files.length; j++, fileCount++) {
304+
for (int j = 0; j < files.length; j++) {
256305
try(final Txn transaction = transact.beginTransaction()) {
257306
final InputSource is = new InputSource(files[j].toUri()
258307
.toASCIIString());
259308

260-
broker.storeDocument(transaction, XmldbURI.create("test" + fileCount + ".xml"), is, MimeType.XML_TYPE, coll);
309+
broker.storeDocument(transaction, XmldbURI.create(documentName(i, j)), is, MimeType.XML_TYPE, coll);
261310
transact.commit(transaction);
262311
}
263312
}
264313
generator.releaseAll();
265314
}
266315
} catch (Exception e) {
267316
LOG.error(e.getMessage(), e);
268-
// fail(e.getMessage());
269-
}
317+
recordTaskFailure(e);
318+
} finally {
319+
storeComplete.countDown();
320+
}
270321
}
271322
}
272323

@@ -331,7 +382,7 @@ public void run() {
331382
}
332383
} catch (Exception e) {
333384
LOG.error(e.getMessage(), e);
334-
fail(e.getMessage());
385+
recordTaskFailure(e);
335386
}
336387
}
337388
}
@@ -348,11 +399,10 @@ public RemoveDocumentTask(final int collectionCount, final int documentCount) {
348399
@Override
349400
public void run() {
350401
boolean removed = false;
351-
do {
402+
for (int attempt = 0; !removed && attempt < MAX_REMOVE_ATTEMPTS; attempt++) {
352403
final int collectionId = random.nextInt(collectionCount);
353404
final String collection = "/db/test/" + collectionId;
354-
final int docId = random.nextInt(documentCount) * collectionId;
355-
final String document = "test" + docId + ".xml";
405+
final String document = documentName(collectionId, random.nextInt(documentCount));
356406
try {
357407
final org.xmldb.api.base.Collection testCollection = DatabaseManager.getCollection("xmldb:exist://" + collection, "admin", "");
358408
final Resource resource = testCollection.getResource(document);
@@ -361,10 +411,18 @@ public void run() {
361411
removed = true;
362412
}
363413
} catch (final XMLDBException e) {
414+
if (isConcurrentRemoveRace(e)) {
415+
continue;
416+
}
364417
LOG.error(e.getMessage(), e);
365-
fail(e.getMessage());
418+
recordTaskFailure(e);
419+
return;
366420
}
367-
} while (!removed);
421+
}
422+
if (!removed) {
423+
recordTaskFailure(new AssertionError(
424+
"Could not remove a document after " + MAX_REMOVE_ATTEMPTS + " attempts"));
425+
}
368426
}
369427
}
370428
}

0 commit comments

Comments
 (0)