CRDT(競合のない複製データ型)は、コラボレーティブ編集およびオフラインファーストモバイルアプリケーションのための支配的な解決策として登場し、OT(操作変革)をYjsやAutomergeなどのフレームワークで置き換えました。初期のテスト戦略は手動での飛行機モードの切替に依存しており、実際のモバイル展開の混沌としたネットワーク条件を再現することには失敗しました。この分野は、単純な機能テストから任意の操作のインタリーブに対する収束特性の数学的検証へと進化しました。
従来のACID準拠テストは即時整合性を前提としていますが、CRDTはレプリカが一時的に分岐する可能性のある強い最終的整合性しか保証しません。テストでは任意のネットワーク分割をシミュレートし、同時更新(例えば、同一のカーソル位置での同時テキスト挿入)がデータの損失なくマージされることを検証し、トンストーンのガベージコレクションが収束を維持することを確保する必要があります。標準のモック技術は、輸送層の直列化の特異性や因果追跡におけるクロックのずれの影響、またはTCPの輻輳動作を捉えることができないため失敗します。
Toxiproxyを使用してネットワーク分断を注入し、Property-based testing(fast-checkやHypothesisを通じて)を使用して任意の操作シーケンスを生成し、定期的にすべてのレプリカをスナップショットして状態の等価性を確認するConvergence Monitorを利用した多層フレームワークを設計します。このフレームワークは、制御された混乱の中(ランダム化されたレイテンシ、ドロップされたパケット)で操作を実行し、次に結合半格の数学的特性:結合性、関連性、マージ関数の冪等性を検証します。
const fc = require('fast-check'); const { setupPartitionedReplicas, healPartition } = require('./test-helpers'); test('ネットワーク混乱下でのCRDT収束', async () => { await fc.assert( fc.asyncProperty( fc.array(fc.tuple(fc.string(), fc.nat()), { minLength: 1, maxLength: 100 }), async (operations) => { const [replicaA, replicaB] = await setupPartitionedReplicas(); // Toxiproxyによって注入されたランダムなレイテンシで操作を適用する await Promise.all([ applyWithChaos(replicaA, operations.filter((_, i) => i % 2 === 0)), applyWithChaos(replicaB, operations.filter((_, i) => i % 2 === 1)) ]); await healPartition(); await waitForConvergence(5000); // 5秒のタイムアウト // 強い最終的整合性を検証する return JSON.stringify(replicaA.state) === JSON.stringify(replicaB.state); } ), { numRuns: 1000, timeout: 60000 } ); });
テレメディスンのスタートアップが、React Nativeを使用してフィールドドクター向けのモバイルアプリを開発し、Yjs CRDTを使用してタブレット間で患者のバイタルサインを同期しました。オフラインで同じ患者の血圧を編集している2人の医師は、一方の更新が再接続時に他方を静かに上書きすることが起こり、ライブラリが競合のない特性を主張しているにもかかわらず、問題が発生しました。この問題は、田舎のクリニックで断続的な接続状態が報告されるまで、3週間の間検出されませんでした。
チームは、Yjsドキュメントに対するカスタムラッパーが数値フィールドに対して不正にLWW(Last-Write-Wins)レジスターを実装しており、PN-Counter(Positive-Negative Counter)ではなく使用していたことを発見しました。標準の単体テストは、単一ユーザースcenarioを逐次的にテストするために合格しましたが、モックネットワークを使用した統合テストは、'遅延同期'ウィンドウを捉えずに直ちに同期されました。この競合状態は、両方の医師がミリ秒単位でオンラインに移行したときのみ発生し、クラウド同期層でタイムスタンプ衝突を引き起こしました。
医療研究者は物理的なタブレットで手動で飛行機モードを有効にし、患者レコードに競合する編集を行った後、同期を強制するために同時に飛行機モードを無効にしました。このアプローチは、制御されたラボ環境で複数の物理デバイスを調整する必要があり、デバイス間の再接続タイミングを同期させるために人間の反射に依存していました。
利点: この方法は、実際のハードウェアのラジオ動作、iOSのバックグラウンドアプリ更新の特異性、およびシミュレーターでは再現できないWebSocket再接続のタイミングにおけるバッテリー最適化効果を捉えることによって、最大のリアリズムを提供しました。
欠点: このアプローチは、人間の反応の遅延により再現不可能なタイミングの問題に悩まされ、2台のデバイスを超えてスケールするためには高価なデバイスファームが必要であり、ミリ秒単位のウィンドウ内で同時再接続する特定のエッジケースを系統的にテストすることはできませんでした。
開発者は、Jest単体テストを実装し、Sinonのフェイクタイマーを使用してCRDT操作間で時計を手動で進め、実際のネットワークの関与なしにオフライン期間をプログラム的にシミュレートしました。これらのテストは、モバイルデバイスの状態を表すインメモリデータ構造を使用して隔離されたNode.jsプロセスで実行されました。このアプローチは、実行環境を完全に制御し、開発中の即時フィードバックを提供しました。
利点: 実行はミリ秒単位で完了し、特定のマージシナリオのデバッグのための決定論的再現性を提供し、ネットワークインフラストラクチャやコンテナオーケストレーションを必要としませんでした。
欠点: テストはProtocol Buffers輸送層での直列化エラーを捕らえることができず、TCPの圧力と再試行の動作を無視し、実際のAndroidおよびiOSデバイスでのSQLiteとは大きく異なるモックストレージを使用しました。
チームは、Docker Composeクラスターを展開し、Toxiproxyを介してAndroidエミュレーターとNode.js同期サーバー間のマンインザミドルとして構成し、ランダムなレイテンシ、パケット損失、およびパーティションシナリオを注入しました。彼らは、さまざまなタイミング特性を持つ数千の任意の操作シーケンスを生成するためにfast-checkを利用し、カスタムヘルスモニターは、デバッグAPIを介してレプリカの状態をポーリングして収束違反を検出しました。このセットアップは、田舎のセルラーネットワークの混沌としたネットワーク条件を正確にモデル化し、シードされたランダム化によって完全な再現性を維持しました。
利点: これにより、ネットワークパーティションの正確な制御による再現可能なカオスエンジニアリングが可能となり、同時増加後の即時パーティション修復のようなエッジケースのプロパティベースの生成を許可し、TLSハンドシェイクのタイムアウトやMTUの断片化問題など、実際のネットワークスタックの動作を捉えました。
欠点: セットアップにはコンテナ化されたエミュレーターファームを維持するための相当なDevOpsの専門知識が必要で、テストの実行はDockerオーバーヘッドのため単体テストよりも遅く、失敗をデバッグするためにはToxiproxy、エミュレーター、および同期サーバー全体の分散ログを相関させる必要がありました。
チームは、一度の生産上のインシデントにより、解決策2のモックがYjsの更新メッセージがセルラーのMTUリミットを超える重大なバグを隠していることが判明したため、解決策3を選択しました。維持にはコストがかかりましたが、カオスエンジニアリングアプローチは、ベクトルクロックの比較を含む修正を検証するための必要な忠実性を提供し、収束特性において回帰が発生しないことを保証しました。
このフレームワークは、同一のシステムタイムスタンプを持つ同時更新がLWWレジスターによって有効な医療データを廃棄する原因となることを検出し、壁時計時間ではなく因果履歴によってマージされるMulti-Value Registersに移行することを促しました。展開後、自動化されたカオステストは、高いパーティション頻度下でのトンストーンの蓄積に関する3つの追加エッジケースを特定し、データ損失のインシデントを99.7%削減し、検出までの平均時間を数日から数分に短縮しました。
メモリのリークテスト時に、状態ベースのCRDT(Replicated Growable Array (RGA)など)のガベージコレクションの非決定性にどう対処しますか?
多くの候補者は、ガベージコレクション(トンストーンの削除)が決定的であり、削除操作の後に直ちにトリガーできると仮定します。実際には、RGAのガベージコレクションは因果的安定性の達成に依存し、これにはすべてのレプリカがベクトルクロック優位性を介して削除マーカーを観察したことを確認する必要があります。正しいテストアプローチは、すべてのノード間でベクトルクロックの境界を追跡するCausal Stability Detectorをハーネスに実装し、検出器が普遍的な確認を確認したときのみトンストーンの削除をトリガーします。テストは、メモリのリークを防ぐためにGCが発生するだけでなく、早期の削除が収束を維持することを確認する必要があります。トンストーンを早すぎる段階で削除すると、長時間実行される同期セッションで数時間後にのみ現れる永続的な分岐を引き起こします。
CRDTの収束を確認するために、なぜ標準的な等値演算子(===)を使用できないのか、代わりにテストフレームワークが検証すべき数学的特性は何ですか?
候補者はしばしば expect(replicaA.state).toEqual(replicaB.state) のようなアサーションを書きますが、これはCRDTには失敗します。なぜなら、内部メタデータ(ベクトルクロック、操作履歴、ノードIDなど)が、ユーザー可視状態が収束しているときでも異なる可能性があるからです。収束の数学的完全性を保証するためには、結合半格の**Least Upper Bound (LUB)**特性を検証し、3つの数学的公理:結合性(merge(A, B) == merge(B, A))、関連性(merge(A, merge(B, C)) == merge(merge(A, B), C))、および冪等性(merge(A, A) == A)を確認する必要があります。テストフレームワークは、内部のCRDTメタデータを無視しながら、マージ後の可観測なユーザーステートを抽出し、すべてのレプリカがマージの順序やパーティション履歴に関係なく、同一のLUB状態に到達することを確認する必要があります。このアプローチは、収束が数学的に正しいことを保証し、実装の詳細に起因する偶然の等価性から解放されます。
無限待機の導入や、一時的なネットワークレイテンシによる誤検知なしに、収束の生存性をテストするにはどうすればよいですか?
この課題は、分散システムに適用された停止問題を表し、候補者はしばしば await sleep(5000) のような任意のタイムアウトを実装して不安定なテストや誤検知を生じさせます。解決策は、指数バックオフポーリングとNetwork Quiescence Detectorを組み合わせたConvergence Predicateを実装し、Toxiproxyのメトリクスまたはパケットキャプチャを監視して、進行中の操作がないことを確認します。ネットワークが静穏になり、すべてのレプリカが同一のベクトルクロック境界を報告したときのみ、収束を宣言できます。この際、(operation_count * max_latency) + clock_skew_bufferから計算された適応タイムアウトを使用します。この計算された上限内で収束が達成されなかった場合、テストは決定論的に失敗し、スタック状態をデバッグするための明確な信号を提供します。