【ITニュース解説】Offline-First Sync Failures That Flight Mode Won't Show You
2026年10月26日に「Dev.to」が公開したITニュース「Offline-First Sync Failures That Flight Mode Won't Show You」について初心者にもわかりやすく解説しています。
ITニュース概要
オフラインファーストアプリのデータ同期は、機内モードだけでは見つからない複雑なバグを生む。デバイスの時計ずれ、削除と編集の競合、不安定な接続による重複などがデータ損失を招く。これを防ぐには、サーバー時刻の利用や衝突解決、徹底的なテストなどの適切な設計が不可欠で、ユーザーに信頼されるアプリを築く鍵となる。
ITニュース解説
オフラインファーストのモバイルアプリケーションは、ネットワーク接続がない状態や不安定な状況でもユーザーが継続して作業できるという大きな利点を持つ。しかし、このようなアプリケーションの同期機能には、一見単純に見える「ネットワークが繋がっているか、いないか」というテストだけでは見つけられない、複雑で深刻な問題が潜んでいる。この記事では、ジャーナルアプリ「Mindleau」を開発する過程で明らかになった、オフラインファースト同期における落とし穴と、それらを解決するための具体的な方法について解説する。
「Mindleau」は、ユーザーが入力した日記などの情報をまずスマートフォンのローカルデータベースに保存し、その後バックグラウンドでクラウドサービスであるFirestoreに同期するという「ローカルファースト」な設計を採用している。この同期の基本的な考え方には、「最終書き込み優先(Last-Write-Wins: LWW)」というシンプルなルールが使われることが多い。これは、同じデータに対して複数の変更があった場合、最も新しい更新日時を持つ変更を正しいものとして採用するというものだ。各データには更新日時、同期状態、そして削除されたことを示す削除日時などの情報が含まれている。同期処理は、まずクラウドから最新のデータを取得し(プル)、ローカルのデータと比較してより新しい方を反映させる。その後、ローカルでまだクラウドに送られていない変更をクラウドに送信する(プッシュ)。このLWW方式は実装が簡単で多くのケースでうまく機能するが、特定の状況下ではユーザーのデータが失われたり、期待しない動作をしたりする原因となる。
具体的な問題の一つは、「デバイスの時計のずれ」だ。LWW方式は異なるデバイスの更新日時を比較して新しい方を優先するため、もし一方のスマートフォンの時計が数時間進んでいたら、そのデバイスで行われた編集は、実際の変更日時が古くても他のデバイスの変更よりも「新しい」と判断されてしまう。これにより、本来残すべき変更が上書きされて消える可能性がある。例えば、時計が進んでいるスマートフォンAで編集を行い、その後に正しい時計のスマートフォンBで同じデータをオフラインで編集したとする。Bがオンラインになり同期を始めると、Aの「新しい」更新日時を見てしまい、B自身の未同期の編集が失われる。この問題への対策として、デバイスの時計に頼らず、クラウド側で書き込みが行われた正確な時刻を記録する「サーバータイムスタンプ」を利用することが有効だ。さらに、ローカルでまだクラウドに送られていない変更がある場合は、無条件にクラウドのデータで上書きするのではなく、何らかの「競合」として扱う必要がある。
次に、「削除と編集の衝突」という問題がある。あるデバイスでデータを削除し、別のデバイスで同じデータを編集した場合、どちらの変更が優先されるべきかというものだ。LWW方式では、削除と編集のどちらが新しい更新日時を持つかによって結果が変わってしまう。例えば、スマートフォンAがオフラインでデータを削除し、スマートフォンBがオンラインで同じデータを編集して同期した場合、Aがオンラインに戻って同期すると、Bの編集の方が更新日時が新しければ、削除されたはずのデータが再び表示されてしまう。ユーザーが削除したはずのデータが元に戻るというのは、非常に困惑する体験だ。この場合、単に更新日時で判断するのではなく、削除と編集が衝突した場合はこれを「競合」として扱い、ユーザーがどちらを残すか選択できるような仕組みを提供することが望ましい。ユーザーにどちらかを選択させる方が、意図せずデータが失われたり、削除したデータが戻ってきたりするよりも良い解決策となる。
このようなデータ変更の「競合」が発生した場合、解決策はいくつか考えられる。LWWのようにどちらか一方を自動的に選択して片方の変更を「黙って」消す方法。あるいはCRDTsのような複雑な技術で自動的に両方の変更をマージする方法。そして、両方の変更を保持し、ユーザー自身に判断を委ねる方法だ。個人のジャーナルアプリのような場合、競合は稀であり、ユーザー自身が唯一の著者であるため、両方を保持してユーザーに選択させるのが最適な中間点となる。つまり、ローカルでまだクラウドに送られていない変更がある状態でクラウドデータが更新されていたら、ローカルの変更を新しい「競合コピー」として保存し、クラウドの変更を受け入れる。そして、ユーザーには競合コピーが明確に表示され、それらを保持、マージ、または削除する選択肢が与えられるべきだ。
また、「不安定なネットワーク接続」はデータの重複という問題を引き起こす可能性がある。ネットワークが完全に切断されるフライトモードの状態だけでなく、電波が一時的に途切れたり、サーバーからの応答が途中で失われたりするような不安定な接続は現実世界では頻繁に発生する。例えば、新しいデータをサーバーにプッシュする際、リクエストはサーバーに届いたが、サーバーからの成功応答がクライアントに戻る前にネットワーク接続がタイムアウトするケースがある。この時、クライアントはサーバーへの書き込みが成功したかを知ることができないため、再度同じデータをプッシュしようとする。しかし、もしサーバーにはすでにデータが書き込まれていた場合、二度目のプッシュは新しいデータとして処理されてしまい、結果として同じデータがユーザーの別のデバイスで二重に表示されることになる。この問題を防ぐためには、ネットワーク操作が「べき等(idempotent)」である必要がある。つまり、同じ操作を何度繰り返しても常に同じ結果になるように設計することだ。具体的には、サーバーにデータを送る前に、ローカルで一意なID(リモートID)を生成してデータに付与し、そのIDを使ってサーバーに書き込みを行う。こうすることで、再試行時にはすでに存在するIDのデータを更新する形となり、重複作成を防ぐことができる。
「二つの同期が同時に実行される」ことも、同様にデータの重複を引き起こす。アプリケーションは、データを保存した後や、ネットワーク接続が回復したときに同期を開始する。もしネットワークが頻繁に切断と再接続を繰り返すような環境では、複数の同期処理がほぼ同時に開始されてしまうことがある。それぞれの同期処理が同じ未同期のデータを読み込み、それぞれで新しいリモートIDを生成してプッシュしようとすると、結果的に同じデータが二重にサーバーに書き込まれてしまう。これを避けるためには、一度に一つの同期処理しか実行されないように「排他制御」を行う必要がある。つまり、同期処理がすでに進行中であれば、新しい同期リクエストは待機するか、進行中の同期が完了した後に再度実行されるようにするべきだ。
さらに、「古いデータベースからのアップグレード」という問題もある。新しいアプリをインストールするユーザーはクリーンな状態で始まるため、この問題は発生しない。しかし、すでにアプリを長期間利用しているユーザーが、新しいバージョンのアプリにアップデートする際には、古いバージョンのデータベースが新しいスキーマに対応する必要がある。Mindleauアプリの例では、初期のバージョンから新しい同期用のカラム(updatedAtなど)を追加する際に、これらのカラムにデフォルト値として「1970年1月1日」という極めて古い日付が設定されていた。この状態でLWW同期を行うと、新しく追加されたカラムの値が古すぎるため、クラウドにあるどんなデータでも、たとえそれが古いバージョンであっても、ローカルのデータを上書きしてしまうことになる。これでは、ユーザーがこれまで蓄積してきた大切なデータが失われる可能性がある。この問題への対策としては、データベースの移行時に、既存のデータに対して正しいupdatedAt(例えば、データの作成日時を使用する)を後から埋める処理を行う必要がある。
これらの問題は、単に「アプリが動くか」というレベルのテストでは発見できない。ユーザーがオフラインファーストアプリを信頼し、大切な個人情報を安心して預けられるようにするためには、「このスマートフォンに保存済み」と「バックアップ済み」の違いを明確にすることも重要だ。ローカルに保存されているだけでは、スマートフォンを紛失した場合にデータは失われる。クラウドにバックアップされて初めて、他のデバイスで復元できる状態となる。さらに、匿名アカウントでバックアップされているだけでは、アカウント連携なしには復元できない場合もある。アプリケーションは、これらの状態をユーザーに明確に提示し、どのレベルでデータが保護されているのかを分かりやすく伝える必要がある。
オフラインファーストの同期機能をテストする際には、飛行機モードでのテストは出発点に過ぎない。前述のような、デバイスの時計ずれ、データの削除と編集の衝突、不安定なネットワーク、複数の同期処理、古いバージョンのデータベースからのアップグレードなど、より複雑で現実的なシナリオを想定した包括的なテストが不可欠だ。これらのテストを徹底することで、ユーザーが安心して利用できる、真に信頼性の高いオフラインファーストアプリを構築できる。