関連するチケット更新の整合性を保つ

CloudflareBeginner
オンラインで実践に進む

はじめに

解決メモが保存されていないチケットはクローズできないようにします。D1 の prepared batch を使って両方の書き込みをまとめて処理し、その後、Sessions API のブックマークをリクエスト間で引き継ぐことで、後続の読み取りからすでにコミットされた内容を確認できるようにします。

この実験では、アトミックなロールバックと、セッションによる順次整合性を区別します。独立した 1 つのデータベースと Worker を使用し、read replica の有効化やレプリケーション競合の再現は必要ありません。

自分の学習アカウントと新しい VM を使用してください。セットアップではまず Node.js 22.22.0 を準備し、その後 /home/labex/project/ticket-databasenpm install を実行して、プロジェクトローカルの Wrangler 4.131.1 と評価に必要な依存関係をインストールします。直接指定された依存関係のバージョンは固定され、インストール時に専用の lockfile が作成されます。セットアップ中にクラウドへのログインや、評価対象となるデータベース操作は実行されません。個人のマシンを使用する場合は、プロジェクト内で npm install --save-dev wrangler@4.131.1 を実行して同じ Wrangler バージョンをインストールしてください。

この演習では、D1 Free allowances の範囲内で小規模な合成レコードを使用します。既存のアカウント使用量もこの上限に含まれます。購入済みのドメインは必要ありません。リソースの削除とログアウトの両方を確認するまで、この VM を保持してください。

この VM を認証してアカウントを選択する

このステップでは、新しいターミナルを自分の学習アカウントに接続します。Dashboard にログインしただけでは VM は認証されません。D1 の権限ではデータベースの作成、SQL の変更、削除が許可されます。Workers の権限ではデプロイが許可され、KV の権限は Wrangler のクリーンアップ時にリソース一覧を確認するために使用されます。認証する前に、Background Access を含む実際の同意画面を確認してください。

準備済みのプロジェクトを開き、固定された CLI のバージョンを確認します。

cd /home/labex/project/ticket-database
npx wrangler --version

4.131.1 と表示されることを確認します。デバイス認証を開始します。--device はブラウザに入力するコードを表示し、--browser=false はブラウザの選択を自分で行えるようにします。

npx wrangler login --device --browser=false --scopes account:read user:read d1:write workers_scripts:write workers_kv:write

表示された URL をブラウザで開き、現在のコードを入力し、学習アカウントと権限を確認して認証を許可します。ターミナルに成功メッセージが表示されるまで待ってください。パスワードやトークンをプロジェクトファイルに貼り付けないでください。

npx wrangler whoami --json

loggedIn: true を確認し、アカウントが 1 つだけ表示される場合でも、アカウントの nameid を読み取ります。使用するアカウントの ID をコピーし、次の設定に入力します。次のシェル変数は 6 バイトの乱数(12 個の 16 進数文字)を使い、他の学習者との名前の衝突を避けます。ヒアドキュメントによって、JSON 行の間にある JSON が書き込まれます。$RUN はその中で展開されます。

$schema の前のバックスラッシュは、この JSON キーを文字どおり保持します。$RUN は引き続き今回の実行に固有の名前に展開されます。

RUN=labex-c04-d05-$(openssl rand -hex 6)
cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-15",
  "workers_dev": true,
  "preview_urls": false
}
JSON

ブロックを実行する前に YOUR_ACCOUNT_ID を置き換えてください。RUN の値を保持できるよう、このターミナルは開いたままにします。name は今回の実行を識別し、account_id はクラウド操作の対象アカウントを選択します。このファイルは通常の JSON であり、JSONC としても有効です。このファイルを書き込んだだけでは Worker はデプロイされません。

チケットと解決メモを準備する

このステップでは、関連する 2 つのテーブルを準備します。チケットをクローズするときは、解決メモも保存する必要があります。一方の書き込みだけが成功すると、スタッフには説明のないクローズ済みチケットが表示される可能性があります。セットアップではスキーマと HTTP ルーターが用意されています。ここでは、関連する SQL 操作を実装します。

一時的なクラウドデータベースを作成します。--binding DB はアプリケーションコードで使用する短い名前を設定し、--update-config は実際の名前と UUID を wrangler.jsonc に記録し、--use-remote=false は開発時にローカルデータベースを使うようにします。

