ken_nogi/NodeSrv/apps/org-master-sync/n8n-merge-master-workflow-design.md
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

12 KiB
Raw Permalink Blame History

org-sync: ② merge-master ワークフロー設計書

対象: n8n workflow iRBAQquKr9411ENG(Webhookトリガー、https://n8n32.next-hd.net/webhook/org-sync-merge-master) 記録時点: 2026-08-17(export/import方式改修後、66ード) 関連サブワークフロー: logic-merge-master-plan(id: asKvC5LSVg3q8tuU)

1. 全体像

LINEWORKS社員台帳とプリザンター実ユーザーを突き合わせ、社員・組織マスタ管理テーブル(正サイト)へCreate/Update/退職処理を反映する。実行順は大きく4ブロック:

[A] 入力データ収集
  ↓
[B] logic-merge-master-plan サブワークフロー呼び出し(マージ判定)
  ↓
[C] 副サイトバックアップ(全クリア→export→import)
  ↓
[D] 選択肢マスタ解決 → Create/Update/退職 実行(3分岐)

Bまでは軽い処理、Cは今回OOMでクラッシュした区間、Dが最終的な書込。C(バックアップ)とD(本処理)は副サイトImportノードを境に直列であり、Cが終わらないとDへ進まない設計。

2. ブロックA: 入力データ収集(Webhook起動 〜 副サイトSiteId取得)

ノード 種別 処理
Webhook起動 Webhook POSTで起動。認証ヘッダーX-API-Key
PLEASANTER_BASE_URL_TEST取得PLEASANTER_USERTYPE_MASTER_SITE_ID_TEST取得(4連) Data Table Get workflow_config_valuesから設定値を1キーずつ直列取得(複数行一括取得による後続反復実行バグの回避策)
lineworks_users全行取得 Data Table Get lineworks_users(619行、rawJson列にLINEWORKS社員データ)をreturnAll:trueで一括取得
lineworks_usersまとめ Aggregate 619アイテムを1アイテム(rows配列)に集約(後続の反復実行を防ぐ定石)
users配列復元 Set rows.map(r => JSON.parse(r.rawJson))で619件分のrawJson文字列を全部その場でJSONパースusers配列を作る
meta取得 Data Table Get lineworks_snapshot_metaからfetchedAt取得
マスタ全件取得 HTTP Request(Pagination) POST .../api/items/{正サイトSiteId}/get。Offsetベースのページングで619件をmaxRequests:20まで取得
マスタ展開 SplitOut Response.Data(619件)を619アイテムに展開
マスタHashフラット化 Set 619アイテムそれぞれに対して1回ずつ実行(n8nの入力アイテム単位実行)。ClassHash等5つのHashをトップレベルへ展開マージ
existingItemsまとめ Aggregate 619件を1アイテム(rows)に再集約 → existingItems
実ユーザー全件取得 HTTP Request(Pagination) POST .../api/users/get。プリザンター実ユーザー全件
実ユーザー展開pleasanterUsersまとめ SplitOut→Aggregate 上と同じ展開→再集約パターン
email overrides取得overridesまとめoverridesマップ化 Data Table Get→Aggregate→Set メール重複時の解決用オーバーライド表を{emailLocal: pleasanterUserId}のマップに変換
サブワークフロー入力組立 Set users/existingItems/pleasanterUsers/emailOverrides/fetchedAtの5項目を1オブジェクトにまとめる。619件×2セットの配列をまるごと1つのJSONアイテムに保持した状態でここを通過

この時点でメモリ上に「619件のLINEWORKS社員生データ」「619件の正サイト全カラム展開データ」「N件の実ユーザーデータ」を同時に保持している

3. ブロックB: マージ判定(logic-merge-master-plan サブワークフロー)

logic-merge-master-plan実行(Execute Workflow)が呼ぶサブワークフローは2ードのみ:

  • Execute Workflow Trigger
  • planMerge計算(Codeード。apps/org-master-sync/src/commands/merge-master.jsplanMerge()をJSに移植したもの)

判定ロジック(概要):

  1. LINEWORKS側usersを1件ずつ走査
    • ClassA(社員ID)一致 → 次点でClassB(メール)ローカル部一致、で正サイト既存行(existingItems)を検索
    • プリザンター実ユーザー(pleasanterUsers)をメールで突き合わせ。同一ローカル部に複数候補いる場合はemailOverridesで解決、無ければneedsReviewへ積む(自動処理をスキップし要人手確認としてマーク)
    • 既存行あればupdates、無ければcreatesへ、生年月日・入社日は「新しい値が無く旧値もダミー日付でなければ1899/12/31でクリアする」という日付クリア専用ロジックあり
  2. 正サイトにいるがLINEWORKS側で見えなくなったプリザンター実ユーザー(mirroredPleasanterUserIdsに含まれない)をupdatesまたはcreatesへ追加登録(在籍フラグCheck062:false)
  3. 正サイトの既存行のうちCheck062:true(LINEWORKS在籍フラグ)なのに今回のLINEWORKSスナップショットに現れない行をretirementsへ(退職処理対象)

出力: {creates, updates, retirements, needsReview} の4配列。この計算自体はn8nコンテナ内のCodeード1回実行で完結し、比較的軽い(619件規模のループだが単純なオブジェクト構築のみ)。

4. ブロックC: 副サイトバックアップ(2026-08-17改修、OOMクラッシュ発生区間)

logic-merge-master-plan実行の直後、メイン処理(件数集計以降)より先に直列で走る:

副サイトSiteId取得 → 副サイト全クリア → 正サイトExport → Import用ファイル変換 → 副サイトImport → (件数集計 / needsReview展開 / 退職対象展開準備 の3分岐)
ノード 種別 処理
副サイトSiteId取得 Data Table Get workflow_config_valuesからPLEASANTER_BACKUP_SITE_ID_TEST(=1362)取得
副サイト全クリア HTTP Request POST .../api/items/1362/bulkdelete{All:true, PhysicalDelete:true}2026-08-17の実行はここで停止しクラッシュ(lastNodeExecuted)
正サイトExport HTTP Request POST .../api/items/{正サイトSiteId}/exportExport.Columnsに全62列を明示指定、Type:"csv"でCSV文字列を取得
Import用ファイル変換 Convert to File(toText) sourceProperty:"Response.Content"のCSV文字列をバイナリ化(binaryPropertyName:"data")
副サイトImport HTTP Request(multipart) POST .../api/items/1362/importparameters(JSON)とfile(バイナリCSV)の2パートで619件をimport

旧方式(〜2026-08-16)は副サイトへ丸ごと反映(bulkupsert、619件のJSON配列を1リクエストで送信)だった。2026-08-17にexport/importへ置き換え済み(単体でのcurl直叩き検証では0.5秒+4秒と高速だった)。

5. ブロックD: 選択肢マスタ解決 → Create/Update/退職 実行

副サイトImportから3方向に分岐、いずれもlogic-merge-master-plan実行の出力(creates/updates/needsReview/retirements)を参照する。

5-1. needsReview分岐

needsReview展開(SplitOut) → needsReview行組立(Set) → needsReview保存(Data Table merge_needs_reviewへInsert)。要人手確認の重複メール候補を記録するのみ、正サイトへの書込は無い。

5-2. メイン分岐(件数集計 → Create/Update)

  1. 件数集計(Set): creates/updates/retirements/needsReviewの件数を数えるだけ
  2. 役職・職級・利用権限タイプの3種マスタをそれぞれマスタ取得(Pagination)→展開(SplitOut)→まとめ(Aggregate)→マップ化(Set、{Title: ResultId})の同一パターンで直列に解決(件数は少数8/6/4件なので軽い)
  3. 欠落タイトル抽出(Set): creates+updatesの中で使われている役職・職級・利用権限タイプ名のうち、マスタに未登録のものを抽出
  4. 役職・職級・利用権限タイプそれぞれについて欠落展開(SplitOut)→新規作成(HTTP Request /create)→新規ペア組立(Set)→新規まとめ(Aggregate)で欠落分を新規登録
  5. 最終選択肢マップ完成(Set): 既存マップ+新規登録分をマージした最終マップ(positionMap/levelMap/userTypeMap)
  6. Create対象Update対象結合(Set): createsupdatestargets配列に結合
  7. 対象展開(SplitOut): targetsを1件ずつに展開
  8. テスト用Limit(1件): ここで初めて1件に絞られる(現状 有効・disabled:false)
  9. 選択肢フィールド解決(Set): 役職名等の文字列を最終選択肢マップ完成のIDへ変換
  10. マスタCreate_Update実行(HTTP Request): itemIdの有無でupdate/createを出し分け、ClassHash/NumHash/DateHash/DescriptionHash/CheckHashへ振り分けて送信

5-3. 退職分岐

退職対象展開準備(Set) → 退職対象展開(SplitOut) → 退職者Update実行(HTTP Request、現状disabled:trueで無効化中)。Check062:falseClassZ:"無"DateA更新のみの軽量Update。

6. 2026-08-17の暴走事故と原因

Webhook実行が副サイト全クリアードで停止、n8n実行履歴のエラーは

"Workflow did not finish, possible out-of-memory issue"

このワークフローの構造的な問題: テスト用Limit(1件)は5-2の手順8、つまりCreate/Update実行の直前にしか効かない。それより前の全工程(ブロックA/B/C、619件のLINEWORKS展開・619件の正サイト展開・実ユーザー展開・マージ判定・バックアップのexport/import)は**「1件テスト」であっても常にフル619件規模で実行される**。n8nコンテナのメモリ制限(768MiB)に対し、以下が同時にメモリへ積み上がる:

  • users配列復元: 619件のLINEWORKS社員オブジェクト
  • existingItemsまとめ: 619件の正サイト全62列展開データ
  • pleasanterUsersまとめ: 実ユーザー全件データ
  • サブワークフロー入力組立logic-merge-master-plan実行: 上記を1つのJSONペイロードとして子ワークフローへコピー渡し
  • 正サイトExport副サイトImport: 619件のCSV文字列(348KB相当)をさらに保持

bulkupsert(旧方式)を2026-08-17にexport/import方式へ置き換えても症状(OOM)が再現したことから、暴走の主因は「バックアップ処理そのもの」ではなく、ブロックA〜Cを通じて複数回複製される619件データの累積メモリ使用量である可能性が高い。単一ステップの置き換えでは解決しない構造的課題。

7. 対策の方向性(未実装・検討のみ)

メモリ増設ができない制約下での案:

  1. Limitをブロックの入口側まで前倒しする: 対象展開直前ではなく、マスタ全件取得/実ユーザー全件取得のページング自体を絞る(ただし社員IDの絞り込み条件が必要で、Pleasanter API側のView条件設計が要る)
  2. ブロックCとブロックA/B/Dを分離する: バックアップ(export/import)は本処理と論理的に独立しているため、別ワークフロー・別トリガーに切り出し、メイン処理のメモリと競合しないタイミング(例: 深夜バッチ)で走らせる
  3. 619件を保持し続けない設計へ: Aggregate→SplitOutの往復を減らし、可能な箇所はn8nの標準ページング機構やバッチ実行(splitInBatches)で「常に少数件だけをメモリに置く」ストリーミング型処理に書き換える(改修規模は大きい)
  4. Codeードへの一本化: 現状Set/Aggregate/SplitOutを多用したーコード的組み方をしているが、ブロックA・Dの一部をCodeード(JS一括処理)にまとめるとn8n側のアイテム管理オーバーヘッド(各ノードでアイテム配列をコピーする分のメモリ)を削減できる可能性がある

いずれも設計変更を伴うため、着手前に方針をユーザーと確認すること。