コンテンツにスキップ

OIC 演習2:ビジネスイベントで外部システムへ通知する

  • ビジネスイベントの購読(有効化、ロール、フィルタ、接続の制約)を設計・設定できる
  • 「イベントを受ける → 最新状態を取り直す → 外部へ REST で通知する」統合を組み立てられる
  • 冪等キー、リトライ、再送信、突合を、障害のパターンごとに設計できる
  • 重複・順不同・受信側の停止・応答の喪失といったケースで、何が起きるかを説明できる
  • 環境がなくても、設計シートとモック API のシミュレーションで同じ考え方を体験できる

イベントの仕組みと一覧は ビジネスイベントとアウトバウンド連携のパターン、アダプタの設定は 接続とアダプタ、フォルト処理と再送信は 障害処理・テスト・監視・移送 を参照してください。

通知は「1 回だけ・順番どおり」とは限らない

Section titled “通知は「1 回だけ・順番どおり」とは限らない”

公式ドキュメントの範囲では、ビジネスイベントの配信回数や順序の保証は確認できていません。確認できている挙動は次のとおりです。

  • OIC は失敗したメッセージを 自動では再送信しません(手動のみ)
  • 統合を無効化するとき購読を削除せず、6 時間以内 に有効化し直すと、その間のイベントが再送されます
  • 手動の再送信は、統合を 最初から 実行し直します

そのため、重複・欠落・順不同のどれも起こり得る 前提で設計します。

原則 内容 この演習での実装
通知は合図 「何が起きたか」の対象だけを使い、内容は REST で取り直す 請求書 ID で最新の請求書を取得する
入力から決まるキー 同じ入力からは、何度作っても同じキーができる InvoiceId:LastUpdateDate
判定は受信側 OIC は「受信側が処理済みか」を知り得ない。キーと版は受信側で見る 同じキーなら何もせず 200、古い版は捨てて 200
やり直しの使い分け 一時エラーはリトライ、恒久エラーは通知、失敗は再送信 While + Wait、Throw New Fault、Errors

いちばん危ないのは「処理したのに応答が届かない」

Section titled “いちばん危ないのは「処理したのに応答が届かない」”

OIC が POST してタイムアウトしたとき、受信側が処理済みかどうかは OIC には分かりません。OIC にできるのは「もう一度送る」ことだけなので、受信側が冪等でなければ 二重に処理されます。

REST アダプタの呼出しには、504 エラーの自動リトライを無効化できる設定があります。受信側が冪等でない場合は、無効化を検討します。受信側を直せないときは、OIC の外に「処理済みキー」を記録する表を持つ設計もありますが、同時実行時の排他に注意が必要です。

リトライ・再送信・再有効化の違い

Section titled “リトライ・再送信・再有効化の違い”
種類 誰が、いつ 再実行されるもの 注意
リトライ 統合の中(設計して作る) 失敗した呼出しだけ 一時エラーだけ。回数と間隔を決める
再送信 運用担当(Errors ページ) 統合を最初から 手動のみ。非同期統合のみ。1 つのインスタンス ID につき 10 回まで
再有効化による再送 Fusion が、購読を残した期間のイベントを再送 イベントごとの統合の実行 6 時間以内。重複がまとめて届く

前提・準備(Fusion 環境あり/なし)

Section titled “前提・準備(Fusion 環境あり/なし)”
項目 内容
会社 サンプル商事株式会社(架空)
起点 Fusion の AP で、請求書が承認された(Payables Invoice Approved)
連携先 予算管理システム(架空)。承認済みの請求書を受け取り、予算の消化額に計上する
目的 承認から数分以内に連携する。ただし、重複・順不同・一時停止があっても 二重計上しない
対象 JP01 営業単位(架空。自社のテスト用 BU に読み替える)の請求書

要件

  1. 請求書が承認されたら、予算管理システムへ REST で通知する
  2. 通知する内容は、通知時点の最新の請求書の状態(金額・仕入先・承認状態)にする
  3. 同じ請求書の通知が 2 回届いても、予算には 1 回しか計上しない
  4. 予算管理システムが一時的に止まっていても、自動で数回やり直す。それでもだめなら運用担当に通知する
  5. 運用担当が、失敗した通知を後から再送できる
  6. 取りこぼしを、1 日 1 回の突合で見つけられるようにする(設計のみ)

受信側の API(架空)