npx wrangler d1 create "$RUN-db" --binding DB --update-config --use-remote=false

作成された名前と ID を読み取り、保存されたバインディングを確認します。

cat wrangler.jsonc

DB エントリには、今回の実行で作成したデータベース名が設定されている必要があります。バインディングは、コードとリソースの間に設定された接続です。UUID はクラウドデータベースを識別し、--local を指定すると VM 内の別の SQLite データベースが使用されます。SQL コマンドでは、必ず --local または --remote のいずれかを指定してください。

cat schema.sql
npx wrangler d1 execute DB --local --file schema.sql
npx wrangler d1 execute DB --remote --file schema.sql

チケット 1 は open で、解決情報はありません。チケット 2 は closed で、resolution event 1 を所有しています。この使用済みの event ID によって、制御された失敗ケースを作れます。別の event 1 を挿入すると、主キー制約に違反します。

書き込みをアトミックにし、読み取りを順次実行する

このステップでは、2 つの独立した整合性問題を解決します。アトミック性とは、関連する 2 つの書き込みが両方とも成功するか、どちらも成功しないことです。D1 の batch() は prepared statement をトランザクションとして実行するため、失敗するとバッチ全体をロールバックします。別々に await した 2 つの書き込みでは、この保証は得られません。

セッションは、クエリの連続実行によって観測されたデータベースの状態を追跡します。用意されているルーターは env.DB.withSession(...) を呼び出し、クライアントにブックマークがない場合は first-primary から開始します。ルーターは getBookmark() の値を x-d1-bookmark ヘッダーで返します。後続のリクエストでそのブックマークを送ると、少なくともそのデータベース状態以降の内容を読み取れます。これは順次整合性であり、HTTP リクエストをまたぐオール・オア・ナッシングのトランザクションではありません。

ルーターから渡されるセッションを使って、両方の関数を実装します。

cat > src/store.js <<'JS'
export async function closeTicket(session, id, eventId, note) {
  await session.batch([
    session.prepare("UPDATE tickets SET status = 'closed' WHERE id = ?").bind(id),
    session.prepare('INSERT INTO resolutions(event_id, ticket_id, note) VALUES (?, ?, ?)').bind(eventId, id, note)
  ]);
}
export async function readTicket(session, id) {
  const ticket = await session.prepare('SELECT id, subject, status FROM tickets WHERE id = ?').bind(id).first();
  if (!ticket) return null;
  const { results } = await session.prepare('SELECT event_id, note FROM resolutions WHERE ticket_id = ? ORDER BY event_id').bind(id).all();
  return { ...ticket, resolutions: results };
}
JS

src/index.js を読み、withSession、受信したブックマークヘッダー、返却されるブックマークの位置を確認します。そのリクエスト内のすべてのデータベース操作は、そのセッションを使います。ブックマークは不透明な位置情報です。解析したり作り直したりせず、そのまま返してください。

cat src/index.js
npx wrangler dev --ip 0.0.0.0 > dev.log 2>&1 &
cat dev.log

ローカルで待ち受けを開始したことを示すメッセージが表示されるまで待ちます。ローカルシミュレーションではバッチのロールバックをテストできますが、実際のリモートレプリケーションやクラウドのブックマークを確認することはできません。

成功するクローズの前にロールバックを確認する

このステップでは、すでに使用されている event ID を意図的に送信します。バッチの 1 つ目の文はチケット 1 をクローズしようとしますが、2 つ目の文が失敗します。失敗後の結果を読み取ります。

curl -i http://localhost:8787/tickets/1/close -H 'Content-Type: application/json' -d '{"event_id":1,"note":"Must roll back"}'
curl -i http://localhost:8787/tickets/1

event_conflict を示す 409 が返り、その後のレスポンスでは、チケット 1 が引き続き open で、resolutions 配列が空であることを確認します。409 だけでは不十分です。続けて読み取ることで、部分的な更新が残っていないことを確認できます。

次に、未使用の event ID 2 を使います。

curl -i http://localhost:8787/tickets/1/close -H 'Content-Type: application/json' -d '{"event_id":2,"note":"Access restored"}'
curl -i http://localhost:8787/tickets/1

