Skip to content

Commit ef372d9

Browse files
duncdrumcursoragent
andcommitted
[test] Harden DeadlockIT store/remove coordination
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 889636e commit ef372d9

1 file changed

Lines changed: 53 additions & 21 deletions

File tree

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

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
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;
@@ -58,6 +59,7 @@
5859
import org.xml.sax.SAXException;
5960
import org.xmldb.api.DatabaseManager;
6061
import org.xmldb.api.base.Database;
62+
import org.xmldb.api.base.ErrorCodes;
6163
import org.xmldb.api.base.ResourceSet;
6264
import org.xmldb.api.base.XMLDBException;
6365
import org.xmldb.api.base.Resource;
@@ -95,12 +97,35 @@ public class DeadlockIT {
9597
/** Max attempts to find and remove an existing document before failing. */
9698
private static final int MAX_REMOVE_ATTEMPTS = 100;
9799

98-
private static final AtomicReference<Throwable> taskFailure = new AtomicReference<>();
100+
private final AtomicReference<Throwable> taskFailure = new AtomicReference<>();
99101

100-
private static void recordTaskFailure(final Throwable t) {
102+
private void recordTaskFailure(final Throwable t) {
101103
taskFailure.compareAndSet(null, t);
102104
}
103105

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+
104129
/** Use 4 test runs, querying different collections */
105130
@Parameters(name = "{0}")
106131
public static java.util.Collection<Object[]> data() {
@@ -197,7 +222,8 @@ public void clearDB() throws XMLDBException {
197222
public void runTasks() {
198223
taskFailure.set(null);
199224
final ExecutorService executor = Executors.newFixedThreadPool(N_THREADS);
200-
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));
201227
synchronized (this) {
202228
try {
203229
wait(DELAY);
@@ -211,6 +237,15 @@ public void runTasks() {
211237
executor.submit(new QueryTask(COLL_COUNT));
212238
}
213239
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();
214249
for (int i = 0; i < REMOVE_COUNT; i++) {
215250
executor.submit(new RemoveDocumentTask(COLL_COUNT, DOC_COUNT));
216251
}
@@ -228,29 +263,23 @@ public void runTasks() {
228263
executor.shutdownNow();
229264
assertTrue("Executor did not terminate within " + AWAIT_TERMINATION_MINUTES + " minutes; possible deadlock or hang", terminated);
230265
}
231-
final Throwable failure = taskFailure.get();
232-
if (failure != null) {
233-
if (failure instanceof RuntimeException re) {
234-
throw re;
235-
}
236-
if (failure instanceof Error err) {
237-
throw err;
238-
}
239-
throw new AssertionError(failure.getMessage(), failure);
240-
}
266+
rethrowTaskFailure();
241267
}
242268

243-
private static class StoreTask implements Runnable {
269+
private class StoreTask implements Runnable {
244270

245271
@SuppressWarnings("unused")
246272
private final String id;
247273
private final int docCount;
248274
private final int collectionCount;
275+
private final CountDownLatch storeComplete;
249276

250-
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) {
251279
this.id = id;
252280
this.collectionCount = collectionCount;
253281
this.docCount = docCount;
282+
this.storeComplete = storeComplete;
254283
}
255284

256285
@Override
@@ -261,7 +290,6 @@ public void run() {
261290

262291
final TestDataGenerator generator = new TestDataGenerator("xdb", docCount);
263292
Collection coll;
264-
int fileCount = 0;
265293
for (int i = 0; i < collectionCount; i++) {
266294
try(final Txn transaction = transact.beginTransaction()) {
267295
coll = broker.getOrCreateCollection(transaction,
@@ -273,12 +301,12 @@ public void run() {
273301
}
274302

275303
final Path[] files = generator.generate(broker, coll, generateXQ);
276-
for (int j = 0; j < files.length; j++, fileCount++) {
304+
for (int j = 0; j < files.length; j++) {
277305
try(final Txn transaction = transact.beginTransaction()) {
278306
final InputSource is = new InputSource(files[j].toUri()
279307
.toASCIIString());
280308

281-
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);
282310
transact.commit(transaction);
283311
}
284312
}
@@ -287,7 +315,9 @@ public void run() {
287315
} catch (Exception e) {
288316
LOG.error(e.getMessage(), e);
289317
recordTaskFailure(e);
290-
}
318+
} finally {
319+
storeComplete.countDown();
320+
}
291321
}
292322
}
293323

@@ -372,8 +402,7 @@ public void run() {
372402
for (int attempt = 0; !removed && attempt < MAX_REMOVE_ATTEMPTS; attempt++) {
373403
final int collectionId = random.nextInt(collectionCount);
374404
final String collection = "/db/test/" + collectionId;
375-
final int docId = collectionId * documentCount + random.nextInt(documentCount);
376-
final String document = "test" + docId + ".xml";
405+
final String document = documentName(collectionId, random.nextInt(documentCount));
377406
try {
378407
final org.xmldb.api.base.Collection testCollection = DatabaseManager.getCollection("xmldb:exist://" + collection, "admin", "");
379408
final Resource resource = testCollection.getResource(document);
@@ -382,6 +411,9 @@ public void run() {
382411
removed = true;
383412
}
384413
} catch (final XMLDBException e) {
414+
if (isConcurrentRemoveRace(e)) {
415+
continue;
416+
}
385417
LOG.error(e.getMessage(), e);
386418
recordTaskFailure(e);
387419
return;

0 commit comments

Comments
 (0)