予算管理システムの受信 API(演習用の架空の仕様)
POST /api/invoice-approved
Idempotency-Key: 300100002:2026-02-21T02:07:00+00:00
Content-Type: application/json
要求の本文(例)
{
"invoiceId": 300100002,
"invoiceNumber": "1001",
"supplier": "仮想オフィス用品株式会社",
"amount": 64800,
"currency": "JPY",
"approvalStatus": "Approved",
"lastUpdateDate": "2026-02-21T02:07:00+00:00"
}
応答 意味 OIC の扱い
201 新規に計上した 成功
200 重複(同じキー)または古い版なので、何もしなかった 成功。再試行しない
400 入力が不正(例:負の金額) 恒久エラー。リトライせず通知
503 一時的に処理できない 一時エラー。リトライ

重複を 409 などの 4xx で返す API もあります。その場合は、フォルト・ハンドラで 409 を「処理済み」として扱う設計にします。

  • Fusion のテスト環境:AP の請求書を作成・検証・承認できる権限、テスト用の BU。本番では行いません
  • 統合ユーザー:SOAOperator(イベントの受信)、請求書を REST で参照できる権限と対象 BU のデータ・アクセス。正確な要件は公式の「Assign Required Roles to an Integration User」で確認します
  • OIC のインスタンスと、統合を作れるロール。プライベート・エンドポイント経由の接続はビジネスイベントを受けられません。OIC に許可リスト(allowlist)を設定している場合は、Fusion の送信元 IP を許可します
  • 受信側:受け取った内容を確認できるテスト用の HTTPS エンドポイント。用意できない場合は、OIC に REST トリガーの統合を 1 本作って代わりにします(受け取った内容を Logger に出し、固定の 201 を返す)。この場合は重複判定と 503 を再現できないため、机上演習とシミュレーションで補います

OIC や Fusion がなくても、次の 2 つで考え方を学べます。

  1. 机上演習:要件から設計シート(表)を作り、解答例と照合します
  2. モック API でのシミュレーション:Python(標準ライブラリのみ)で、イベントの処理、重複、受信側の停止、応答の喪失、再送信を再現し、冪等性の有無で結果がどう変わるかを見ます(練習用 API プレイグラウンド と同じモック API を使います)

ステップ1 イベントを有効にする

Section titled “ステップ1 イベントを有効にする”
  1. Setup and Maintenance で、プロファイル・オプションを管理するタスクを開きます
  2. AP: Enable Business Events(コード ORA_AP_ENABLE_BUSINESS_EVENTS)を、サイト・レベルで Y にします
  3. 対象のイベントが一覧にあることを、Fusion の /soa-infra/PublicEvent/catalog で確認します
  1. プロジェクト(例:SMP_AP_EVENT)を作成します
  2. ERP Cloud 接続 SMP_ERP_CLOUD:ロールは トリガーと呼出し(イベントの受信に使うトリガーと、請求書の取得に使う呼出しの両方が必要なため)。セキュリティ・ポリシーと統合ユーザーを設定して Test します
  3. REST 接続 SMP_BUDGET_REST:ロールは呼出し。受信側のベース URL と認証方式を設定します

ステップ3 イベントで起動する統合を作る

Section titled “ステップ3 イベントで起動する統合を作る”
  1. パターンは Application を選び、名前と識別子を AP_INV_APPROVED_NOTIFY にします
  2. トリガーに SMP_ERP_CLOUD を使い、Receive Business Events raised within ERP Cloud を選びます
  3. 請求書の承認のイベントを選びます
  4. 必要ならフィルタを書きます。対象を JP01 に絞る条件を、ワイヤー・ペイロード(アクティビティ・ストリームの TriggerWirePayload_tTextPayload)の構造で書きます。ペイロードに BU の項目がなければ、次のステップの取得後の Switch で判定します

ステップ4 最新の請求書を取得する

Section titled “ステップ4 最新の請求書を取得する”
  1. 呼出しに SMP_ERP_CLOUD を追加し、Query, Create, Update, or Delete Information → Business (REST) Resource を選び、請求書リソースの 1 件取得(get)を選びます。子リソースは選びません
  2. マップで、イベントのペイロードから 請求書を特定できる項目(請求書 ID など)を、取得の入力に対応付けます。項目名は、実際のワイヤー・ペイロードで確認します
  3. 取得する項目は、fields で必要なものに絞ります(InvoiceId、InvoiceNumber、Supplier、InvoiceAmount、InvoiceCurrency、ApprovalStatus、BusinessUnit、LastUpdateDate)

ステップ5 対象かどうかを判定する

