Skip to content

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) #834

Open

Description

@weinierfei

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 [Flutter] openStore() is not working across multiple FlutterEngines #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 [Flutter] query.watch() never seems to close the underlying query #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 false → 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:

  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.

Activity

  1. greenrobot-team commented on Aug 31, 2026

    @greenrobot-team
    Member

    Thanks for this detailed report! I shared this with the development team.

  2. ericccg commented on Aug 31, 2026

    @ericccg

    Experience the same issue with app with BLE and foreground service on multiple Android 16 Samsung devices.
    For the Android 15 and lower devices, haven't experience it.

  3. greenrobot commented on Aug 31, 2026

    @greenrobot
    Member

    Thanks for the detailed report and the careful analysis — this is a well-described scenario. We looked into it on the wrapper side and tried to reproduce the mechanism you describe. Here is what we found.

    What the Dart wrapper does when an isolate dies without closing the Store

    • Store attaches a NativeFinalizer for obx_store_close. Dart guarantees it runs at isolate-group shutdown, and destroying a FlutterEngine is exactly that. Note that it calls the raw obx_store_close only — none of the Dart-side cleanup in
      Store.close() runs (boxes are not closed, onClose listeners don't fire, observers are not closed).
    • Query (and PropertyQuery, Admin) also have native finalizers (obx_query_close etc.). The order relative to the store finalizer is unspecified.
    • Observers created by store.watch<T>(), store.entityChanges and therefore query.watch() have no finalizer. If the isolate dies with live subscriptions, obx_observer_close is never called. You are right about that part (it's related to [Flutter] query.watch() never seems to close the underlying query #448).
    • Store.isOpen() is a direct call to obx_store_is_open(path) in the native library; there is no Dart-side registry involved across engines.

    What we tested

    A Dart isolate opens a Store, sets up some state, never closes the Store, and is then either killed (Isolate.kill, which is what an engine destroy amounts to) or exits via Isolate.exit(). The main isolate then checks Store.isOpen() and opens the
    same directory.

    State at isolate death isOpen() afterwards Reopen
    nothing false 2–3 ms
    store.watch<T>() / entityChanges subscriptions false 3–6 ms
    3× query.watch(triggerImmediately: true) subscriptions false 3–4 ms
    5 unclosed Query objects false 3–4 ms
    50 unawaited putAsync() + 50 putQueued() false 3–5 ms
    all of the above combined false 5 ms
    a runAsync() worker isolate still holding an attached store true fails fast with error 10001 (the known #436 case)

    Every variant reopened immediately, in both kill and exit modes, and no ObjectBox threads were left behind (verified with a debugger). So with the current library we could not produce a state where isOpen() is false and obx_store_open blocks —
    regardless of live watchers, unclosed queries or pending async writes. (This was on Linux x64; the native core is the same on Android, but we can't rule out a platform-specific difference yet.)

    Notes on the proposed sequence

    1. "The NativeFinalizer close got stuck waiting for transactions/queries" — this doesn't fit your own log. FlutterEngine.destroy() runs on the Android main thread and blocks until the UI isolate has shut down; the native finalizers run
      synchronously as part of that shutdown. If obx_store_close had hung there, Activity.onDestroy() would never have returned and main() could not have been re-run in that process 25 minutes later. So the earlier close either completed, or the
      finalizer never ran at all.
    2. "obx_store_open waits on a not-yet-released env" — we could not find any state that produces this. Whether the core could block on a same-path store in a closing state is something we'll check with the core team, but we need a concrete trigger
      first.
    3. objectbox-java#625 is not the same mechanism: that hang was in the Java wrapper's System.runFinalization() call inside verifyNotAlreadyOpen(), not in the native open.

    What we'd need from you

    The log shows openStore() being called and nothing after it, but it doesn't show where the thread actually is. The following would help a lot:

    1. The ANR trace (/data/anr/… from a bugreport, or the ANR stack from Play Console). The native frames of the blocked thread answer the question directly — whether it's in obx_store_open, in LMDB locking, or somewhere else entirely (e.g.
      engine/plugin setup on the main thread).
    2. Versions: objectbox, objectbox_flutter_libs, Flutter SDK. Also whether the app runs with merged UI/platform threads (default on recent Flutter versions) — a blocked Dart FFI call only produces an ANR if Dart runs on the main thread.
    3. Does the foreground service run in the same process (no android:process attribute), and does it run any Dart code or touch ObjectBox itself (e.g. via a background engine / background service plugin)? A second process or engine touching the same
      directory would give exactly isOpen() == false plus a blocked open, since isOpen() only knows about the current process.
    4. A run with Store.debugLogs = true and debugFlags: DebugFlags.logTransactionsWrite | DebugFlags.logTransactionsRead passed to openStore(). The native library then logs to logcat (tag Box) whether the open was entered and how far the earlier
      close got.

    Mitigations for now

    • Explicitly cancelling watchers and calling Store.close() on AppLifecycleListener.onDetach is the right thing to do, as you already did. Also make sure the Store instance stays strongly referenced for the lifetime of the isolate, so the finalizer
      doesn't race with garbage collection.
    • Keep the Store.isOpen(path) ? Store.attach(...) : openStore(...) guard; it covers the case where an engine/isolate is still alive.

    On our side, independent of this issue, we noted two wrapper-level gaps that are worth improving: observers should be closed when the store is finalized (they currently leak native observers on isolate death), and the finalizer path should run the same
    cleanup as an explicit Store.close() (e.g. putQueued() async handles are currently not closed by the finalizer). Neither showed any effect on reopening in our tests, but they should be tightened.

  4. weinierfei commented on Sep 1, 2026

    @weinierfei
    Author

    Thanks for the thorough analysis and for testing all those scenarios — much appreciated. Answers to your questions, plus what we've set up to capture the next occurrence.

    2. Versions / threading

    • objectbox: 5.3.2, objectbox_flutter_libs: 5.3.2 (both latest stable)
    • Flutter 3.41.9 stable, default threading configuration — so merged UI/platform threads, which matches the blocked FFI call surfacing as an ANR-style kill of the process.
    • Device: Android 16, Chinese vendor ROM. Note @ericccg's comment above — multiple Samsung devices on Android 16, none on ≤ 15. Ours is also Android 16, so an OS-16-specific angle looks plausible.

    3. Process / foreground-service topology

    • The BLE foreground service runs in the default process (no android:process), is pure Kotlin (LifecycleService), creates no FlutterEngine and never touches ObjectBox files.
    • The only component in a second process (merged manifest) is the Baidu location SDK service (:remote) — pure native, no access to our documents directory.
    • However, the app uses workmanager, so there is a second FlutterEngine in the same process for periodic background tasks (cloud sync / weather). Its Dart entrypoint runs the same DB init: Store.isOpen(path) ? Store.attach(...) : openStore(...). The three days of collected app logs around the incident show no background-task activity, but the background engine logs to a separate file that wasn't collected, so we can't fully rule out background-engine activity near the incident. (If a background engine had been holding an attached store at that moment, isOpen() should have returned true, which contradicts the observed false — mentioned for completeness, as it is the only other thing in this process that ever opens this store.)
    • The Store instance is held by a singleton for the whole isolate lifetime — no GC/finalizer race on our side.

    1. + 4. ANR trace & debug logs

    Your point about engine destroy blocking on the Android main thread is fair: since main() re-ran in the same process ~2.5–25 minutes after the old isolate died, the finalizer close either completed or never ran — it cannot have been stuck inside engine destroy. So the "close stuck in the finalizer" part of our hypothesis is wrong. What remains observed is only: Store.isOpen() == false, openStore() never returning, and native (non-Dart) threads of the same process running normally throughout the 38-second hang, after which the process was killed and a fresh process opened the same directory in 7 ms.

    We've added a diagnostics flag to the app (Store.debugLogs = true + debugFlags: DebugFlags.logTransactionsRead | DebugFlags.logTransactionsWrite on open). Test devices are running this build now; when the hang reproduces we'll capture:

    • adb bugreport — ANR traces with the native frames of the blocked thread
    • logcat filtered on tag Box

    and report back here.

  5. ericccg commented on Sep 1, 2026

    @ericccg

    From the stack trace,

    Bad state: failed to create store: Cannot open store: another store is still open using the same path: "/data/data/***/app_flutter/objectbox" (OBX_ERROR code 10001)

    It's possible that BLE & foreground service keeps the process alive past previous app session removal.
    Although app activity is destroyed and the isolate is torn down as well when kill the app, objectbox's store is owned by the process, not by the isolate. When relaunch the app, it reuses the same process, main() runs in a fresh isolate, and openStore() hits 10001 error.

    I have tried run close upon detach, it does not work on our end of devices, so I tinker around and maybe found a workaround? Instead of trying to close the store upon on the way out; attach to the exact leaked one on the way in. Some thing like this.

    Future<Store> openObStore() async {
      final directory = (await defaultStoreDirectory()).path;
      await loadObjectBoxLibraryAndroidCompat();
      return Store.isOpen(directory)
          ? Store.attach(getObjectBoxModel(), directory)
          : openStore(directory: directory);
    }
    
  6. greenrobot commented on Sep 1, 2026

    @greenrobot
    Member

    Thanks for the answers — the second FlutterEngine (workmanager) was the missing piece. Following that thread, we found a mechanism that reproduces the exact state of the original report (Store.isOpen() false while the native store is not released), and
    it comes with a very concrete thing to look for in your logs. But first, since the last comment describes a different state, let's separate the two.

    Two different states in this thread

    Store.isOpen(path) openStore() Reported by
    A true fails fast with 10001 another store is still open using the same path @ericccg (stack trace above)
    B false blocks forever, no error @weinierfei (original report)

    @ericccg: your case is A. The store is not "owned by the process" as such — the wrapper closes it via a native finalizer when the isolate that opened it shuts down — but the underlying store stays open as long as any handle to it exists, and those
    can outlive the engine: isolates spawned with Isolate.spawn are not terminated when the engine's root isolate shuts down (we verified this; they keep running as orphans with their attached store handle), and a second engine (workmanager, background
    service) may hold one too. One concrete way we found: a query.stream() whose consuming isolate dies before the stream completed leaves a worker isolate waiting forever, holding a store handle — we'll fix that the worker exits when its parent is gone.
    So Store.isOpen(path) ? Store.attach(...) : openStore(...) is the right pattern for state A (it has been the recommended one since #436), and attaching to the still-open store is safe. It would help to know which ObjectBox APIs your app uses
    (query.stream(), *Async() calls, putQueued) and whether there is a second engine, so we can pin down what keeps the handle alive on your devices.

    The original report is state B — the guard is already in place there, isOpen() returns false, and the open itself never returns. The rest of this comment is about state B.

    A state where isOpen() is false but the store is not released

    We can produce it on Linux with the current library:

    1. A worker isolate is inside a write transaction and gets terminated with Isolate.kill() (or anything else that ends an isolate through the VM's unwind path). The unwind skips catch/finally, so the wrapper never closes the transaction — it
      stays "active" in the native store.
    2. When the last store handle is closed (explicitly, or by the finalizer when an isolate shuts down), the native close waits for active transactions. This one never ends, so the closing thread waits forever. The database library logs this (tag Box
      in logcat, no debug flags needed):
      [WARN ] Cannot close store yet: TX #5 is still active in thread 2. Waiting...
      [ERROR] Cannot close store yet: TX #5 still active. Waiting...     <- repeated every 10 s
      
    3. While that close is waiting, the path is already deregistered: from another engine/isolate group, Store.isOpen(path) returns false.
    4. obx_store_open() from the other engine then goes ahead against a store that is still alive in the same process. On Linux x64 this open succeeds (LMDB re-initialises its lock region). Android uses different locking primitives (no robust mutexes),
      and whether this same step blocks there is exactly what we are checking with the core team now — the ANR trace would show it directly.

    If the close waits on a worker/pool thread (e.g. inside a worker isolate's finalizer), nothing in the app notices: the main thread is free, native threads keep running, and only the next openStore() is affected. That matches your log — and a freshly
    started process sees none of this, which matches the 7 ms reopen.

    For completeness: a destroyed FlutterEngine alone does not produce this. Orphaned workers (see above) keep their attached store handle, so isOpen() stays true until they finish — that is state A, not B. Something has to actually terminate an
    isolate mid-transaction.

    What to check on your side (@weinierfei)

    1. logcat, tag Box, for Cannot close store yet. If this is what hit you, the line was repeating every 10 s in the old process for the ~25 minutes before the tap. Any occurrence — including from the workmanager engine's runs — is what we need,
      with the timestamp of the first one.
    2. Does the app, or a plugin/package it uses, call Isolate.kill() (or Isolate.exit() from inside code that touches the store)? Think worker isolates for BLE parsing that get terminated on lifecycle changes, isolate-pool packages, etc. Does
      anything use Isolate.runOnPlatformThread (platform isolates are shut down together with the engine)?
    3. Which ObjectBox APIs does the background task use: putManyAsync/runInTransactionAsync/runAsync, putQueued, query.stream(), …?
    4. The ANR trace remains the most valuable piece: the native frames of the blocked thread tell us whether the open is blocked in env open, in a store's close-wait, or somewhere else.

    Until then: keep the isOpen ? attach : openStore guard, and avoid terminating isolates (Isolate.kill) while they may be inside ObjectBox calls — let them finish and Isolate.exit() after store.close().

  7. ericccg commented on Sep 2, 2026

    @ericccg

    @greenrobot Thanks for clearing things up.
    Our App with v5.0.4 & Flutter 3.35.7

    For API, we used none of the ones you mentioned above, only sync calls: put, putMany, getAll, removeMany, removeAll, box.query(...).build() with explicit query.close().

    As for second engine, we only have single FlutterEngine, no workmanager. The BLE foreground service we used is flutter_foreground_task in the default process with ForegroundTaskEventAction.nothing() — no Dart callback, never touches the store.

    Our own Isolate.spawn worker is the case you described. Several run at once (one per DB). Close command comes from repo/provider dispose method, which never runs on swipe-away. The process survives on the foreground service, so the workers stay parked with attached handles — isOpen() is true and openStore() gives me 10001. That also explains why close-on-detach didn't work for us: the handle keeping the store alive belongs to a different, already-orphaned isolate, so closing the root isolate's store can't help.

    On my earlier Android 16 comment — probably not an OS difference. On ≤15 our process gets killed on swipe-away so the orphans die with it; on 16 it survives and carry into the relaunch.

    Is there a recommended way for a spawned worker holding an attached Store to detect its parent isolate is gone?

  8. weinierfei commented on Sep 2, 2026

    @weinierfei
    Author

    Thanks — the "TX still active → close waits forever → path deregistered → isOpen() false" mechanism is the first thing that fits every observation we have (main thread free, native threads running, 7 ms reopen in a fresh process). Answers to your four points, after an audit of the app and its dependencies:

    1. logcat Box / Cannot close store yet

    Not available retroactively: the logs we collected are the app's own xlog files, native logcat was not captured. The diagnostics build is on test devices now. In addition, since the string is now known, we'll add a startup self-check to the app: before openStore(), run logcat -d -s Box in-process and copy any Cannot close store yet lines (with timestamps) into our app log — so devices without adb access (end users) can report it through the normal log upload.

    2. Isolate.kill() / Isolate.exit() / platform isolates

    • App code and our shared packages: no Isolate.kill, Isolate.exit, Isolate.spawn or Isolate.runOnPlatformThread. The only isolate we create ourselves is one Isolate.run() for image resizing (no ObjectBox access).
    • Dependencies containing Isolate.kill/exit: dio (its compute), image (isolate executor), media_kit — none of them touch ObjectBox.
    • ObjectBox worker isolates we use: query.findAsync() (your runAsync worker) from one UI page; query.watch() in ~19 DAOs. query.stream() appears in two DAO methods, but both have no callers (dead code), so no stream worker is ever spawned.

    3. Background task (workmanager engine) — ObjectBox APIs

    • Cloud sync: a periodic task every 1 hour plus a one-off task 1 minute after app start, network-constrained. It runs in workmanager's own FlutterEngine, does Store.isOpen(path) ? attach : openStore, then uses only synchronous APIs (box.put/putMany, query.find, store.runInTransaction). No *Async, no putQueued, no watch/stream. When the task returns, workmanager destroys that engine.
    • The weather task does not touch ObjectBox.

    So in this process an engine is destroyed roughly hourly (background engine) plus whenever the Activity is recycled (main engine), but we cannot find any code path that terminates an isolate mid-transaction ourselves.

    4. ANR trace

    Still waiting for a reproduction. One thing worth knowing for anyone else trying to capture it: in the original incident the process exit reason (ApplicationExitInfo) was USER_REQUESTED / [FORCE STOP] due to SwipeUpClean — the system never declared an ANR, because the stuck window was the launch screen with no input dispatched to it. So there is no trace in /data/anr afterwards; a bugreport has to be taken while the hang is in progress. We've instructed testers accordingly.

    A question back

    Given the above, the only in-process candidates for a mid-transaction termination we can identify are FlutterEngine teardowns with an ObjectBox worker (findAsync/runAsync) still in flight — main engine on Activity recycling, background engine after each run. You verified orphan behaviour on the standalone Dart VM; could you check the same on an actual FlutterEngine? If Flutter's engine teardown ends child isolates through the VM unwind path rather than leaving them as orphans, a findAsync worker inside its read transaction at that moment would produce exactly the state you described.
    @greenrobot

  9. greenrobot commented on Sep 2, 2026

    @greenrobot
    Member

    Thanks to both of you — the new details show that these are two different states, and they settle one of them completely and narrow the other.

    @ericccg — state A (isOpen() true, openStore() fails with 10001)

    Your reading is right, and it matches what we verified on our side: a FlutterEngine teardown shuts down only the engine's root isolate. Isolates spawned with Isolate.spawn are
    terminated neither by Flutter nor by the Dart VM — they keep running as orphans, together with their attached store handles. Your dispose() never runs on swipe-away, so the parked
    workers keep the store open. On Android 16 the process survives the swipe-away thanks to the foreground service, so the relaunch meets that state; on ≤ 15 the process died with the
    Activity. So no OS-level difference in ObjectBox, just process lifetime.

    Attaching in that situation (Store.isOpen(path) ? Store.attach(...) : openStore(...)) is safe: the store is healthy, it is just owned by isolates you can't reach anymore.

    To let a worker notice that its parent is gone, use an exit listener: pass the spawning isolate's control port to the worker, register a listener on it and close the attached store
    when the message arrives. An engine teardown is a normal isolate shutdown, so the listener fires:

    // Spawning isolate: hand over the DB path and its own control port
    await Isolate.spawn(workerMain, [dbPath, Isolate.current.controlPort]);
    
    // Worker
    void workerMain(List<Object> args) {
      final store = Store.attach(getObjectBoxModel(), args[0] as String);
      final parentExited = ReceivePort();
      Isolate(args[1] as SendPort).addOnExitListener(parentExited.sendPort);
      parentExited.listen((_) {
        store.close(); // releases this isolate's handle
        Isolate.exit();
      });
      // ... your work ...
    } 

    We verified this: the worker's handle is closed within milliseconds of the parent's exit and Store.isOpen() turns false (the message is handled by the worker's event loop, so a
    worker in the middle of a transaction finishes it first). We'll use the same mechanism for our own Query.stream() worker, which currently waits for its spawning isolate forever, and
    document it for Store.attach().

    @weinierfei — state B (isOpen() false, openStore() blocks)

    To your question back: no, engine teardown does not end child isolates through the VM's unwind path. Flutter shuts down the root isolate only (platform isolates aside), the VM merely
    waits for spawns that are still in progress, and @ericccg's workers surviving on a real device confirm it. A findAsync() worker in flight at teardown therefore becomes an orphan,
    finishes its read transaction, closes its handle and exits normally — it cannot leave a transaction behind.

    With your inventory that leaves no code path we know of that keeps a write transaction active without something terminating an isolate — and only a write transaction makes the close
    wait unboundedly (read transactions only affect a bounded wait). We also checked the two remaining candidates your list suggests: query.watch() observers of a dead isolate just
    post to a closed port (non-blocking, cannot stall a commit), and the order in which Dart runs the finalizers at isolate shutdown (store before queries) is harmless. So the mechanism
    still fits every observation you have, but we cannot name the trigger in your app. The data you are collecting will decide:

    • logcat, tag Box: Cannot close store yet: TX #… is still active in thread … — the startup self-check that copies these lines into your app log is a great idea.
    • a bugreport taken during the hang — thanks for the SwipeUpClean detail, good to know there is no ANR trace to collect afterwards.
    • with Store.debugLogs = true the wrapper prints Opening store (C lib …) … isOpen=… immediately before the native open call, so the diagnostics build will also show whether the
      thread is inside obx_store_open() at all, or stuck before it.

    What we changed meanwhile

    Core (next release): closing a store no longer waits forever for a transaction that only the closing thread itself could finish — it aborts that transaction with an error log instead
    — and a bounded close no longer ends in undefined behavior. That removes at least one way to arrive at "close waits forever, path already deregistered": a transaction leaked by a
    terminated isolate whose finalizer closes the store on the very same thread. Whether that is what hit your app, only the logs above can tell.

  10. added this to the 6.0.0 milestone on Oct 12, 2026
  11. greenrobot-team commented on Oct 12, 2026

    @greenrobot-team
    Member

    The latest prerelease 6.0.0-preview.4 has various fixes to no longer prevent an isolate from exiting and the store from closing, see the release notes for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions