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>
130 lines
12 KiB
Markdown
130 lines
12 KiB
Markdown
# 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側のアイテム管理オーバーヘッド(各ノードでアイテム配列をコピーする分のメモリ)を削減できる可能性がある
|
||
|
||
いずれも設計変更を伴うため、着手前に方針をユーザーと確認すること。
|