Skip to content

Commit 901235b

Browse files
Tests: closing Store while Transaction is active should not crash
1 parent 88e3122 commit 901235b

File tree

3 files changed

+103
-5
lines changed

3 files changed

+103
-5
lines changed

objectbox-java/src/main/java/io/objectbox/InternalAccess.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 ObjectBox Ltd. All rights reserved.
2+
* Copyright 2017-2024 ObjectBox Ltd. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,12 @@
2121
import io.objectbox.annotation.apihint.Internal;
2222
import io.objectbox.sync.SyncClient;
2323

24+
/**
25+
* This is a workaround to access internal APIs, notably for tests.
26+
* <p>
27+
* To avoid this, future APIs should be exposed via interfaces with an internal implementation that can be used by
28+
* tests.
29+
*/
2430
@Internal
2531
public class InternalAccess {
2632

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 ObjectBox Ltd. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.objectbox.query;
18+
19+
import io.objectbox.annotation.apihint.Internal;
20+
21+
/**
22+
* This is a workaround to access internal APIs for tests.
23+
* <p>
24+
* To avoid this, future APIs should be exposed via interfaces with an internal implementation that can be used by
25+
* tests.
26+
*/
27+
@Internal
28+
public class InternalQueryAccess {
29+
30+
/**
31+
* For testing only.
32+
*/
33+
public static <T> void nativeFindFirst(Query<T> query, long cursorHandle) {
34+
query.nativeFindFirst(query.handle, cursorHandle);
35+
}
36+
37+
}

tests/objectbox-java-test/src/test/java/io/objectbox/TransactionTest.java

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 ObjectBox Ltd. All rights reserved.
2+
* Copyright 2017-2024 ObjectBox Ltd. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,10 @@
1616

1717
package io.objectbox;
1818

19+
import org.junit.Ignore;
20+
import org.junit.Test;
21+
import org.junit.function.ThrowingRunnable;
22+
1923
import java.io.IOException;
2024
import java.util.ArrayList;
2125
import java.util.concurrent.Callable;
@@ -27,13 +31,14 @@
2731
import java.util.concurrent.ThreadPoolExecutor;
2832
import java.util.concurrent.TimeUnit;
2933
import java.util.concurrent.atomic.AtomicInteger;
34+
import java.util.concurrent.atomic.AtomicReference;
3035

3136
import io.objectbox.exception.DbException;
3237
import io.objectbox.exception.DbExceptionListener;
3338
import io.objectbox.exception.DbMaxReadersExceededException;
34-
import org.junit.Ignore;
35-
import org.junit.Test;
36-
import org.junit.function.ThrowingRunnable;
39+
import io.objectbox.query.InternalQueryAccess;
40+
import io.objectbox.query.Query;
41+
3742

3843
import static org.junit.Assert.assertArrayEquals;
3944
import static org.junit.Assert.assertEquals;
@@ -44,6 +49,7 @@
4449
import static org.junit.Assert.assertThrows;
4550
import static org.junit.Assert.assertTrue;
4651
import static org.junit.Assert.fail;
52+
import static org.junit.Assume.assumeFalse;
4753

4854
public class TransactionTest extends AbstractObjectBoxTest {
4955

@@ -315,6 +321,55 @@ private void assertThrowsTxClosed(ThrowingRunnable runnable) {
315321
assertEquals("Transaction is closed", ex.getMessage());
316322
}
317323

324+
@Test
325+
public void nativeCallInTx_storeIsClosed_throws() throws InterruptedException {
326+
// Ignore test on Windows, it was observed to crash with EXCEPTION_ACCESS_VIOLATION
327+
assumeFalse(TestUtils.isWindows());
328+
329+
System.out.println("NOTE This test will cause \"Transaction is still active\" and \"Irrecoverable memory error\" error logs!");
330+
331+
CountDownLatch callableIsReady = new CountDownLatch(1);
332+
CountDownLatch storeIsClosed = new CountDownLatch(1);
333+
CountDownLatch callableIsDone = new CountDownLatch(1);
334+
AtomicReference<Exception> callableException = new AtomicReference<>();
335+
336+
// Goal: be just passed closed checks on the Java side, about to call a native query API.
337+
// Then close the Store, then call the native API. The native API call should not crash the VM.
338+
Callable<Void> waitingCallable = () -> {
339+
Box<TestEntity> box = store.boxFor(TestEntity.class);
340+
Query<TestEntity> query = box.query().build();
341+
// Obtain Cursor handle before closing the Store as getActiveTxCursor() has a closed check
342+
long cursorHandle = io.objectbox.InternalAccess.getActiveTxCursorHandle(box);
343+
344+
callableIsReady.countDown();
345+
try {
346+
if (!storeIsClosed.await(5, TimeUnit.SECONDS)) {
347+
throw new IllegalStateException("Store did not close within 5 seconds");
348+
}
349+
// Call native query API within the transaction (opened by callInReadTx below)
350+
InternalQueryAccess.nativeFindFirst(query, cursorHandle);
351+
query.close();
352+
} catch (Exception e) {
353+
callableException.set(e);
354+
}
355+
callableIsDone.countDown();
356+
return null;
357+
};
358+
new Thread(() -> store.callInReadTx(waitingCallable)).start();
359+
360+
callableIsReady.await();
361+
store.close();
362+
storeIsClosed.countDown();
363+
364+
if (!callableIsDone.await(10, TimeUnit.SECONDS)) {
365+
fail("Callable did not finish within 10 seconds");
366+
}
367+
Exception exception = callableException.get();
368+
assertTrue(exception instanceof IllegalStateException);
369+
// Note: the "State" at the end of the message may be different depending on platform, so only assert prefix
370+
assertTrue(exception.getMessage().startsWith("Illegal Store instance detected! This is a severe usage error that must be fixed."));
371+
}
372+
318373
@Test
319374
public void testRunInTxRecursive() {
320375
final Box<TestEntity> box = getTestEntityBox();

0 commit comments

Comments
 (0)