|
28 | 28 | import java.util.List; |
29 | 29 | import java.util.Random; |
30 | 30 | import java.util.Set; |
| 31 | +import java.util.concurrent.CountDownLatch; |
| 32 | +import java.util.concurrent.TimeUnit; |
| 33 | +import java.util.concurrent.atomic.AtomicReference; |
| 34 | +import java.util.concurrent.locks.ReadWriteLock; |
31 | 35 | import java.util.stream.Collectors; |
32 | 36 |
|
33 | 37 | import org.eclipse.rdf4j.model.IRI; |
@@ -407,6 +411,58 @@ public void testStaleBNodeHashCodeIgnoresReusedIdAfterClear() throws Exception { |
407 | 411 | assertEquals("stale values must keep their original hash after a revision change", firstHash, first.hashCode()); |
408 | 412 | } |
409 | 413 |
|
| 414 | + @Test |
| 415 | + public void testReadersFullCleanupDoesNotCloseInUseReadTxn() throws Exception { |
| 416 | + ReadWriteLock txnLock = getField(valueStore, "txnLock", ReadWriteLock.class); |
| 417 | + Set<?> activeReadTransactions = getField(valueStore, "activeReadTransactions", Set.class); |
| 418 | + long env = getLongField(valueStore, "env"); |
| 419 | + |
| 420 | + CountDownLatch readLockReleased = new CountDownLatch(1); |
| 421 | + CountDownLatch cleanupAttempted = new CountDownLatch(1); |
| 422 | + AtomicReference<Throwable> workerFailure = new AtomicReference<>(); |
| 423 | + |
| 424 | + Thread worker = new Thread(() -> { |
| 425 | + try { |
| 426 | + valueStore.readTransaction(env, (stack, txn) -> { |
| 427 | + txnLock.readLock().unlock(); |
| 428 | + try { |
| 429 | + readLockReleased.countDown(); |
| 430 | + try { |
| 431 | + assertTrue("cleanup should run", cleanupAttempted.await(10, TimeUnit.SECONDS)); |
| 432 | + } catch (InterruptedException e) { |
| 433 | + Thread.currentThread().interrupt(); |
| 434 | + throw new IOException(e); |
| 435 | + } |
| 436 | + } finally { |
| 437 | + txnLock.readLock().lock(); |
| 438 | + } |
| 439 | + return null; |
| 440 | + }); |
| 441 | + } catch (Throwable t) { |
| 442 | + workerFailure.set(t); |
| 443 | + } |
| 444 | + }, "value-store-read-transaction-gap"); |
| 445 | + worker.start(); |
| 446 | + |
| 447 | + assertTrue("read transaction should reach the resize-style read lock gap", |
| 448 | + readLockReleased.await(10, TimeUnit.SECONDS)); |
| 449 | + assertEquals("test setup should have one active read transaction", 1, activeReadTransactions.size()); |
| 450 | + |
| 451 | + try { |
| 452 | + invokeCloseInactiveReadTransactions(valueStore); |
| 453 | + assertEquals("reader cleanup must not close a read transaction that is still executing", 1, |
| 454 | + activeReadTransactions.size()); |
| 455 | + } finally { |
| 456 | + cleanupAttempted.countDown(); |
| 457 | + worker.join(TimeUnit.SECONDS.toMillis(10)); |
| 458 | + } |
| 459 | + |
| 460 | + assertFalse("worker should finish", worker.isAlive()); |
| 461 | + if (workerFailure.get() != null) { |
| 462 | + throw new AssertionError(workerFailure.get()); |
| 463 | + } |
| 464 | + } |
| 465 | + |
410 | 466 | private long storeValueAndReopen(Value value) throws Exception { |
411 | 467 | return storeValueAndReopen(value, new LmdbStoreConfig()); |
412 | 468 | } |
@@ -438,6 +494,24 @@ private boolean isInitialized(Object value) throws Exception { |
438 | 494 | return initializedField.getBoolean(value); |
439 | 495 | } |
440 | 496 |
|
| 497 | + private <T> T getField(Object target, String fieldName, Class<T> fieldType) throws Exception { |
| 498 | + Field field = target.getClass().getDeclaredField(fieldName); |
| 499 | + field.setAccessible(true); |
| 500 | + return fieldType.cast(field.get(target)); |
| 501 | + } |
| 502 | + |
| 503 | + private long getLongField(Object target, String fieldName) throws Exception { |
| 504 | + Field field = target.getClass().getDeclaredField(fieldName); |
| 505 | + field.setAccessible(true); |
| 506 | + return field.getLong(target); |
| 507 | + } |
| 508 | + |
| 509 | + private void invokeCloseInactiveReadTransactions(ValueStore store) throws Exception { |
| 510 | + var method = store.getClass().getDeclaredMethod("closeInactiveReadTransactions"); |
| 511 | + method.setAccessible(true); |
| 512 | + method.invoke(store); |
| 513 | + } |
| 514 | + |
441 | 515 | @AfterEach |
442 | 516 | public void after() throws Exception { |
443 | 517 | try { |
|
0 commit comments