Section titled “ステップ5 対象かどうかを判定する”
  1. Switch で、取り直した ApprovalStatus が承認済みで、BusinessUnit が JP01 であることを確認します
  2. 対象外なら、Logger で理由を記録して正常終了します(通知は送りません)
  1. Assign で、変数 idempotencyKey に InvoiceId と LastUpdateDate を : でつないだ値を入れます(式ビルダーの concat を使います)
  2. キーに、実行のたびに変わる値(現在時刻、乱数)を入れません。再送信しても同じキーになることが目的です

ステップ7 外部システムへ通知する

Section titled “ステップ7 外部システムへ通知する”
  1. 呼出しに SMP_BUDGET_REST を追加し、メソッドは POST、要求と応答の JSON のサンプルを設定します
  2. 要求ヘッダーに Idempotency-Key を追加し、マップでステップ6のキーを対応付けます
  3. 本文は、ステップ4で取り直した請求書から作ります(イベントのペイロードは使いません)
  4. 受信側が冪等でない場合は、REST アダプタの 504 の自動リトライを無効にすることを検討します

ステップ8 リトライと通知を作る

Section titled “ステップ8 リトライと通知を作る”
  1. ステップ7の呼出しを Scope で囲みます
  2. 障害処理・テスト・監視・移送 の「リトライのフロー」を使い、一時エラー(接続失敗、タイムアウト、5xx)だけ を 3 回までやり直します。待機は回数に応じて延ばします。回数が尽きたら、ループの後ろで Notification と Throw New Fault を実行します
  3. 恒久エラー(4xx) は、スコープのフォルト・ハンドラの中で、リトライせずに Notification で通知し、Re-throw Fault で外側へ投げ直します。一時エラーと恒久エラーを分ける項目は、テストのときにアクティビティ・ストリームで実際のフォルトの内容を見て決めます
  4. 統合の グローバル・フォルトハンドラ は、Notification のあとに Re-throw Fault にします。Stop は使いません(失敗が Errors に出なくなるため)

ステップ9 追跡できるようにする

Section titled “ステップ9 追跡できるようにする”
  1. ビジネス識別子 に、請求書番号(プライマリ)と請求書 ID を設定します
  2. Logger で、冪等キーと受信側の応答(201 か 200 か)を記録します

ステップ10 有効化して購読を確認する

Section titled “ステップ10 有効化して購読を確認する”
  1. 統合を有効化します。有効化した時点で、Fusion に購読が登録されます
  2. Fusion の /soa-infra/PublicEvent/subscriptions で、endpointURL が今使っている OIC を指していること、状態が有効であること、フィルタが想定どおりであることを確認します
ケース 操作 期待する結果
正常 Fusion で請求書を作成・検証し、承認する インスタンスが成功。受信側に 201 で計上される
重複 同じキーの要求を、受信側へ curl で 2 回送る 2 回目は 200(重複)。計上は 1 回
一時停止 受信側を止める(または接続の URL を一時的に誤った値にする)。請求書を承認する リトライされ、リトライ切れで通知と Errors への表示がある
再送信 受信側を復旧し、Errors からそのインスタンスを再送信する 最初から実行され、201 か 200 で終わる。二重計上されない
恒久エラー 負の金額の請求書(クレジット・メモ)を承認する リトライせず、すぐ通知される
対象外 別の BU の請求書を承認する 起動しない、または対象外として記録して終了する

確認は、Observability > Instances(アクティビティ・ストリームでキーと応答を確認)、Errors、受信側の記録の 3 か所で行います。

ステップ12 運用の手順を決める

Section titled “ステップ12 運用の手順を決める”
  1. 購読の扱い:統合を無効化するとき、Delete Event Subscription を選ぶか決めます。選ばずに 6 時間以内に有効化し直すと、その間のイベントが再送されます
  2. 環境の切替:テスト環境の Fusion に、本番の OIC を指す購読が残っていないかを endpointURL で確認します
  3. 突合:日次で、承認済み請求書を REST(LastUpdateDate で差分取得)で取り、受信側の件数・金額と比べます。差分は再通知します
  4. Errors の確認:失敗を放置しないよう、確認の担当と頻度を決めます
  • AP のビジネスイベントを有効にし、購読が登録されている(endpointURL・状態・フィルタを確認した)
  • 通知の本文を、イベントのペイロードではなく、取り直した最新状態から作っている
  • 冪等キーが InvoiceId:LastUpdateDate で、再実行しても同じ値になる
  • 同じ通知を 2 回送っても、受信側の計上は 1 回になる
  • 一時エラーだけをリトライし、400 はリトライせずに通知される
  • リトライ切れが Errors に残り、復旧後の再送信で正しく終わる
  • グローバル・フォルトハンドラが、通知のあとに Re-throw している(Stop で隠していない)
  • 購読の扱い(Delete Event Subscription、環境ごとの endpointURL)を運用手順に書いた
  • 突合の頻度・照合キー・差分の補正方法を書いた

