2626import java .io .IOException ;
2727import java .nio .file .Path ;
2828import java .util .*;
29+ import java .util .concurrent .CountDownLatch ;
2930import java .util .concurrent .ExecutorService ;
3031import java .util .concurrent .Executors ;
3132import java .util .concurrent .TimeUnit ;
33+ import java .util .concurrent .atomic .AtomicReference ;
3234
3335import org .apache .logging .log4j .LogManager ;
3436import org .apache .logging .log4j .Logger ;
5759import org .xml .sax .SAXException ;
5860import org .xmldb .api .DatabaseManager ;
5961import org .xmldb .api .base .Database ;
62+ import org .xmldb .api .base .ErrorCodes ;
6063import org .xmldb .api .base .ResourceSet ;
6164import org .xmldb .api .base .XMLDBException ;
6265import 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