HTTP 200 が返り、チケット 1 が closed になり、Access restored というメモを持つ resolution event 2 が表示されることを確認します。これで両方のレコードが一致します。レプリケーション遅延を発生させるために、ローカルデータをリセットしたり、リモートデータベースを変更したりしないでください。

ブックマークを使ってリモートセッションを続行する

このステップでは、同じバッチを D1 に対して実行し、実際のブックマークをリクエスト間で引き継ぎます。リモートの初期データはまだ変更されていません。

npx wrangler deploy

実際のデプロイ URL をコピーします。まず、失敗するバッチをもう一度実行してロールバックを確認します。

URL='YOUR_DEPLOYED_HTTPS_URL'
curl -i "$URL/tickets/1/close" -H 'Content-Type: application/json' -d '{"event_id":1,"note":"Must roll back"}'
curl -i "$URL/tickets/1"

409 が返り、その後の読み取りで、チケットが open のまま、解決情報がないことを確認します。デプロイの反映に時間がかかっている場合は、最大 1 分間、読み取りを再試行してください。プラットフォームのエラーページを、アプリケーションが返す JSON の仕様と間違えないでください。

成功するクローズを送信します。

curl -i "$URL/tickets/1/close" -H 'Content-Type: application/json' -d '{"event_id":2,"note":"Access restored"}'

クローズ済みのチケットと、そのメモが返ることを確認します。レスポンスヘッダー x-d1-bookmark に含まれる空でない値を余分な空白なしでコピーし、次の変数に設定します。

BOOKMARK='YOUR_RESPONSE_BOOKMARK'
curl -i "$URL/tickets/1" -H "x-d1-bookmark: $BOOKMARK"

続く読み取りで、クローズ済みのチケットと resolution event 2 が確認できる必要があります。ブックマークは、読み取りで許容される古さを制限するものであり、認証トークンではありません。この手順では、stale read を必要とせず、read replication を有効にすることもありません。ここで確認しているのはセッションの仕様であり、レプリカ競合が発生したと主張しているわけではありません。

Dashboard で実際の Worker を開き、その DB バインディングが今回の実行で作成したデータベースを指していることを確認します。クリーンアップの前に機能確認を完了してください。

Worker と D1 のバインディング

この例では、Worker の DB バインディングが D1 データベースを指しています。生成されたリソース名の接頭辞はこの実行例を識別するもので、あなたの名前は異なります。スクリーンショットで確認できるのはバインディングのみです。上記の HTTP チェックで、アトミックなロールバック、更新の成功、ブックマークを使った読み取りの継続を検証しています。

一時リソースを削除する

このステップでは、VM が認証されたままの状態で、この実験で作成したリソースだけを削除します。最初にすべての機能確認を完了してください。削除の確認が終わるまで設定を保持します。

npx wrangler delete

今回の設定に記録された Worker 名だけが削除対象であることを確認します。

npx wrangler d1 delete DB

プロンプトを確認し、今回の実行で作成したデータベースだけを削除対象にします。その後、データベース一覧を表示します。

npx wrangler d1 list --json

記録したデータベース名と UUID が、成功したレスポンスに含まれていないことを確認します。他のリソースは残っている可能性があります。認証エラーやネットワークエラーでは判断できません。アクセスを解決し、次の操作に進む前に一覧取得を再実行してください。このステップの確認は、ログインしたまま実行します。

ローカル開発ジョブも停止します。ジョブを一覧表示し、自分が開始した wrangler dev ジョブだけを終了します(ジョブ番号が異なる場合は %1 を置き換えてください)。

jobs
kill %1

この VM の認証を終了する

このステップでは、独立した削除確認が成功した後でのみ認証を終了します。ログアウトすると、この VM に保存された Wrangler の認証情報が削除されます。VM を閉じただけでは、クラウドのクリーンアップにはなりません。

npx wrangler logout
npx wrangler whoami --json

loggedIn: false と表示されることを確認します。この未認証のクエリはゼロ以外の終了コードになる場合があります。構造化されたレスポンスに明示的にログアウト済みと示されている場合に限り、それは想定された動作です。確認を完了したら、実験環境を閉じます。

まとめ

関連するチケット更新の整合性を保つ方法を実践しました。データベースの結果を確認し、選択したアカウントとローカル状態を明示的に管理し、ログアウトする前に一時リソースを削除しました。