机上演習(環境なしルート①)

Section titled “机上演習(環境なしルート①)”

課題1:上のシナリオと要件から、設計シート を表で作ってください。

  1. 表1 イベントと起動条件
  2. 表2 処理フロー
  3. 表3 冪等性の設計
  4. 表4 エラーと再送の設計
  5. 表5 欠落・順序・突合の設計

課題2:次の 2 つの設計を、6 つの事象で比べてください。

  • 設計 A:イベントのペイロードをそのまま POST する。キーはなく、受信側は届くたびに金額を加算する
  • 設計 B:最新状態を取り直して POST する。キーを付け、受信側がキーと版(LastUpdateDate)で判定する

比べる事象は、(1) 同じイベントが 2 回届く、(2) 受信側が処理したが応答が届かず、リトライする、(3) イベントのあとに請求書が修正された、(4) 古い通知が遅れて届く、(5) 受信側が 3 回失敗し、復旧後に再送信する、(6) イベントが 1 件届かない、です。

解答例を表示

表1 イベントと起動条件

項目 設計
購読するイベント 請求書の承認(Payables Invoice Approved)。画面の表示名は一覧で確認
Fusion 側の有効化 プロファイル・オプション「AP: Enable Business Events」をサイト・レベルで Y
統合ユーザー SOAOperator、請求書の参照権限、対象 BU のデータ・アクセス
フィルタ JP01 のみ(ワイヤー・ペイロードの構造で書く。項目がなければ取得後の Switch で判定)
接続の制約 プライベート・エンドポイント経由は不可。許可リストがあれば Fusion の送信元 IP を許可
統合 AP_INV_APPROVED_NOTIFY の 1 本。イベント 1 件につき 1 インスタンス

表2 処理フロー

順 処理 アクション 設計の要点
1 イベントを受ける ERP Cloud アダプタ(トリガー) 請求書 ID など対象を特定する項目だけ使う
2 最新状態を取得 ERP Cloud アダプタ(REST リソースの get) fields で必要な項目に絞る
3 対象か判定 Switch 承認済みか、JP01 か。対象外は記録して正常終了
4 キーを作る Assign InvoiceId:LastUpdateDate
5 外部へ POST REST アダプタ(呼出し) Idempotency-Key ヘッダーを付ける
6 一時エラーのリトライ Scope + While + Wait + Assign 3 回まで、待機を延ばす。4xx はやり直さない
7 失敗の通知 Notification + Re-throw Fault(リトライ切れは Throw New Fault) 請求書番号、キー、応答を本文に入れる
8 記録 Logger キーと受信側の応答(201 か 200 か)

表3 冪等性の設計

項目 設計
冪等キー InvoiceId:LastUpdateDate(入力から決まるので、再送しても同じ)
誰が判定するか 受信側。キーを記録し、同じキーなら何もせず 200 を返す
古い版 受信側が請求書ごとに LastUpdateDate を保存し、古ければ捨てて 200 を返す
キーに入れないもの 実行のたびに変わる値(現在時刻、乱数)
受信側を直せない場合 OIC の外に処理済みキーの表を持つ(排他に注意)。504 の自動リトライを無効化する

表4 エラーと再送の設計

事象 分類 OIC の動作 回復
受信側が 503・タイムアウト 一時 3 回までリトライ。尽きたら通知して Errors へ 復旧後に Errors から再送信(最初から実行。冪等なので安全)
受信側が 400 恒久 リトライせず、すぐ通知して Errors へ 元のデータや連携先の仕様を直してから再送信。直す前の再送信は同じエラーになる
請求書の取得が失敗 原因による 一時ならリトライ。認証・権限は通知 権限や接続を直して再送信
対象外(別 BU、未承認) 正常 記録して正常終了 不要
同じイベントの重複 正常 通常どおり処理し、受信側が 200 を返す 不要
イベントが届かない 検知できない 何も起きない 突合の差分で気付き、再通知する

表5 欠落・順序・突合の設計

