Skip to content

Commit 4207508

Browse files
BoxStore: do not block file open check from obtaining lock on creation #240
This prevents an "Another BoxStore is still open for this directory" exception when a previous instance using the same directory has to be finalized (because it wasn't closed explicitly).
1 parent e956bfe commit 4207508

File tree

3 files changed

+34
-16
lines changed

3 files changed

+34
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ 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+
913
## 4.3.1 - 2025-08-12
1014

1115
- 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

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package io.objectbox;
1818

19-
import org.junit.Ignore;
2019
import org.junit.Test;
2120
import org.junit.function.ThrowingRunnable;
2221

@@ -175,24 +174,17 @@ public void openSamePath_afterClose_works() {
175174
assertEquals(0, BoxStore.nativeGloballyActiveEntityTypes());
176175
}
177176

178-
@Ignore("Fails due to related issue")
179177
@Test
180178
public void openSamePath_closedByFinalizer_works() {
181179
System.out.println("Removing reference to " + store);
182180
store = null;
183181

184-
// TODO Can't, at least on a JVM 21, confirm that the gc and runFinalization calls are blocking close() from
185-
// accessing openFiles.
186-
// Instead, found another issue where openFiles appears to still contain the db directory on this thread but
187-
// not for the checker thread.
188-
189182
// When another Store is still open using the same path, a checker thread is started that periodically triggers
190183
// garbage collection and finalization in the VM, which here should close store and allow store2 to be opened.
191-
// Note that user code should not rely on this (for ex. finalizers on Android have a timeout, which might be
192-
// exceeded if closing the store takes long, because it needs to wait on transactions to finish), the store
193-
// should be explicitly closed instead.
184+
// Note that user code should not rely on this, see notes on BoxStore.finalize().
194185
BoxStore store2 = createBoxStore();
195186
store2.close();
187+
System.out.println("Closed " + store2);
196188
}
197189

198190
@Test

0 commit comments

Comments
 (0)