objectbox / objectbox/objectbox-dart
openStore() blocks forever without error when a new isolate re-opens the store after the previous isolate died (process kept alive, Store.isOpen() returns false)
Nobody has claimed this yet.
- Dominant language
- Dart
- Stars
- 1.2k
- Forks
- 162
- Avg merge
- 15m
- Merged PRs (30d)
- 1
Description
Is there an existing issue?
- I have searched existing issues
Build info
- objectbox version: 5.3.2 (latest stable, with objectbox_flutter_libs 5.3.2)
- Flutter/Dart version: Flutter 3.41.9 stable
- Build OS: macOS 15
- Deployment OS or device: Android 16, physical device, release build
Summary
openStore() blocks forever, with no error, when main() is re-run by a new FlutterEngine/isolate in a process whose previous main isolate died without explicitly closing the Store. At that point Store.isOpen(dbPath) already returns false, so the Store.attach() workaround from #436 does not help — the code correctly takes the openStore() path and then never returns. The UI thread stays blocked until the process is ANR-killed by Android; the freshly restarted process then opens the very same directory in ~7 ms.
This is different from the well-known 10001 Cannot open store: another store is still open using the same path cases (#436, #387, #699): there the old store is still registered and open fails fast with an error. Here the registry already says "not open", yet the underlying env/lock apparently hasn't been released, and obx_store_open seems to wait for it indefinitely.
Steps to reproduce
Hard to reproduce in a minimal project on an emulator (emulators kill the process together with the Activity), but the conditions are well understood. Our app is a wearable companion app:
- Android app whose process is kept alive by a foreground service (BLE device companion). The process in our logs had been alive for 1.5+ days.
main()opens the Store with the guard recommended in #436:
Store.isOpen(path) ? Store.attach(model, path) : await openStore(directory: path).- The Store is never explicitly closed; closing relies on the
NativeFinalizer(store_close)attached by the library. The app has several activequery.watch()subscriptions (see also #448 — watch never closes the underlying query). - Put the app in the background and let Android reclaim the Activity: the Flutter engine and the main isolate are destroyed, the process stays alive (foreground service).
- Tap the app icon: the same process creates a new FlutterEngine and re-runs
main(). Store.isOpen(dbPath)returnsfalse→openStore()is called → it never returns.
Expected behavior
Either the open succeeds (possibly after briefly waiting for the closing store), or it fails fast with an error (like the 10001 error in the registered-store case). Anything but an indefinite, silent block of the Dart/UI thread.
Actual behavior
openStore() blocks forever. No exception, no log output from the database library. Native (non-Dart) threads of the process keep running normally, which rules out the process being frozen — only the Dart thread inside the synchronous obx_store_open FFI call is stuck. After ~38 s Android kills the process (ANR); the automatically restarted process opens the same directory in 7 ms, which shows the stale lock/env lived (and died) with the old process.
Code
Code
class DBManager {
Store? _store;
Future<void> init({String? directory}) async {
if (_store != null) return;
final docsDir = await getApplicationDocumentsDirectory();
final dbPath = p.join(docsDir.path, 'my-db');
if (Store.isOpen(dbPath)) {
// Workaround from #436 — not taken in this case, isOpen() returns false
_store = Store.attach(getObjectBoxModel(), dbPath);
} else {
// Blocks forever here
_store = await openStore(directory: dbPath, maxDBSizeInKB: 262144);
}
}
}
Logs, stack traces
Timestamps from our production (release build) file log. Instrumentation lines around the store open are custom [Startup] markers.
Logs
# Old process (pid 32078), alive for >1.5 days, kept alive by a foreground service.
# Its Flutter engine + main isolate had been destroyed earlier while backgrounded
# (Dart-side log lines stopped ~25 min before this; native threads still logging).
# User taps the app icon -> same process, new FlutterEngine, main() re-runs:
11:45:42.914 [Startup] LogUtils initialized, debug=false
11:45:42.915 [Startup] SPManager initialized
11:45:42.915 [Startup] DBManager.init() begin
11:45:42.917 [Startup] DB path resolved <- getApplicationDocumentsDirectory() returned
11:45:42.917 [Startup] DB store opening <- Store.isOpen(dbPath) == false, calling openStore()
# ... openStore() never returns. For the next 38 seconds the process keeps logging
# from native BLE threads, but nothing more from Dart. No exception anywhere.
11:46:20.8 <process killed by the system (ANR)>
# Android immediately restarts the process (pid 20435):
11:46:21.752 [Startup] DBManager.init() begin
11:46:21.752 [Startup] DB path resolved
11:46:21.752 [Startup] DB store opening <- same branch, fresh process
11:46:21.756 [Startup] DBManager initialized (+7ms, total=21ms)
Analysis / questions
Our best reading of the sequence:
- When the old main isolate died, the
NativeFinalizer(store_close)ranobx_store_close. The store had livequery.watch()streams / queries whose own native resources are also only cleaned up by finalizers, with no ordering guarantee relative to the store finalizer, andobx_store_closedocuments that it waits for transactions to finish. - That close apparently got far enough to remove the path from the "open stores" registry (
Store.isOpen()→ false) but not far enough to release the underlying env/lock. - The new isolate's
obx_store_openthen waits on that never-to-be-released env — a deadlock that only process death resolves.
objectbox-java#625 (2018) describes the same skeleton on Android/Java — Activity destroyed, process kept alive by the OEM, re-creating the BoxStore gets stuck — and was closed as not reproducible on emulators. A process kept alive by a foreground service is exactly the precondition emulators don't reproduce; with Android 16's aggressive background Activity reclamation this combination is getting more common, not less.
Questions:
- Does
obx_store_openwait (indefinitely?) for a concurrently-closing env of the same path? Could it fail with an error or a bounded timeout instead? - Is there a recommended way to make the isolate-shutdown close robust when watched queries are still alive?
Our app-side mitigation is to explicitly cancel watchers and Store.close() on AppLifecycleListener.onDetach, but detached is not guaranteed to run, so this narrows the window without closing it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing the Store.isOpen()/Store.attach()/openStore() path into obx_store_open and the NativeFinalizer(store_close) lifecycle. Check how query.watch() resources interact with store shutdown, including AppLifecycleListener.onDetach, and reproduce the isolate restart sequence if possible. Done means the same-path reopen cannot block indefinitely and either opens or reports an error.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, dart, flutter
- Domain
- databases, mobile
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Needs clarification
- Newbie friendliness
- 38/100