TT Lab
はじめる
学ぶ 学習パス コース

EAI 中間層をつくる

同じ GUID が二度来た — 台帳で止める

TT Labで続きを見る

目標

中継層にGUIDを基準とする状態元帳(SQLite)を組み込んで二重処理を防ぎ、結果未確定の取引を照会で確定し、未送信が確定した取引だけを安全に再処理します。

なぜ重要なのか

チャネルはタイムアウトになると同じGUIDで再送し、勘定系は冪等ではありません。中継層が「このGUIDをすでに送ったか、結果を知っているか」を覚えていなければ、再送がそのまま二重振込になります。そして、失敗の一覧をそっくり再送する再処理が、最もよくある大事故です。結果のわからない取引と、送れなかった取引を区別する必要があります。

ステップ

  1. /root/eaimw/dedup/schema.sqlを書いてください。テーブルtxには、guid(主キー)、tx_code、state(RECEIVED・SENT・DONE・UNKNOWN・FAILEDのみを許可するCHECK)、rsp_code、request(リクエスト電文のBLOB)、response(レスポンス電文のBLOB)、attempts(整数)、updated_at(datetime('now')形式の文字列)を置きます。2回適用してもエラーが出てはいけません(IF NOT EXISTS)。
  2. cp /opt/lab/fixtures/eaimw/dedup/relay_base.py /root/eaimw/dedup/relay.pyで始めてください。--db <경로>(プレースホルダーはパスです)引数を追加し、起動時に同じディレクトリのschema.sqlを適用します。勘定系を呼び出す前にGUIDで1行を先取りし(SENT)、結果を受け取ったらDONEとレスポンス電文を保存します。DONEのGUIDが再び来たら、勘定系を呼ばずに、保存したレスポンスをそのまま返します。
  3. 先取りしたGUIDがまだ処理中(SENT)のときに同じGUIDが来たら、待たずにすぐE903で答えてください。先取りは原子的に行います(INSERT ... ON CONFLICT(guid) DO NOTHINGの行数で判断)。
  4. 読み取りタイムアウト(E901)は元帳にUNKNOWN、接続失敗(E902)はFAILEDとして残してください。UNKNOWNのGUIDが再び来たら、勘定系を呼ばずにE901で答えます。FAILEDのGUIDが再び来たら、再送してかまいません(試行回数+1)。
  5. /root/eaimw/dedup/resolve.py --db <경로> --core <URL> --min-age <초>(プレースホルダーはパスと秒数です)を作成してください。更新から--min-age秒以上経過したUNKNOWNだけを、勘定系の照会API(GET /v1/transfers/<guid>)で確定します。200ならDONE・0000とレスポンス電文(リクエストヘッダーから作ったR電文+照会結果から作った45バイトの本文)、404ならFAILED・E902です。照会中に接続エラーが出たら、そのままにします。振込を再送してはいけません。
  6. /root/eaimw/dedup/reprocess.py --db <경로> --core <URL> --max-attempts <N>(プレースホルダーはパスです)を作成してください。FAILEDかつrsp_codeがE902で、attemptsがN未満の行だけを、保存したリクエスト電文で同じGUIDのまま再送します(先取りしてからattemptsを+1し、結果はステップ2と同じルールで記録)。UNKNOWNは絶対に送りません。
  7. /root/eaimw/dedup/purge.py --db <경로> --days <N>(プレースホルダーはパスです)を作成してください。N日を過ぎたDONEだけを削除します。UNKNOWN・FAILED・SENTは、期間に関係なく残します。

参考

状態元帳のスキーマ

/root/eaimw/dedup/schema.sqlに、txテーブル(guidが主キー、状態のCHECK、リクエスト・レスポンスの原文、試行回数、更新時刻)を書いてください。

CREATE TABLE IF NOT EXISTSで、2回適用しても安全にします。CHECK (state IN (...))が、誤った状態の書き間違いを防ぎます。原文はBLOBです。

完了した取引の再送。保存した答えをそのまま返す

relay_base.pyをコピーして元帳を組み込んでください。送る前にSENTで先取りし、結果はDONEとレスポンス電文で保存し、DONEの再送には保存したレスポンスをそのまま返します。

handleでcall_coreを呼ぶ前に、INSERT ... ON CONFLICT(guid) DO NOTHINGで1行を先取りし、すでにあればSELECTで状態を見ます。起動時にschema.sqlをexecutescriptで適用してください。

処理中の再送。すぐE903

SENT状態のGUIDが再び来たら、待たずにE903で答えてください。

先取りに失敗したのにDONEでなければ、誰かが処理中です。照会してから挿入する2段階ではなく、挿入1回の行数で判断すれば、同時再送でも片方だけが先取りします。

わからないものはUNKNOWN、送れなかったものはFAILED

E901はUNKNOWN、E902はFAILEDとして残してください。UNKNOWNの再送は呼び出さずにE901、FAILEDの再送は再送信します。

結果を書くときに、応答コードによって状態を選びます。FAILEDの再先取りも、UPDATE ... WHERE state='FAILED'の行数で原子的に行います。

照会で確定する。再送はしない

/root/eaimw/dedup/resolve.pyが、古いUNKNOWNだけを照会APIで確定するようにしてください(200→DONE、404→FAILED/E902、エラー→そのまま)。

updated_at <= datetime('now', '-N seconds')で、古いものだけを選びます。たった今タイムアウトした取引は、勘定系がまだ処理中かもしれません。レスポンス電文はlhstd.reply(リクエスト電文, '0000', 本文)です。

未送信が確定したものだけを同じGUIDで再処理する

/root/eaimw/dedup/reprocess.pyが、FAILED/E902で、attemptsが上限未満の行だけを、保存したリクエストで再送するようにしてください。

対象の選択条件がすべてです。state、rsp_code、attemptsです。送る前にSENTで先取りし直し、attemptsを上げます。GUIDは、保存したリクエスト電文のものをそのまま使います。

元帳の整理。削除してよいものだけ

/root/eaimw/dedup/purge.py --days Nが、N日を過ぎたDONEだけを削除するようにしてください。

DELETEのWHEREに、状態と期間の両方を掛けます。UNKNOWNを削除すると、その取引は調査する根拠を失います。