Skip to content

Commit 992af54

Browse files
Merge branch '240-boxstore-openfiles-deadlock' into 'dev'
BoxStore: when constructing, actually wait on finalization of other instances #240 See merge request objectbox/objectbox-java!161
2 parents bc95f51 + a43169f commit 992af54

File tree

5 files changed

+53
-11
lines changed

5 files changed

+53
-11
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ For more insights into what changed in the ObjectBox C++ core, [check the Object
66

77
## 4.3.2 - in development
88

9+
- When re-creating a `BoxStore` for the same directory and `close()` wasn't called on the previous instance, don't throw
10+
an "Another BoxStore is still open for this directory" exception. Note that calling `close()` *is recommended* before
11+
creating a new instance. [#1201](https://github.com/objectbox/objectbox-java/issues/1201)
12+
- When using `BoxStoreBuilder.buildDefault()`, don't leak Store when setting as default fails.
13+
914
## 4.3.1 - 2025-08-12
1015

1116
- Requires at least Kotlin compiler and standard library 1.7.

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

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,16 +359,34 @@ static String getCanonicalPath(File directory) {
359359
}
360360

361361
static void verifyNotAlreadyOpen(String canonicalPath) {
362+
// Call isFileOpen before, but without checking the result, to try to close any unreferenced instances where
363+
// it was forgotten to close them.
364+
// Only obtain the lock on openFiles afterward as the checker thread created by isFileOpen needs to obtain it to
365+
// do anything.
366+
isFileOpen(canonicalPath);
362367
synchronized (openFiles) {
363-
isFileOpen(canonicalPath); // for retries
364368
if (!openFiles.add(canonicalPath)) {
365-
throw new DbException("Another BoxStore is still open for this directory: " + canonicalPath +
366-
". Hint: for most apps it's recommended to keep a BoxStore for the app's life time.");
369+
throw new DbException("Another BoxStore is still open for this directory (" + canonicalPath +
370+
"). Make sure the existing instance is explicitly closed before creating a new one.");
367371
}
368372
}
369373
}
370374

371-
/** Also retries up to 500ms to improve GC race condition situation. */
375+
/**
376+
* Returns if the given path is in {@link #openFiles}.
377+
* <p>
378+
* If it is, (creates and) briefly waits on an existing "checker" thread before checking again and returning the
379+
* result.
380+
* <p>
381+
* The "checker" thread locks {@link #openFiles} while it triggers garbage collection and finalization in this Java
382+
* Virtual Machine to try to close any unreferenced BoxStore instances. These might exist if it was forgotten to
383+
* close them explicitly.
384+
* <p>
385+
* Note that the finalization mechanism relied upon here is scheduled for removal in future versions of Java and may
386+
* already be disabled depending on JVM configuration.
387+
*
388+
* @see #finalize()
389+
*/
372390
static boolean isFileOpen(final String canonicalPath) {
373391
synchronized (openFiles) {
374392
if (!openFiles.contains(canonicalPath)) return false;
@@ -522,9 +540,13 @@ public long getDbSizeOnDisk() {
522540
}
523541

524542
/**
525-
* Closes this if this is finalized.
543+
* Calls {@link #close()}.
544+
* <p>
545+
* It is strongly recommended to instead explicitly close a Store and not rely on finalization. For example, on
546+
* Android finalization has a timeout that might be exceeded if closing needs to wait on transactions to finish.
526547
* <p>
527-
* Explicitly call {@link #close()} instead to avoid expensive finalization.
548+
* Also finalization is scheduled for removal in future versions of Java and may already be disabled depending on
549+
* JVM configuration (see documentation on super method).
528550
*/
529551
@SuppressWarnings("deprecation") // finalize()
530552
@Override

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,13 @@ static File getDbDir(@Nullable File baseDirectoryOrNull, @Nullable String nameOr
679679
*/
680680
public BoxStore buildDefault() {
681681
BoxStore store = build();
682-
BoxStore.setDefault(store);
682+
try {
683+
BoxStore.setDefault(store);
684+
} catch (IllegalStateException e) {
685+
// Clean up the new store if it can't be set as default
686+
store.close();
687+
throw e;
688+
}
683689
return store;
684690
}
685691

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

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,6 @@ protected Box<TestEntity> getTestEntityBox() {
162162

163163
@After
164164
public void tearDown() {
165-
// Collect dangling Cursors and TXs before store closes
166-
System.gc();
167-
System.runFinalization();
168-
169165
if (store != null) {
170166
try {
171167
store.close();

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ public void openSamePath_afterClose_works() {
174174
assertEquals(0, BoxStore.nativeGloballyActiveEntityTypes());
175175
}
176176

177+
@Test
178+
public void openSamePath_closedByFinalizer_works() {
179+
System.out.println("Removing reference to " + store);
180+
store = null;
181+
182+
// When another Store is still open using the same path, a checker thread is started that periodically triggers
183+
// garbage collection and finalization in the VM, which here should close store and allow store2 to be opened.
184+
// Note that user code should not rely on this, see notes on BoxStore.finalize().
185+
BoxStore store2 = createBoxStore();
186+
store2.close();
187+
System.out.println("Closed " + store2);
188+
}
189+
177190
@Test
178191
public void testOpenTwoBoxStoreTwoFiles() {
179192
File boxStoreDir2 = new File(boxStoreDir.getAbsolutePath() + "-2");

0 commit comments

Comments
 (0)