開発Tips

Flutter化でハマった不具合と解消④: isolate をまたぐ ObjectBox store の競合

ログ簿はバックグラウンドで通知やデータ更新を行うため workmanager を使っています。移行後、まれに再現するクラッシュと状態の食い違いを追った結果、原因は isolate をまたいだ ObjectBox ストアの競合でした。

背景: バックグラウンドは「別 isolate」

Flutter の workmanagercallbackDispatcher は、UI とは 完全に別の isolate で実行されます。Dart の isolate はメモリ空間が分離されているため、

  • UI 側で保持しているシングルトン
  • Riverpod のプロバイダの状態
  • 開いてある ObjectBox の Store インスタンス

を、バックグラウンド isolate からそのまま共有することは できません

ここで素朴に「バックグラウンド側でも openStore() すればいい」とやると、同じデータベースファイルに対して、UI 側とバックグラウンド側の2つの Store が並行して触る状態が生まれます。ObjectBox は同一プロセス内で同じデータベースを複数回開くことを想定しておらず、競合・状態の不整合・クラッシュにつながります。

UI isolate ─────────► openStore()  ┐
                                    ├─ 同じ .mdb を別Storeが同時に触る → 競合
workmanager isolate ─► openStore()  ┘   (これが事故のもと)

対処: ストアの「開く主体と回数」を一元化する

解決の方針は、「誰が・いつ・何回ストアを開くか」を設計で一本化することでした。

  • ストア生成を一箇所に集約し、同一プロセス内で二重に openStore() しないことを保証する。
  • バックグラウンド isolate が必要とする情報は、共有可能な形(ファイルパスやハンドル経由)で受け渡す。各 isolate が思い思いに新しいストアを開かないようにする。
  • 「この処理はどの isolate で走り、そこでストアをどう扱うか」を暗黙にせず、明示的に決める。

ログ簿では、この一元化(ストアの open を単一の窓口に集約し、isolate 間で安全に共有する同期)を入れることで、競合を解消できました。

つまずきやすい点

  • ネイティブ時代の感覚で移植する。 「DB はシングルトンで1個持てばいい」はネイティブ(単一プロセス・共有メモリ)では成り立ちますが、Flutter の isolate 境界ではメモリが共有されないため気づきにくい不具合につながります。
  • バックグラウンドは低頻度でしか走らない。 だからこそ競合は「まれに再現」になり、原因特定が遅れます。再現条件(バックグラウンド起動のタイミング)を意識してログを仕込むのが有効でした。

まとめ

Flutter のバックグラウンド処理は「別 isolate=状態は共有されない」が大前提です。ObjectBox のような共有リソースは、開く主体と回数を設計段階で一本化し、isolate をまたぐ暗黙の並行 open を無くす。ネイティブからの移植ほど、この isolate 境界の前提を最初に押さえておくべきだと実感しました。