@@ -233,7 +233,7 @@ public static boolean isSyncServerAvailable() {
233
233
private final File directory ;
234
234
private final String canonicalPath ;
235
235
/** Reference to the native store. Should probably get through {@link #getNativeStore()} instead. */
236
- private long handle ;
236
+ volatile private long handle ;
237
237
private final Map <Class <?>, String > dbNameByClass = new HashMap <>();
238
238
private final Map <Class <?>, Integer > entityTypeIdByClass = new HashMap <>();
239
239
private final Map <Class <?>, EntityInfo <?>> propertiesByClass = new HashMap <>();
@@ -636,13 +636,14 @@ public boolean isReadOnly() {
636
636
}
637
637
638
638
/**
639
- * Closes the BoxStore and frees associated resources.
640
- * This method is useful for unit tests;
641
- * most real applications should open a BoxStore once and keep it open until the app dies.
639
+ * Closes this BoxStore and releases associated resources.
642
640
* <p>
643
- * WARNING:
644
- * This is a somewhat delicate thing to do if you have threads running that may potentially still use the BoxStore.
645
- * This results in undefined behavior, including the possibility of crashing.
641
+ * Before calling, <b>all database operations must have finished</b> (there are no more active transactions).
642
+ * <p>
643
+ * If that is not the case, the method will briefly wait on any active transactions, but then will forcefully close
644
+ * them to avoid crashes and print warning messages ("Transactions are still active"). If this occurs,
645
+ * analyze your code to make sure all database operations, notably in other threads or data observers,
646
+ * are properly finished.
646
647
*/
647
648
public void close () {
648
649
boolean oldClosedState ;
@@ -658,19 +659,42 @@ public void close() {
658
659
}
659
660
660
661
// Closeable recommendation: mark as closed before any code that might throw.
662
+ // Also, before checking on transactions to avoid any new transactions from getting created
663
+ // (due to all Java APIs doing closed checks).
661
664
closed = true ;
665
+
662
666
List <Transaction > transactionsToClose ;
663
667
synchronized (transactions ) {
668
+ // Give open transactions some time to close (BoxStore.unregisterTransaction() calls notify),
669
+ // 1000 ms should be long enough for most small operations and short enough to avoid ANRs on Android.
670
+ if (hasActiveTransaction ()) {
671
+ System .out .println ("Briefly waiting for active transactions before closing the Store..." );
672
+ try {
673
+ // It is fine to hold a lock on BoxStore.this as well as BoxStore.unregisterTransaction()
674
+ // only synchronizes on "transactions".
675
+ //noinspection WaitWhileHoldingTwoLocks
676
+ transactions .wait (1000 );
677
+ } catch (InterruptedException e ) {
678
+ // If interrupted, continue with releasing native resources
679
+ }
680
+ if (hasActiveTransaction ()) {
681
+ System .err .println ("Transactions are still active:"
682
+ + " ensure that all database operations are finished before closing the Store!" );
683
+ }
684
+ }
664
685
transactionsToClose = new ArrayList <>(this .transactions );
665
686
}
687
+ // Close all transactions, including recycled (not active) ones stored in Box threadLocalReader.
688
+ // It is expected that this prints a warning if a transaction is not owned by the current thread.
666
689
for (Transaction t : transactionsToClose ) {
667
690
t .close ();
668
691
}
669
- if (handle != 0 ) { // failed before native handle was created?
670
- nativeDelete (handle );
671
- // The Java API has open checks, but just in case re-set the handle so any native methods will
672
- // not crash due to an invalid pointer.
673
- handle = 0 ;
692
+
693
+ long handleToDelete = handle ;
694
+ // Make isNativeStoreClosed() return true before actually closing to avoid Transaction.close() crash
695
+ handle = 0 ;
696
+ if (handleToDelete != 0 ) { // failed before native handle was created?
697
+ nativeDelete (handleToDelete );
674
698
}
675
699
676
700
// When running the full unit test suite, we had 100+ threads before, hope this helps:
@@ -814,9 +838,27 @@ public void removeAllObjects() {
814
838
public void unregisterTransaction (Transaction transaction ) {
815
839
synchronized (transactions ) {
816
840
transactions .remove (transaction );
841
+ // For close(): notify if there are no more open transactions
842
+ if (!hasActiveTransaction ()) {
843
+ transactions .notifyAll ();
844
+ }
817
845
}
818
846
}
819
847
848
+ /**
849
+ * Returns if {@link #transactions} has a single transaction that {@link Transaction#isActive() isActive()}.
850
+ * <p>
851
+ * Callers must synchronize on {@link #transactions}.
852
+ */
853
+ private boolean hasActiveTransaction () {
854
+ for (Transaction tx : transactions ) {
855
+ if (tx .isActive ()) {
856
+ return true ;
857
+ }
858
+ }
859
+ return false ;
860
+ }
861
+
820
862
void txCommitted (Transaction tx , @ Nullable int [] entityTypeIdsAffected ) {
821
863
// Only one write TX at a time, but there is a chance two writers race after commit: thus synchronize
822
864
synchronized (txCommitCountLock ) {
@@ -1290,6 +1332,18 @@ public long getNativeStore() {
1290
1332
return handle ;
1291
1333
}
1292
1334
1335
+ /**
1336
+ * For internal use only. This API might change or be removed with a future release.
1337
+ * <p>
1338
+ * Returns if the native Store was closed.
1339
+ * <p>
1340
+ * This is {@code true} shortly after {@link #close()} was called and {@link #isClosed()} returns {@code true}.
1341
+ */
1342
+ @ Internal
1343
+ public boolean isNativeStoreClosed () {
1344
+ return handle == 0 ;
1345
+ }
1346
+
1293
1347
/**
1294
1348
* Returns the {@link SyncClient} associated with this store. To create one see {@link io.objectbox.sync.Sync Sync}.
1295
1349
*/
0 commit comments