論点 起こること 対策
重複 再有効化による再送、再送信、リトライ 冪等キーを受信側で判定する
順不同 並行して動くインスタンスが追い越すことがある 最新状態の取り直し、受信側の版の比較
欠落 購読の不備、停止中、失敗の放置 日次の突合(承認済み請求書の件数・金額を受信側と比べる)、Errors の定期確認
役割分担 イベントは速報、突合は確定 差分は突合のバッチが再通知する

課題2 の解答例

事象 設計 A(そのまま POST、キーなし) 設計 B(取り直し、キーと版)
(1) 同じイベントが 2 回届く 2 回加算される(二重計上) 2 回目は 200(重複)で、加算されない
(2) 処理済みなのに応答が届かず、リトライ 2 回加算される 2 回目は 200(重複)
(3) イベントのあとに請求書が修正された 古い金額を通知する 最新の金額を通知する
(4) 古い通知が遅れて届く 古い内容で上書きされる 版が古いので 200(古い)で捨てられる
(5) 3 回失敗し、復旧後に再送信 最初から実行されるため、二重計上の恐れがある 同じキーなので安全。受信側が未処理なら 201 になる
(6) イベントが 1 件届かない 気付けない 気付けない。突合が必要

解説

  • (2) が最も見落とされやすいケースです。OIC から見ると失敗なので、リトライや再送信で もう一度送る ことになります。受信側がキーで判定しない限り、防げません
  • (3) と (4) は、イベントの内容ではなく 最新状態と版 を使うことで防ぎます。ただし、並行して動くインスタンスが追い越す場合は、受信側の版の比較が最後の砦になります
  • (6) は、設計 B でも防げません。イベントは「速報」、突合は「確定」と役割を分けます

モック API でシミュレーションする(環境なしルート②)

Section titled “モック API でシミュレーションする(環境なしルート②)”

OIC の統合が行う処理を、Python の関数で置き換えて動かします。請求書の取得には練習用モック API を使い、受信側の予算管理システムは、スクリプトの中のクラスで再現します。モック API は 架空の応答を返すだけ で、保存はしません(仕様は 練習用 API プレイグラウンド を参照)。

  • 7 件のイベントを、受信順に処理します。重複、受信側の停止、応答の喪失、負の金額、リトライ切れが入っています
  • 請求書は、イベントの内容ではなく、モック API の GET /invoices/{InvoiceId} で 取り直します
  • 冪等キーは InvoiceId:LastUpdateDate です
  • 最後に、受信側の予算の消化額を、重複なしの期待値と比べます
  1. 次のスクリプトを event_sim.py として保存します
  2. 環境変数を設定して実行します
