キューの入場券処理を安全に完了する

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

はじめに

Nightjar Print Studio はワークショップ登録を受け付け、ダウンロードできる入場券を準備します。ワーカー停止中でも登録は安全に待機できますが、現在のワーカーは配信を早すぎる段階で確認します。また、完了した処理を再試行すると重複券が生じる可能性があります。

基本的な端末操作と、短い Python 関数、インデント、辞書参照を読む能力が必要です。下の短い橋渡し説明で構文を復習し、SQL とトランザクションを紹介します。RabbitMQ、Python、SQLite データベース、完全なサイトは準備済みで、編集するのは handler.py だけです。インストール、外部アカウント、支払い、メールは不要です。

実際の登録・入場券ページには Nightjar、コマンドには Workstation を使います。各単元は独立して始まり、サイトと実際のラベルは英語のままです。単一マシンでのワーカー中断演習であり、高可用性や厳密に一度だけの配信を保証しません。

登録から入場券までを追う

このステップは Nightjar から始めます。ワーカーを変更する前に、業務の通常動作を確認します。

Explore the evening を開き、内容を読んで Plan your visit に進みます。The evening で戻り、Guest name に Normal guest を入力して Register for the evening を選択します。新しい登録ページには R-0002 などの ID と Your ticket is being prepared が表示されます。登録の保存と入場券の発行は別です。Your registrations にある Mina Patel の既存券も保持してください。

Nightjar ワークショップの実際の登録フォーム

Guest name フォームはワークショップページにあり、登録状態と入場券は別々のページです。

Workstation に切り替えます。pwd は作業ディレクトリ、cat はテキストファイルを表示します。ディレクトリは /home/labex/project/ticket-desk のはずです。

pwd
cat handler.py

キューは処理を待つメッセージを保存します。RabbitMQ は実際のキュー broker です。ワーカーは登録 ID と名前を含むメッセージを受信し、入場券を書き、配信を確認します。確認はその配信を削除してよいと RabbitMQ に伝えるもので、入場券自体を作る操作ではありません。

Python の def handle(...) は関数を定義し、インデントされた行が本体になります。準備済みワーカーが各メッセージについて呼び出します。job は辞書で、job['id'] と job['name'] は値の参照です。ch.basic_ack(delivery.delivery_tag) は受信したチャネルでその配信を確認します。checkpoint は学習用の一時停止関数で、通常は何もしません。対応する --pause オプションを指定すると Enter または Ctrl+C を待ちます。業務処理や broker イベントを偽装しません。

データベースは行の集まりであるテーブルを持ちます。db.execute(...) が SQL を実行し、INSERT INTO tickets... が入場券の行を追加します。各 ? に続くタプルの値を渡すので、名前を SQL 命令ではなくデータとして扱えます。with db: はインデントされたブロックが正常終了すると書き込みをコミットし、例外が外に出るとロールバックします。コミットによって変更が永続化されます。現在のコードは処理前に確認しており、次で順序を直します。

python3 worker.py は準備済みワーカーを一度動かします。準備できたメッセージを 1 件受信し、ハンドラーを呼んで終了します。まず中断せずに実行します。

python3 worker.py

Received R-0002 redelivered=False と Handler returned. が期待値です。追加登録すると ID は変わります。redelivered は入場券データベースではなく RabbitMQ の情報です。危険な順序でも正常終了すれば本物の券が作られます。

Nightjar に戻り、Your registrations、Normal guest、Open ticket の順に開きます。Download ticket からテキストを読み、同じ登録 ID と名前があることを確認します。Back to registration で戻ります。Mina Patel の券も開ける必要があります。修正後もこの顧客経路を保持します。

No ready registrations が出たら再試行前に一覧を見ます。以前の実行で処理済みかもしれません。名前の入力ミスは指定名で再登録できますが、各 ID は独立したままです。結果を作るためにデータベースを編集しないでください。

完了した処理だけを確認する

このステップは Workstation から始めます。確認をトランザクションの後に移し、未完了ジョブを中断して RabbitMQ に戻る様子を見ます。

先に確認してから障害が起きると、RabbitMQ に再試行すべき未確認の配信は残りません。コミット後の確認なら券が存在するまで未確認にできます。ワーカー中断への対策ですが、RabbitMQ と SQLite が一つのトランザクションになるわけではありません。

次のブロックで handler.py だけを置き換えます。cat > handler.py はファイルを上書きし、<<'PYTHON' は内容をそのまま扱うヒアドキュメントを開始します。最後の PYTHON を単独行にして全体を貼り付け、4 空白のインデントを保ちます。トランザクションは変えず、確認だけを後に移します。

cat > handler.py <<'PYTHON'
from common import checkpoint

def handle(ch, delivery, job, db, pause):
    checkpoint(pause, 'before-write')
    with db:
        db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
                   (job['id'], job['name']))
    checkpoint(pause, 'after-commit')
    ch.basic_ack(delivery.delivery_tag)
PYTHON

Nightjar の The evening で Retry guest を登録し、そのページを開いておきます。Workstation に戻ります。--pause before-write はデータベース書き込み直前で止まります。

python3 worker.py --pause before-write

Paused before-write. を待ち、端末で Ctrl+C を押します。この意図的な中断は終了状態 130 です。直後に $? で直前のコマンドの終了状態を表示します。

echo $?

130 が期待値です。Nightjar で Retry guest の登録を更新しても券はありません。Workstation に戻り、停止オプションなしで再実行します。

python3 worker.py

今度は redelivered=True が期待値です。前の接続が閉じたとき、RabbitMQ が実際に未確認の配信を戻しました。Nightjar で登録を更新し、券を開いてダウンロードします。復旧した券は同じ登録 ID に属します。

