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

130 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.js`の`planMerge()`を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}/export`、`Export.Columns`に全62列を明示指定、`Type:"csv"`でCSV文字列を取得 |
| `Import用ファイル変換` | Convert to File(`toText`) | `sourceProperty:"Response.Content"`のCSV文字列をバイナリ化(`binaryPropertyName:"data"`) |
| `副サイトImport` | HTTP Request(multipart) | `POST .../api/items/1362/import`。`parameters`(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): `creates`と`updates`を`targets`配列に結合
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:false`・`ClassZ:"無"`・`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側のアイテム管理オーバーヘッド(各ノードでアイテム配列をコピーする分のメモリ)を削減できる可能性がある
いずれも設計変更を伴うため、着手前に方針をユーザーと確認すること。