OIC 演習2:ビジネスイベントで外部システムへ通知する
この演習で身につくこと
Section titled “この演習で身につくこと”- ビジネスイベントの購読(有効化、ロール、フィルタ、接続の制約)を設計・設定できる
- 「イベントを受ける → 最新状態を取り直す → 外部へ REST で通知する」統合を組み立てられる
- 冪等キー、リトライ、再送信、突合を、障害のパターンごとに設計できる
- 重複・順不同・受信側の停止・応答の喪失といったケースで、何が起きるかを説明できる
- 環境がなくても、設計シートとモック API のシミュレーションで同じ考え方を体験できる
イベントの仕組みと一覧は ビジネスイベントとアウトバウンド連携のパターン、アダプタの設定は 接続とアダプタ、フォルト処理と再送信は 障害処理・テスト・監視・移送 を参照してください。
冪等性と再送の考え方
Section titled “冪等性と再送の考え方”通知は「1 回だけ・順番どおり」とは限らない
Section titled “通知は「1 回だけ・順番どおり」とは限らない”公式ドキュメントの範囲では、ビジネスイベントの配信回数や順序の保証は確認できていません。確認できている挙動は次のとおりです。
- OIC は失敗したメッセージを 自動では再送信しません(手動のみ)
- 統合を無効化するとき購読を削除せず、6 時間以内 に有効化し直すと、その間のイベントが再送されます
- 手動の再送信は、統合を 最初から 実行し直します
そのため、重複・欠落・順不同のどれも起こり得る 前提で設計します。
設計の 4 原則
Section titled “設計の 4 原則”| 原則 | 内容 | この演習での実装 |
|---|---|---|
| 通知は合図 | 「何が起きたか」の対象だけを使い、内容は 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 環境あり/なし)”シナリオ(架空)
Section titled “シナリオ(架空)”| 項目 | 内容 |
|---|---|
| 会社 | サンプル商事株式会社(架空) |
| 起点 | Fusion の AP で、請求書が承認された(Payables Invoice Approved) |
| 連携先 | 予算管理システム(架空)。承認済みの請求書を受け取り、予算の消化額に計上する |
| 目的 | 承認から数分以内に連携する。ただし、重複・順不同・一時停止があっても 二重計上しない |
| 対象 | JP01 営業単位(架空。自社のテスト用 BU に読み替える)の請求書 |
要件
- 請求書が承認されたら、予算管理システムへ REST で通知する
- 通知する内容は、通知時点の最新の請求書の状態(金額・仕入先・承認状態)にする
- 同じ請求書の通知が 2 回届いても、予算には 1 回しか計上しない
- 予算管理システムが一時的に止まっていても、自動で数回やり直す。それでもだめなら運用担当に通知する
- 運用担当が、失敗した通知を後から再送できる
- 取りこぼしを、1 日 1 回の突合で見つけられるようにする(設計のみ)
受信側の API(架空)
POST /api/invoice-approvedIdempotency-Key: 300100002:2026-02-21T02:07:00+00:00Content-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/OIC 環境ありルート
Section titled “Fusion/OIC 環境ありルート”- 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 を再現できないため、机上演習とシミュレーションで補います
環境なしルート
Section titled “環境なしルート”OIC や Fusion がなくても、次の 2 つで考え方を学べます。
- 机上演習:要件から設計シート(表)を作り、解答例と照合します
- モック API でのシミュレーション:Python(標準ライブラリのみ)で、イベントの処理、重複、受信側の停止、応答の喪失、再送信を再現し、冪等性の有無で結果がどう変わるかを見ます(練習用 API プレイグラウンド と同じモック API を使います)
ステップ1 イベントを有効にする
Section titled “ステップ1 イベントを有効にする”- Setup and Maintenance で、プロファイル・オプションを管理するタスクを開きます
- AP: Enable Business Events(コード
ORA_AP_ENABLE_BUSINESS_EVENTS)を、サイト・レベルでYにします - 対象のイベントが一覧にあることを、Fusion の
/soa-infra/PublicEvent/catalogで確認します
ステップ2 接続を作る
Section titled “ステップ2 接続を作る”- プロジェクト(例:
SMP_AP_EVENT)を作成します - ERP Cloud 接続
SMP_ERP_CLOUD:ロールは トリガーと呼出し(イベントの受信に使うトリガーと、請求書の取得に使う呼出しの両方が必要なため)。セキュリティ・ポリシーと統合ユーザーを設定して Test します - REST 接続
SMP_BUDGET_REST:ロールは呼出し。受信側のベース URL と認証方式を設定します
ステップ3 イベントで起動する統合を作る
Section titled “ステップ3 イベントで起動する統合を作る”- パターンは Application を選び、名前と識別子を
AP_INV_APPROVED_NOTIFYにします - トリガーに
SMP_ERP_CLOUDを使い、Receive Business Events raised within ERP Cloud を選びます - 請求書の承認のイベントを選びます
- 必要ならフィルタを書きます。対象を JP01 に絞る条件を、ワイヤー・ペイロード(アクティビティ・ストリームの
TriggerWirePayload_tTextPayload)の構造で書きます。ペイロードに BU の項目がなければ、次のステップの取得後の Switch で判定します
ステップ4 最新の請求書を取得する
Section titled “ステップ4 最新の請求書を取得する”- 呼出しに
SMP_ERP_CLOUDを追加し、Query, Create, Update, or Delete Information → Business (REST) Resource を選び、請求書リソースの 1 件取得(get)を選びます。子リソースは選びません - マップで、イベントのペイロードから 請求書を特定できる項目(請求書 ID など)を、取得の入力に対応付けます。項目名は、実際のワイヤー・ペイロードで確認します
- 取得する項目は、
fieldsで必要なものに絞ります(InvoiceId、InvoiceNumber、Supplier、InvoiceAmount、InvoiceCurrency、ApprovalStatus、BusinessUnit、LastUpdateDate)
ステップ5 対象かどうかを判定する
Section titled “ステップ5 対象かどうかを判定する”- Switch で、取り直した
ApprovalStatusが承認済みで、BusinessUnitが JP01 であることを確認します - 対象外なら、Logger で理由を記録して正常終了します(通知は送りません)
ステップ6 冪等キーを作る
Section titled “ステップ6 冪等キーを作る”- Assign で、変数
idempotencyKeyにInvoiceIdとLastUpdateDateを:でつないだ値を入れます(式ビルダーのconcatを使います) - キーに、実行のたびに変わる値(現在時刻、乱数)を入れません。再送信しても同じキーになることが目的です
ステップ7 外部システムへ通知する
Section titled “ステップ7 外部システムへ通知する”- 呼出しに
SMP_BUDGET_RESTを追加し、メソッドは POST、要求と応答の JSON のサンプルを設定します - 要求ヘッダーに
Idempotency-Keyを追加し、マップでステップ6のキーを対応付けます - 本文は、ステップ4で取り直した請求書から作ります(イベントのペイロードは使いません)
- 受信側が冪等でない場合は、REST アダプタの 504 の自動リトライを無効にすることを検討します
ステップ8 リトライと通知を作る
Section titled “ステップ8 リトライと通知を作る”- ステップ7の呼出しを Scope で囲みます
- 障害処理・テスト・監視・移送 の「リトライのフロー」を使い、一時エラー(接続失敗、タイムアウト、5xx)だけ を 3 回までやり直します。待機は回数に応じて延ばします。回数が尽きたら、ループの後ろで Notification と Throw New Fault を実行します
- 恒久エラー(4xx) は、スコープのフォルト・ハンドラの中で、リトライせずに Notification で通知し、Re-throw Fault で外側へ投げ直します。一時エラーと恒久エラーを分ける項目は、テストのときにアクティビティ・ストリームで実際のフォルトの内容を見て決めます
- 統合の グローバル・フォルトハンドラ は、Notification のあとに Re-throw Fault にします。Stop は使いません(失敗が Errors に出なくなるため)
ステップ9 追跡できるようにする
Section titled “ステップ9 追跡できるようにする”- ビジネス識別子 に、請求書番号(プライマリ)と請求書 ID を設定します
- Logger で、冪等キーと受信側の応答(201 か 200 か)を記録します
ステップ10 有効化して購読を確認する
Section titled “ステップ10 有効化して購読を確認する”- 統合を有効化します。有効化した時点で、Fusion に購読が登録されます
- Fusion の
/soa-infra/PublicEvent/subscriptionsで、endpointURLが今使っている OIC を指していること、状態が有効であること、フィルタが想定どおりであることを確認します
ステップ11 テストする
Section titled “ステップ11 テストする”| ケース | 操作 | 期待する結果 |
|---|---|---|
| 正常 | Fusion で請求書を作成・検証し、承認する | インスタンスが成功。受信側に 201 で計上される |
| 重複 | 同じキーの要求を、受信側へ curl で 2 回送る | 2 回目は 200(重複)。計上は 1 回 |
| 一時停止 | 受信側を止める(または接続の URL を一時的に誤った値にする)。請求書を承認する | リトライされ、リトライ切れで通知と Errors への表示がある |
| 再送信 | 受信側を復旧し、Errors からそのインスタンスを再送信する | 最初から実行され、201 か 200 で終わる。二重計上されない |
| 恒久エラー | 負の金額の請求書(クレジット・メモ)を承認する | リトライせず、すぐ通知される |
| 対象外 | 別の BU の請求書を承認する | 起動しない、または対象外として記録して終了する |
確認は、Observability > Instances(アクティビティ・ストリームでキーと応答を確認)、Errors、受信側の記録の 3 か所で行います。
ステップ12 運用の手順を決める
Section titled “ステップ12 運用の手順を決める”- 購読の扱い:統合を無効化するとき、Delete Event Subscription を選ぶか決めます。選ばずに 6 時間以内に有効化し直すと、その間のイベントが再送されます
- 環境の切替:テスト環境の Fusion に、本番の OIC を指す購読が残っていないかを
endpointURLで確認します - 突合:日次で、承認済み請求書を REST(
LastUpdateDateで差分取得)で取り、受信側の件数・金額と比べます。差分は再通知します - Errors の確認:失敗を放置しないよう、確認の担当と頻度を決めます
確認ポイント
Section titled “確認ポイント”- AP のビジネスイベントを有効にし、購読が登録されている(
endpointURL・状態・フィルタを確認した) - 通知の本文を、イベントのペイロードではなく、取り直した最新状態から作っている
- 冪等キーが
InvoiceId:LastUpdateDateで、再実行しても同じ値になる - 同じ通知を 2 回送っても、受信側の計上は 1 回になる
- 一時エラーだけをリトライし、400 はリトライせずに通知される
- リトライ切れが Errors に残り、復旧後の再送信で正しく終わる
- グローバル・フォルトハンドラが、通知のあとに Re-throw している(Stop で隠していない)
- 購読の扱い(Delete Event Subscription、環境ごとの
endpointURL)を運用手順に書いた - 突合の頻度・照合キー・差分の補正方法を書いた
机上演習(環境なしルート①)
Section titled “机上演習(環境なしルート①)”課題1:上のシナリオと要件から、設計シート を表で作ってください。
- 表1 イベントと起動条件
- 表2 処理フロー
- 表3 冪等性の設計
- 表4 エラーと再送の設計
- 表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です - 最後に、受信側の予算の消化額を、重複なしの期待値と比べます
- 次のスクリプトを
event_sim.pyとして保存します - 環境変数を設定して実行します
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:ビジネスイベント通知の「冪等性・再送」を、練習用モック 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 base64import jsonimport osimport sysimport timeimport urllib.errorimport urllib.requestfrom 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()最初の実行(キーと版の両方で判定)の出力例を表示
受信側:キー判定 あり、版判定 あり[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 の側の工夫だけでは二重計上を防げません
よくあるエラーと対処
Section titled “よくあるエラーと対処”| 症状 | 原因と対処 |
|---|---|
| イベントが届かない | イベントを有効にしたか、統合が有効か、統合ユーザーに SOAOperator があるか、購読(/soa-infra/PublicEvent/subscriptions)が登録されているかを確認する。プライベート・エンドポイント経由は不可。許可リストがあれば Fusion の送信元 IP を許可する |
| フィルタが常に false になる | マッパーのペイロードではなく、ワイヤー・ペイロードの構造で書く。result と項目の間の Value、名前空間ごとに別の接頭辞、文字列の引用符を確認する |
| テスト環境のイベントが本番の統合を起動する | 購読の endpointURL を確認し、不要な購読を削除する。環境の複製や切替のあとに必ず確認する |
| Errors から再送信できない | 同期統合のエラーは再送信できない。非同期でも、破棄(Discard)したものと、10 回に達したものはできない |
| 二重に計上される | 受信側がキーも版も見ていない。キーに現在時刻や乱数を入れて、毎回変わっている。504 の自動リトライで二重に届いている |
| 古い内容で上書きされる | 受信側に版の比較がない。イベントの内容をそのまま送っている |
| 通知メールが大量に届く | 失敗のたびに送っている。リトライ切れのときだけ送る、または集約する |
| 購読を残したまま無効化していて、再有効化後に大量に届く | 6 時間以内の再有効化で、その間のイベントが再送される仕様。受信側が冪等であれば問題ない。不要なら無効化時に購読を削除する |
- 突合の統合を作る:日次のスケジュール統合で、承認済み請求書を
LastUpdateDateで差分取得し、受信側の件数・金額と比べて、差分を再通知する(スケジュール統合とファイル処理 のスケジュール・パラメータで前回の位置を持つ) - 別のイベントを追加する:支払の作成(Payables Payment Created)を購読し、受信側に支払済みを通知する。冪等キーと版を、支払ではどうするか考える
- 受信側を直せない場合の設計:OIC の外に処理済みキーの表を持ち、通知の前に確認する。同時に 2 つのインスタンスが同じキーを確認したときの排他を考える
- 同じ請求書の処理を直列にする:並行処理による追い越しを、キューなどで直列にする案と、受信側の版の比較で防ぐ案を比べる
- 通知の集約:リトライ切れの通知を、1 時間ごとにまとめて送る設計を考える
- シミュレーションを拡張する:
EVENTSに、請求書の取得が失敗するケースを加え、一時エラーと認証エラーでリトライの扱いを分ける
公式ドキュメント
Section titled “公式ドキュメント”- Oracle ERP Cloud Adapter Capabilities(対象イベントの一覧)
- Trigger Request Page(ERP Cloud Adapter)
- Best Practices for Creating Event Filters
- Business Event Troubleshooting Issues
- Failed Messages Are Not Automatically Resubmitted
- Assign Required Roles to an Integration User
- Resubmit Failed Messages
- Add Global Fault Handling to Integrations
- Enable Business Events(REST API for Oracle Fusion Cloud Financials 26D)
本サイトは学習目的で個人が作成した非公式の教材であり、Oracle Corporation およびその関連会社とは関係ありません。内容の正確性は保証しません。最新の仕様・試験範囲は必ず公式ドキュメントと Oracle University の情報で確認してください。
模擬テストの問題は公開されている出題範囲(Exam Topics)と公式ドキュメントをもとに独自に作成したもので、実際の試験問題ではありません。
Oracle、Java、MySQL は Oracle Corporation およびその子会社、関連会社の米国およびその他の国における登録商標です。その他の製品名・会社名は各社の商標です。
© 2026 Fusion ERP 実装ラボ