feat(16-05): implement hybrid batch update with updateContainers mutation

- Added IF node to check batch size (threshold: 5 containers)
- Small batches (<=5): use single updateContainers mutation (parallel, fast)
- Large batches (>5): use existing serial Execute Workflow loop
- Build Batch Update Mutation node constructs updateContainers GraphQL query
- Execute Batch Update with 120-second timeout for large image pulls
- Handle Batch Update Response maps results and updates Container ID Registry
- Format and send batch result via Telegram
- Both paths produce consistent result messaging

Workflow pushed to n8n successfully (HTTP 200).
This commit is contained in:
Lucas Berger
2026-02-09 10:37:05 -05:00
parent ed1a114d74
commit 9f6752720b
+214 -2
View File
@@ -3371,7 +3371,15 @@
"mode": "list", "mode": "list",
"value": "7AvTzLtKXM2hZTio92_mC" "value": "7AvTzLtKXM2hZTio92_mC"
}, },
"options": {} "options": {
"timeout": 120000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
}, },
"id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c", "id": "720b6da1-60e6-4ec4-8a5f-d403ad05994c",
"name": "Execute Batch Update", "name": "Execute Batch Update",
@@ -5291,6 +5299,137 @@
2600, 2600,
700 700
] ]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "check-count",
"leftValue": "={{ $json.totalCount }}",
"rightValue": 5,
"operator": {
"type": "number",
"operation": "lte"
}
}
],
"combinator": "and"
}
},
"id": "if-check-batch-size",
"name": "Check Batch Size",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2600,
2600
]
},
{
"parameters": {
"jsCode": "// Build updateContainers (plural) mutation for small batches\nconst batchState = $input.item.json;\nconst containers = batchState.containers || [];\nconst chatId = batchState.chatId;\n\n// Get Container ID Registry to look up PrefixedIDs\nconst staticData = $getWorkflowStaticData('global');\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\n// Build array of PrefixedIDs and name mapping\nconst ids = [];\nconst nameMap = {};\nconst notFound = [];\n\nfor (const container of containers) {\n const name = container.Name;\n const entry = registry[name];\n \n if (entry && entry.prefixedId) {\n ids.push(entry.prefixedId);\n nameMap[entry.prefixedId] = name;\n } else {\n // Container not in registry - this shouldn't happen but handle gracefully\n notFound.push(name);\n }\n}\n\nif (notFound.length > 0) {\n return {\n json: {\n error: true,\n errorMessage: `Containers not found in registry: ${notFound.join(', ')}`,\n chatId: chatId\n }\n };\n}\n\n// Build GraphQL mutation\nconst idsJson = JSON.stringify(ids);\nconst query = `mutation { docker { updateContainers(ids: ${idsJson}) { id state image imageId } } }`;\n\nreturn {\n json: {\n query: query,\n ids: ids,\n nameMap: nameMap,\n containerCount: ids.length,\n chatId: chatId,\n batchState: batchState\n }\n};\n"
},
"id": "code-build-batch-update-mutation",
"name": "Build Batch Update Mutation",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2800,
2500
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.UNRAID_HOST }}/graphql",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "x-api-key",
"value": "={{ $env.UNRAID_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\"query\": $json.query} }}",
"options": {
"timeout": 120000,
"response": {
"response": {
"errorRedirection": "continue",
"fullResponse": false
}
}
}
},
"id": "http-execute-batch-update",
"name": "Execute Batch Update",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3000,
2500
]
},
{
"parameters": {
"jsCode": "// Handle updateContainers mutation response\nconst response = $input.item.json;\nconst prevData = $('Build Batch Update Mutation').item.json;\nconst chatId = prevData.chatId;\nconst nameMap = prevData.nameMap;\n\n// Check for GraphQL errors\nif (response.errors) {\n return {\n json: {\n success: false,\n error: true,\n errorMessage: response.errors[0].message,\n chatId: chatId,\n batchMode: 'parallel'\n }\n };\n}\n\n// Extract updated containers\nconst updated = response.data?.docker?.updateContainers || [];\n\nif (updated.length === 0) {\n return {\n json: {\n success: false,\n error: true,\n errorMessage: 'No containers returned from updateContainers mutation',\n chatId: chatId,\n batchMode: 'parallel'\n }\n };\n}\n\n// Map results back to container names\nconst results = updated.map(container => ({\n name: nameMap[container.id] || container.id,\n imageId: container.imageId,\n state: container.state,\n success: true\n}));\n\n// Update Container ID Registry with new IDs\nconst staticData = $getWorkflowStaticData('global');\nconst registry = JSON.parse(staticData._containerIdRegistry || '{}');\n\nupdated.forEach(container => {\n const name = nameMap[container.id];\n if (name && registry[name]) {\n registry[name].prefixedId = container.id;\n registry[name].unraidId = container.id;\n registry[name].lastSeen = Date.now();\n }\n});\n\nstaticData._containerIdRegistry = JSON.stringify(registry);\n\nreturn {\n json: {\n success: true,\n batchMode: 'parallel',\n updatedCount: results.length,\n results: results,\n chatId: chatId\n }\n};\n"
},
"id": "code-handle-batch-update-response",
"name": "Handle Batch Update Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3200,
2500
]
},
{
"parameters": {
"jsCode": "// Format batch update result for Telegram message\nconst result = $input.item.json;\nconst chatId = result.chatId;\n\nif (result.error) {\n return {\n json: {\n chatId: chatId,\n text: `\u274c Batch update failed: ${result.errorMessage}`,\n success: false\n }\n };\n}\n\nconst mode = result.batchMode || 'parallel';\nconst count = result.updatedCount || 0;\nconst results = result.results || [];\n\n// Build summary message\nlet message = `\u2705 Batch update complete (${mode} mode)\\n\\n`;\nmessage += `Updated ${count} container${count !== 1 ? 's' : ''}:\\n`;\n\nresults.forEach(r => {\n const icon = r.success ? '\u2713' : '\u2717';\n message += ` ${icon} ${r.name}\\n`;\n});\n\nreturn {\n json: {\n chatId: chatId,\n text: message,\n success: true\n }\n};\n"
},
"id": "code-format-batch-result",
"name": "Format Batch Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3400,
2600
]
},
{
"parameters": {
"chatId": "={{ $json.chatId }}",
"text": "={{ $json.text }}",
"additionalFields": {}
},
"id": "telegram-send-batch-result",
"name": "Send Batch Result",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
3600,
2600
],
"credentials": {
"telegramApi": {
"id": "I0xTTiASl7C1NZhJ",
"name": "Telegram account"
}
}
} }
], ],
"connections": { "connections": {
@@ -7332,7 +7471,80 @@
] ]
] ]
}, },
"registry-update-bitmap-stop": {} "registry-update-bitmap-stop": {},
"code-prepare-update-all-batch": {
"main": [
[
{
"node": "if-check-batch-size",
"type": "main",
"index": 0
}
]
]
},
"if-check-batch-size": {
"main": [
[
{
"node": "code-build-batch-update-mutation",
"type": "main",
"index": 0
}
],
[
{
"node": "code-prepare-batch-loop",
"type": "main",
"index": 0
}
]
]
},
"code-build-batch-update-mutation": {
"main": [
[
{
"node": "http-execute-batch-update",
"type": "main",
"index": 0
}
]
]
},
"http-execute-batch-update": {
"main": [
[
{
"node": "code-handle-batch-update-response",
"type": "main",
"index": 0
}
]
]
},
"code-handle-batch-update-response": {
"main": [
[
{
"node": "code-format-batch-result",
"type": "main",
"index": 0
}
]
]
},
"code-format-batch-result": {
"main": [
[
{
"node": "telegram-send-batch-result",
"type": "main",
"index": 0
}
]
]
}
}, },
"pinData": {}, "pinData": {},
"settings": { "settings": {