止まらない場合はオプションの綴りと cat handler.py を確認します。誤って Enter を押した場合は正常終了したので、別の Retry guest を登録して中断を繰り返します。古いコードで確認後に中断したなら、python3 desk.py status で ID を調べ、python3 desk.py retry R-0003 の ID を実値に置き換えて既存登録の再配信を要求できます。これは学習者による明示的な復旧で、採点は行いません。次のステップで重複排除を追加するまで、発行済み券を再試行しないでください。

データベースの結果を安全に繰り返す

このステップは Workstation から始めます。第二の障害区間が残っています。SQLite が券をコミットしてから RabbitMQ の確認前にワーカーが止まると、現コードは再配信時に別の券を作ります。

ここでの冪等性は、同じ登録 ID を再処理しても業務結果を一つに保つことです。名前は登録の識別子になりません。同じ Same name という 2 人にも別の券が必要です。準備済み completed テーブルの主キー id は同じ ID の 2 行を許しません。

INSERT OR IGNORE は ID の追加を試みます。rowcount は新規行なら 1、既存 ID なら 0 です。(job['id'],) は値が一つのタプルで、カンマが重要です。if added: は新規 ID の場合だけインデントされた券の追加を実行します。二つの書き込みは同じ with db: ブロック内に置きます。別だと、完了キーだけを保存して券を失い、正当な登録を永久にスキップしかねません。

関数を次の短い完全な実装に置き換えます。ロールバックを観察できるよう、トランザクション内の停止点を二つの書き込みの間に置いています。

cat > handler.py <<'PYTHON'
from common import checkpoint

def handle(ch, delivery, job, db, pause):
    checkpoint(pause, 'before-write')
    with db:
        added = db.execute('INSERT OR IGNORE INTO completed VALUES (?)',
                           (job['id'],)).rowcount
        checkpoint(pause, 'inside-transaction')
        if added:
            db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
                       (job['id'], job['name']))
    checkpoint(pause, 'after-commit')
    ch.basic_ack(delivery.delivery_tag)
PYTHON

Nightjar で Same name を 2 回登録します。通常は R-0004 と R-0005 という別 ID になります。Workstation に戻り、最初の処理をトランザクション内で止めます。

python3 worker.py --pause inside-transaction

Paused inside-transaction. で Ctrl+C を押します。Nightjar で登録を更新すると券はなく、完了キーも残ってはいけません。Workstation で同じ ID を再試行し、今度はコミット後に止めます。

python3 worker.py --pause after-commit

Paused after-commit. で Nightjar の登録を開くと、券が 1 枚あります。Workstation に戻って Ctrl+C を押し、未確認の配信をもう一度処理します。

python3 worker.py

redelivered=True が表示されても、Nightjar で更新した登録の券はちょうど 1 枚のままです。重複する完了キーの追加が 0 を返すため、ハンドラーは二枚目を作らず配信を確認します。

Workstation に戻ります。--drain は準備できたメッセージがなくなるまで同じ 1 件処理を繰り返します。

python3 worker.py --drain

二つ目の Same name 登録も自分の券を得ます。desk.py status は実際の行を読みます。

python3 desk.py status

明示的な重複配信を試すため、出力の最初の Same name ID を使います。通常は R-0004 ですが、異なる場合は実値に置き換えます。

python3 desk.py retry R-0004
python3 worker.py --drain

Nightjar で二つの Same name ID がそれぞれダウンロード可能な券を 1 枚持ち、Mina Patel、Normal guest、Retry guest も保持されます。待機する登録があってはいけません。No ready registrations だけでは成功の証明になりません。ジョブを失ってもキューは空になります。

Python のインデントや構文エラーなら関数を再コピーして再試行します。未確認の処理は RabbitMQ に戻ります。二つの書き込みを一緒に保ち、誤った方法を正しく見せるために券や完了キーを削除しないでください。

古い非冪等コードで発行済み ID を処理したら、まず handler.py を修正します。準備済み repair 操作で他の参加者をリセットせず局所復旧できます。python3 desk.py status で対象 ID を調べます。例えば Normal guest が R-0002 ならプレビューします。

python3 desk.py repair R-0002

残す最初の原券、削除する追加券番号、この ID だけの完了記録が表示されます。まだ変更しません。--apply が一つのトランザクション内で計画を明示的に適用します。

python3 desk.py repair R-0002 --apply

対象 ID に置き換えてください。未知の ID、券がない ID、登録と券の名前が不一致の場合は拒否されます。最初の券番号と内容、他の登録と券は保持されます。修復した ID を完了済みに記録するので、正しいハンドラーは安全にスキップできます。キューのメッセージを消費・確認することはありません。Nightjar で保持した券を開き、python3 worker.py --drain で修正後のワーカーに残りを処理させます。修復しても危険なハンドラーが隔離試験を通るわけではありません。

採点は実環境の顧客結果を読み、変更していないハンドラーを別の RabbitMQ broker と使い捨てデータベースで実行します。実環境のジョブを消費、確認、修復せずに、中断、実際の再配信、ロールバックを試験します。安全なのは繰り返すデータベースの業務効果であり、厳密に一度だけの配信の保証ではありません。外部メールや支払いの副作用には別の設計が必要です。

まとめ

キューの出票作業を復旧し、配信と業務完了を区別し、同名でも独立した登録を保持しました。コミット後の確認は再試行を可能にし、同じトランザクション内の完了キーと券の書き込みは、このデータベース結果について再試行を安全にします。厳密に一度だけの配信は保証せず、外部メール、支払い、broker 障害、すべての本番障害を扱うものでもありません。