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)

Open
#834 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Dart
Stars
1.2k
Forks
162
Avg merge
15m
Merged PRs (30d)
1

Description

Is there an existing issue?
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:

  1. 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.
  2. main() opens the Store with the guard recommended in #436:
    Store.isOpen(path) ? Store.attach(model, path) : await openStore(directory: path).
  3. The Store is never explicitly closed; closing relies on the NativeFinalizer(store_close) attached by the library. The app has several active query.watch() subscriptions (see also #448 — watch never closes the underlying query).
  4. 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).
  5. Tap the app icon: the same process creates a new FlutterEngine and re-runs main().
  6. Store.isOpen(dbPath) returns falseopenStore() 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:

  1. When the old main isolate died, the NativeFinalizer(store_close) ran obx_store_close. The store had live query.watch() streams / queries whose own native resources are also only cleaned up by finalizers, with no ordering guarantee relative to the store finalizer, and obx_store_close documents that it waits for transactions to finish.
  2. 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.
  3. The new isolate's obx_store_open then 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_open wait (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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.