コンテンツにスキップ

Sync Errors

client.onError は、回復可能・回復不可能なすべての sync イベントで発火します。 kind フィールドで種別を判別します。このページは、union が定義する 6 つの kind (push-httppull-httpnetworkrebase-replayschema-mismatchblob-upload-failed) に対する完全なトリアージ表です。

原因: fetch() が throw しました。オフライン、DNS 障害、TLS 障害など。

回復: plasma は指数バックオフで自動的にリトライを続けます。ユーザーに オフラインであることを知らせる以外に、あなたがすべきことはありません。

UI:

onError: (err) => {
if (err.kind === "network") setBanner("Reconnecting…")
}

次の push/pull が成功したらバナーを消します (明示的なイベントはありません。 outbox の深さをヒューリスティックとして使いましょう — 減ったならオンラインに 戻っています)。

原因: push (または pull) の HTTP 呼び出しが、リトライ後に plasma が 回復不可能と判断した非 2xx ステータスを返しました。典型的にはリトライ枯渇後の 5xx、あるいは呼び出しレイヤーが表面化させる 4xx です。

回復: plasma はポーリングを続けます。次の試行が成功すれば暗黙に解消されます。 ログを取る以外にすべきことはありません。

UI:

onError: (err) => {
if (err.kind === "push-http" || err.kind === "pull-http") {
console.warn(`${err.kind} ${err.status} at ${err.url}`)
}
}

サーバー側の mutator が throw する — client kind なし、静かに revert

Section titled “サーバー側の mutator が throw する — client kind なし、静かに revert”

mutator がサーバー上で throw した場合 (PlasmaAuthorizationErrorMutatorValidationError、あるいは素の throw)、push の HTTP 呼び出しは 依然として { ok: true } を返し、SyncClientError は発火しません。

サーバーは次のことを行います。

  1. mutator のトランザクションをロールバックする。
  2. _plasma_client_mutations.last_mutation_id を poison エントリの先へ進める (永遠にリトライされないように)。
  3. 失敗した mutation の change log 行を出力しない。

クライアントは次の pull でこのドロップを知ります。lastMutationIDs が進んだ ウォーターマークを報告するのに、対応する change が現れないためです。 dropConfirmed が outbox エントリを削除し、rebuildOptimistic が、その mutation の書き込みを持たない base ストアの上に、生き残った outbox を再実行します。 optimistic ビューは revert します。

revert に向けた UX を設計する。 mutator が、UI で予測できる理由 (クォータ超過、拒否、アイテム削除) で失敗しうる場合は、mutate() を呼ぶ前に コンポーネントで事前チェックしてください。特定の mutation の結果をクライアント側で 監視するということは、次の rebase で行そのものが消えるのを観察することを意味します。

原因: クライアントとサーバーが SCHEMA_VERSION について食い違っています。 Schema mismatch で扱っています。

原因: rebuildOptimistic 中にリプレイされているキュー済み mutation が throw しました。通常は、参照していた行が別のクライアントによってすでに削除された ためです (pull が削除を持ち込み、ローカルのリプレイが db.update(...).where(id=...) を試みて失敗する)。

回復: plasma は rebase 中の throw を飲み込みます。次の push サイクルは、 その mutation をサーバーにも送ります — サーバー側の実行が同じ理由で throw し、 それが last_mutation_id を poison エントリの先へ進めます。outbox は次の pull で 空になります。

UI: rebase-replay の境界ですべきことはありません。ログで 1 つの mutation に 対する rebase-replay のストリームが見られる場合、それは row-not-found の競合が 2 回以上の pull サイクルにわたって続いていることを意味します。これは通常、 スケジュールされた削除が mutation の push と競合しているヒントであり、調査に 値します。

原因: R2 の PUT /sync/blob/:hashblobUploadRetry.maxAttempts (デフォルト 5) 後に失敗しました。ネットワーク隣接ですが、独自のリトライ予算で ゲートされています。

回復: upload レコードが state: "failed" に移ります。その blob 依存を持つ mutation は BLOCKED になります — upload が解決するまで、その outbox エントリは push されません。

イベントの形: { kind: "blob-upload-failed", hash, attempts, error }。 このイベントは mutation ID ではなく blob のハッシュ を運ぶことに注意してください — 1 つのハッシュが複数の outbox エントリを裏付けることがあるため、回復はハッシュ 単位です。

UI アクション:

onError: (err) => {
if (err.kind === "blob-upload-failed") {
setPendingUploads((prev) => [...prev, err])
}
}
// Later, in the UI:
<button onClick={() => client.retryBlobUpload(err.hash)}>Retry</button>

retryBlobUpload(hash) は新しい試行予算で再キューします。ハッシュ単位の破棄は ありません — mutation を完全に放棄するには、失敗したハッシュを参照する各 outbox エントリに対して client.discardMutation(mutationID) を呼びます (client の state をたどるか、Devtools パネルで ID を探します)。

原因: GET /sync/blob/:hash リクエストが SyncHandlerOptions.readAuthMaxRefs (デフォルト 128) を超える参照を持っていたため、サーバーは提供するかどうかを 決める前にすべての ref をチェックしませんでした。

回復: blob リクエストは正常に完了します — この上限は正しさではなく パフォーマンスのガードです。チェックされた ref のいずれかが auth.read を通過すれば blob は提供されます。どれも通らなければ 404 です。

UI: これはサーバー側のイベントで、client.onError 経由では公開されません。 サーバー側のテレメトリ hook から出力してください。これが見られる場合、影響を受けた ユーザーは共有 blob の認可ギャップに当たっている可能性があります (通常の対処は _plasma_blob_refs の最も古い参照を刈り込んで fan-out を下げることです)。

phase: "push" | "pull" フィールドを運ぶのは networkschema-mismatch の kind だけです — この 2 つについては、push 側 (ユーザーが保存を試みたばかり) と pull 側 (バックグラウンドポーリング) の失敗を区別できます。

onError: (err) => {
if ("phase" in err) {
console.error(`[${err.phase}] ${err.kind}`, err)
} else {
console.error(`[?] ${err.kind}`, err)
}
}

他の kind は暗黙に phase が定まっています。push-http は push、pull-http は pull、rebase-replay は rebase、blob-upload-failed は upload ワーカーです。