実行
export MOCK_BASE="https://<このサイトのURL>/api/mock/fscmRestApi/resources/11.13.18.05"
export MOCK_USER=learner MOCK_PASS=welcome1
python3 event_sim.py # 受信側が、キーと版の両方で判定する
python3 event_sim.py --no-key # 受信側が、キーを見ない
python3 event_sim.py --no-version # 受信側が、版を見ない
python3 event_sim.py --naive # 受信側が、キーも版も見ない
event_sim.py を表示
event_sim.py
"""event_sim.py:ビジネスイベント通知の「冪等性・再送」を、練習用モック API と架空の受信側で再現する。
OIC の統合が行う処理(イベントを受ける → 請求書の最新状態を REST で取り直す → 外部システムへ POST →
一時エラーはリトライ → だめなら Errors に残す → 再送信)を、Python の関数で置き換えたもの。
受信側の外部システムはこのスクリプトの中のクラスで、モック API は「請求書の取得」だけに使う。
使い方:
export MOCK_BASE="https://<このサイトのURL>/api/mock/fscmRestApi/resources/11.13.18.05"
export MOCK_USER=learner MOCK_PASS=welcome1
python3 event_sim.py # 受信側が、キーと版の両方で判定する
python3 event_sim.py --no-key # 受信側が、キーを見ない
python3 event_sim.py --no-version # 受信側が、版(LastUpdateDate)を見ない
python3 event_sim.py --naive # 受信側が、キーも版も見ない
"""
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime
BASE = os.environ["MOCK_BASE"].rstrip("/")
AUTH = "Basic " + base64.b64encode(f'{os.environ["MOCK_USER"]}:{os.environ["MOCK_PASS"]}'.encode()).decode()
MAX_ATTEMPTS = 3 # OIC では While + Wait + Assign で作るリトライの回数
WAIT_SECONDS = 0.3 # OIC では Wait アクションの待ち時間(実際は数十秒〜数分)
class Temporary(Exception):
"""一時的なエラー(停止、タイムアウト)。時間をおけば直る可能性があるので再試行する"""
class Permanent(Exception):
"""恒久的なエラー(入力の不正など)。再試行しても直らないので、すぐ通知する"""
def get_invoice(invoice_id):
"""イベントの内容ではなく、REST で請求書の最新状態を取り直す(OIC では ERP Cloud アダプタの呼出し)"""
req = urllib.request.Request(f"{BASE}/invoices/{invoice_id}?onlyData=true",
headers={"Authorization": AUTH})
try:
with urllib.request.urlopen(req, timeout=30) as res:
return json.load(res)
except urllib.error.HTTPError as e:
sys.exit(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')}")
def to_payload(inv, version=None):
return {"invoiceId": inv["InvoiceId"], "invoiceNumber": inv["InvoiceNumber"], "supplier": inv["Supplier"],
"amount": inv["InvoiceAmount"], "currency": inv["InvoiceCurrency"],
"approvalStatus": inv["ApprovalStatus"], "lastUpdateDate": version or inv["LastUpdateDate"]}
def ts(text):
return datetime.fromisoformat(text.replace("Z", "+00:00")) # 文字列ではなく日時にして比べる
class ExternalSystem:
"""受信側の外部システム(架空の予算管理システム)。キーで重複を、版で古い通知を弾く"""
def __init__(self, use_key=True, use_version=True):
self.use_key, self.use_version = use_key, use_version
self.keys = set() # 処理済みの Idempotency-Key
self.versions = {} # 請求書 ID → 保存済みの LastUpdateDate
self.spent = 0 # 予算の消化額(二重に計上されたかを見るための合計)
self.plan = {} # 請求書 ID → 試行ごとの挙動("down":停止、"lost":処理したが応答が届かない)
def receive(self, key, p):
plan = self.plan.get(p["invoiceId"], [])
mode = plan.pop(0) if plan else "ok"
if mode == "down":
raise Temporary("503 Service Unavailable")
result = self._apply(key, p)
if mode == "lost":
raise Temporary("タイムアウト(応答なし)") # 受信側は処理済み。呼び出し側からは失敗に見える
return result
def _apply(self, key, p):
if p["amount"] < 0:
return 400, "負の金額は受け付けません"
if self.use_key and key in self.keys:
return 200, "duplicate" # 同じ通知:何もしない
saved = self.versions.get(p["invoiceId"])
if self.use_version and saved and ts(p["lastUpdateDate"]) <= ts(saved):
return 200, "stale" # 古い版(同じ版を含む):捨てる
self.keys.add(key)
self.versions[p["invoiceId"]] = p["lastUpdateDate"]
self.spent += p["amount"]
return 201, "created"
def handle_event(event, ext):
"""OIC の統合 1 回分の処理(イベント受信 → 取り直し → キー作成 → POST → リトライ)"""
inv = get_invoice(event["invoiceId"])
key = f'{inv["InvoiceId"]}:{inv["LastUpdateDate"]}' # 入力から決まる値。再送しても同じキーになる
for attempt in range(1, MAX_ATTEMPTS + 1):
try:
code, body = ext.receive(key, to_payload(inv))
except Temporary as e:
print(f" 試行 {attempt}: 一時エラー({e})")
if attempt == MAX_ATTEMPTS:
raise
time.sleep(WAIT_SECONDS)
continue
if code >= 400:
raise Permanent(f"{code} {body}") # 再試行しない
return f"{code} {body}(試行 {attempt} 回)"
# 受信順に届いたイベント(演習用の架空データ。請求書 ID はモック API のもの)
EVENTS = [
# (イベント ID, 請求書 ID, 受信側の挙動(試行ごと), 説明)
("E1", 300100002, [], "通常"),
("E2", 300100003, [], "通常"),
("E1", 300100002, [], "同じイベントが重複して届く"),
("E3", 300100004, ["down", "down"], "受信側が停止。2 回失敗して 3 回目に成功"),
("E4", 300100005, ["lost"], "受信側は処理したが、応答が届かない"),
("E5", 300100018, [], "受信側が負の金額(クレジット・メモ)を拒否"),
("E6", 300100006, ["down", "down", "down"], "受信側の停止が長引き、リトライが尽きる"),
]
def main():
flags = sys.argv[1:]
naive = "--naive" in flags
ext = ExternalSystem(use_key=not naive and "--no-key" not in flags,
use_version=not naive and "--no-version" not in flags)
print(f"受信側:キー判定 {'あり' if ext.use_key else 'なし'}、版判定 {'あり' if ext.use_version else 'なし'}")
errors = [] # OIC の Errors ページに残るインスタンス
print("[1] イベントを順に処理します")
for event_id, invoice_id, plan, note in EVENTS:
ext.plan[invoice_id] = list(plan)
print(f" {event_id} 請求書 {invoice_id}:{note}")
try:
print(f" 結果:{handle_event({'invoiceId': invoice_id}, ext)}")
except Temporary as e:
print(f" NOTIFY: リトライ切れ({e})。Errors に残ります → 再送信の対象")
errors.append((event_id, invoice_id, "temporary"))
except Permanent as e:
print(f" NOTIFY: 再試行しても直らないエラー({e})。元のデータや連携先の仕様を直す")
errors.append((event_id, invoice_id, "permanent"))
print("[2] 受信側が復旧しました。Errors の再送信(最初から実行し直し)を行います")
for event_id, invoice_id, kind in errors:
if kind == "permanent":
print(f" {event_id}:同じエラーになるため再送信しない(元を直してから再送信、または破棄)")
continue
ext.plan[invoice_id] = []
print(f" {event_id} 請求書 {invoice_id}:再送信")
print(f" 結果:{handle_event({'invoiceId': invoice_id}, ext)}")
print("[3] 古い版が遅れて届く(並行して動いたインスタンスの追い越し)")
inv = get_invoice(300100002)
old = "2026-02-20T00:00:00+00:00" # 保存済みの版より古い日時
code, body = ext.receive(f'{inv["InvoiceId"]}:{old}', to_payload(inv, version=old))
print(f" 結果:{code} {body}")
expected = sum(get_invoice(i)["InvoiceAmount"] for i in (300100002, 300100003, 300100004, 300100005, 300100006))
print(f"[4] 予算の消化額 {ext.spent:,}(重複なしの期待値 {expected:,})")
if ext.spent != expected:
sys.exit(f"不一致:差分 {ext.spent - expected:+,}。二重に計上されています")
print(" 一致:二重に計上されていません")
if __name__ == "__main__":
main()
最初の実行(キーと版の両方で判定)の出力例を表示
python3 event_sim.py
受信側:キー判定 あり、版判定 あり
[1] イベントを順に処理します
E1 請求書 300100002:通常
結果:201 created(試行 1 回)
E2 請求書 300100003:通常
結果:201 created(試行 1 回)
E1 請求書 300100002:同じイベントが重複して届く
結果:200 duplicate(試行 1 回)
E3 請求書 300100004:受信側が停止。2 回失敗して 3 回目に成功
試行 1: 一時エラー(503 Service Unavailable)
試行 2: 一時エラー(503 Service Unavailable)
結果:201 created(試行 3 回)
E4 請求書 300100005:受信側は処理したが、応答が届かない
試行 1: 一時エラー(タイムアウト(応答なし))
結果:200 duplicate(試行 2 回)
E5 請求書 300100018:受信側が負の金額(クレジット・メモ)を拒否
NOTIFY: 再試行しても直らないエラー(400 負の金額は受け付けません)。元のデータや連携先の仕様を直す
E6 請求書 300100006:受信側の停止が長引き、リトライが尽きる
試行 1: 一時エラー(503 Service Unavailable)
試行 2: 一時エラー(503 Service Unavailable)
試行 3: 一時エラー(503 Service Unavailable)
NOTIFY: リトライ切れ(503 Service Unavailable)。Errors に残ります → 再送信の対象
[2] 受信側が復旧しました。Errors の再送信(最初から実行し直し)を行います
E5:同じエラーになるため再送信しない(元を直してから再送信、または破棄)
E6 請求書 300100006:再送信
結果:201 created(試行 1 回)
[3] 古い版が遅れて届く(並行して動いたインスタンスの追い越し)
結果:200 stale
[4] 予算の消化額 5,834,800(重複なしの期待値 5,834,800)
一致:二重に計上されていません

実行したら、次のことを確認してください。

  • 重複した E1 は 200 duplicate、応答が届かなかった E4 は 2 回目に 200 duplicate になり、二重に計上されない
  • E3 は、受信側が 2 回失敗したあと 3 回目で成功する(リトライ)
  • E5 は 400 で、リトライせずに NOTIFY が出る。E6 はリトライが尽きて Errors に残り、復旧後の再送信で 201 created になる
  • 古い版の通知は 200 stale で捨てられる
  • 最後に、消化額が期待値と一致する

次に、予想してから 4 つのモードを実行して、結果を比べてください。

実行 予想する点 実際の結果
--naive 重複、応答の喪失、古い版のすべてで加算される 差分 +261,600。E1 の重複、E4 の再試行、古い版が二重に計上される
--no-key 重複は、同じ版として「古い」と判定されて弾かれる 一致する。重複は stale と表示される
--no-version 重複は防げるが、古い版は通ってしまう 差分 +64,800。古い版だけが計上される

キーは「同じ通知」を、版は「古い通知」を弾きます。この演習のデータでは、版だけでも重複は防げます。ただし、版が同じで内容が違う場合や、版を持たない連携先では、キーが必要です。守る範囲が違うため、受信側は両方で判定するのが堅実です。

OIC の実装との違いも整理してください。

  • 取り直し(get_invoice())は、ERP Cloud アダプタの REST リソースの呼出しです
  • リトライ(handle_event() の for ループ)は、Scope + While + Wait + Assign で作ります
  • NOTIFY は、Notification アクションと、グローバル・フォルトハンドラでの Re-throw に相当します。リトライ切れの失敗は Errors ページに残ります
  • 手順 [2] の再送信は、運用担当が Errors ページから行います。統合が最初から実行し直されるため、取り直しとキーの作成からやり直しになります
  • 受信側のクラス(ExternalSystem)は、OIC の外にある別のシステムです。ここを冪等にしない限り、OIC の側の工夫だけでは二重計上を防げません
症状 原因と対処
イベントが届かない イベントを有効にしたか、統合が有効か、統合ユーザーに SOAOperator があるか、購読(/soa-infra/PublicEvent/subscriptions)が登録されているかを確認する。プライベート・エンドポイント経由は不可。許可リストがあれば Fusion の送信元 IP を許可する
フィルタが常に false になる マッパーのペイロードではなく、ワイヤー・ペイロードの構造で書く。result と項目の間の Value、名前空間ごとに別の接頭辞、文字列の引用符を確認する
テスト環境のイベントが本番の統合を起動する 購読の endpointURL を確認し、不要な購読を削除する。環境の複製や切替のあとに必ず確認する
Errors から再送信できない 同期統合のエラーは再送信できない。非同期でも、破棄(Discard)したものと、10 回に達したものはできない
二重に計上される 受信側がキーも版も見ていない。キーに現在時刻や乱数を入れて、毎回変わっている。504 の自動リトライで二重に届いている
古い内容で上書きされる 受信側に版の比較がない。イベントの内容をそのまま送っている
通知メールが大量に届く 失敗のたびに送っている。リトライ切れのときだけ送る、または集約する
購読を残したまま無効化していて、再有効化後に大量に届く 6 時間以内の再有効化で、その間のイベントが再送される仕様。受信側が冪等であれば問題ない。不要なら無効化時に購読を削除する
  1. 突合の統合を作る:日次のスケジュール統合で、承認済み請求書を LastUpdateDate で差分取得し、受信側の件数・金額と比べて、差分を再通知する(スケジュール統合とファイル処理 のスケジュール・パラメータで前回の位置を持つ)
  2. 別のイベントを追加する:支払の作成(Payables Payment Created)を購読し、受信側に支払済みを通知する。冪等キーと版を、支払ではどうするか考える
  3. 受信側を直せない場合の設計:OIC の外に処理済みキーの表を持ち、通知の前に確認する。同時に 2 つのインスタンスが同じキーを確認したときの排他を考える
  4. 同じ請求書の処理を直列にする:並行処理による追い越しを、キューなどで直列にする案と、受信側の版の比較で防ぐ案を比べる
  5. 通知の集約:リトライ切れの通知を、1 時間ごとにまとめて送る設計を考える
  6. シミュレーションを拡張する:EVENTS に、請求書の取得が失敗するケースを加え、一時エラーと認証エラーでリトライの扱いを分ける

本サイトは学習目的で個人が作成した非公式の教材であり、Oracle Corporation およびその関連会社とは関係ありません。内容の正確性は保証しません。最新の仕様・試験範囲は必ず公式ドキュメントと Oracle University の情報で確認してください。

模擬テストの問題は公開されている出題範囲(Exam Topics)と公式ドキュメントをもとに独自に作成したもので、実際の試験問題ではありません。

Oracle、Java、MySQL は Oracle Corporation およびその子会社、関連会社の米国およびその他の国における登録商標です。その他の製品名・会社名は各社の商標です。

© 2026 Fusion ERP 実装ラボ