plugin-cluster-manager 1.1.11 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/914.c0bce51908fd81d7.js +10 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +6 -5
- package/dist/locale/en-US.json +138 -124
- package/dist/locale/vi-VN.json +139 -125
- package/dist/locale/zh-CN.json +140 -125
- package/dist/server/actions/cluster-nodes.js +2 -6
- package/dist/server/actions/doctor.js +1 -5
- package/dist/server/actions/orchestrator.js +37 -0
- package/dist/server/actions/queue-mappings.js +107 -0
- package/dist/server/collections/worker-queue-mappings.js +106 -0
- package/dist/server/orchestrator/PackageManager.js +1 -8
- package/dist/server/orchestrator/docker-adapter.js +49 -27
- package/dist/server/plugin.js +10 -8
- package/dist/server/queue-scanner.js +141 -0
- package/dist/server/utils/node.js +30 -2
- package/package.json +46 -42
- package/src/client/AclCacheManager.tsx +292 -287
- package/src/client/CacheMonitor.tsx +2 -2
- package/src/client/ClusterManagerLayout.tsx +6 -0
- package/src/client/ClusterNodes.tsx +11 -4
- package/src/client/ContainerOrchestrator.tsx +186 -104
- package/src/client/Doctor.tsx +2 -2
- package/src/client/EventQueueMonitor.tsx +2 -2
- package/src/client/LockMonitor.tsx +2 -2
- package/src/client/NginxCacheManager.tsx +2 -2
- package/src/client/PackageInstaller.tsx +2 -2
- package/src/client/PluginOperations.tsx +2 -2
- package/src/client/QueueAssignment.tsx +355 -0
- package/src/client/RedisMonitor.tsx +3 -3
- package/src/client/TaskManager.tsx +194 -187
- package/src/client/WorkflowExecutions.tsx +243 -238
- package/src/client/utils.ts +1 -1
- package/src/client-v2/plugin.tsx +24 -0
- package/src/locale/en-US.json +138 -124
- package/src/locale/vi-VN.json +139 -125
- package/src/locale/zh-CN.json +140 -125
- package/src/server/actions/cluster-nodes.ts +4 -7
- package/src/server/actions/doctor.ts +11 -9
- package/src/server/actions/orchestrator.ts +54 -2
- package/src/server/actions/queue-mappings.ts +94 -0
- package/src/server/adapters/redis-node-registry.ts +126 -131
- package/src/server/collections/worker-queue-mappings.ts +85 -0
- package/src/server/orchestrator/PackageManager.ts +2 -10
- package/src/server/orchestrator/docker-adapter.ts +74 -37
- package/src/server/plugin.ts +15 -12
- package/src/server/queue-scanner.ts +154 -0
- package/src/server/utils/node.ts +48 -0
- package/dist/client/AclCacheManager.d.ts +0 -2
- package/dist/client/CacheMonitor.d.ts +0 -2
- package/dist/client/ClusterManagerLayout.d.ts +0 -2
- package/dist/client/ClusterNodes.d.ts +0 -2
- package/dist/client/ContainerOrchestrator.d.ts +0 -2
- package/dist/client/Doctor.d.ts +0 -2
- package/dist/client/EventQueueMonitor.d.ts +0 -2
- package/dist/client/LockMonitor.d.ts +0 -2
- package/dist/client/NginxCacheManager.d.ts +0 -2
- package/dist/client/PackageInstaller.d.ts +0 -2
- package/dist/client/PluginOperations.d.ts +0 -2
- package/dist/client/RedisMonitor.d.ts +0 -2
- package/dist/client/TaskManager.d.ts +0 -2
- package/dist/client/WorkflowExecutions.d.ts +0 -2
- package/dist/client/index.d.ts +0 -5
- package/dist/client/utils/clientSafeCache.d.ts +0 -3
- package/dist/client/utils/requestDedupInterceptor.d.ts +0 -2
- package/dist/client/utils.d.ts +0 -12
- package/dist/index.d.ts +0 -2
- package/dist/server/actions/acl-cache.d.ts +0 -53
- package/dist/server/actions/cache-monitor.d.ts +0 -33
- package/dist/server/actions/cluster-nodes.d.ts +0 -64
- package/dist/server/actions/doctor.d.ts +0 -82
- package/dist/server/actions/event-queue-monitor.d.ts +0 -13
- package/dist/server/actions/lock-monitor.d.ts +0 -19
- package/dist/server/actions/orchestrator.d.ts +0 -58
- package/dist/server/actions/package-manager.d.ts +0 -6
- package/dist/server/actions/plugin-operations.d.ts +0 -6
- package/dist/server/actions/redis-monitor.d.ts +0 -12
- package/dist/server/actions/tasks.d.ts +0 -7
- package/dist/server/actions/workflow-executions.d.ts +0 -7
- package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
- package/dist/server/adapters/redis-node-registry.d.ts +0 -12
- package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
- package/dist/server/collections/app.d.ts +0 -8
- package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
- package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
- package/dist/server/collections/cluster-manager-doctor-runs.d.ts +0 -3
- package/dist/server/collections/cluster-manager-doctor.d.ts +0 -18
- package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
- package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
- package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
- package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
- package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
- package/dist/server/collections/cluster-manager.d.ts +0 -22
- package/dist/server/collections/orchestrator-settings.d.ts +0 -59
- package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
- package/dist/server/collections/worker-orchestrator.d.ts +0 -22
- package/dist/server/collections/worker-packages-configs.d.ts +0 -3
- package/dist/server/collections/worker-packages.d.ts +0 -22
- package/dist/server/hooks/cacheInvalidationHooks.d.ts +0 -1
- package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +0 -2
- package/dist/server/orchestrator/PackageManager.d.ts +0 -39
- package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
- package/dist/server/orchestrator/index.d.ts +0 -4
- package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
- package/dist/server/orchestrator/leader-election.d.ts +0 -48
- package/dist/server/orchestrator/types.d.ts +0 -84
- package/dist/server/plugin.d.ts +0 -26
- package/dist/server/utils/node.d.ts +0 -6
- package/dist/server/utils/redis.d.ts +0 -29
- package/dist/server/utils/versionManager.d.ts +0 -10
- package/dist/shared/packages.d.ts +0 -23
- /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
|
@@ -1,131 +1,126 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import { scanKeys, getRedisClient } from '../utils/redis';
|
|
3
|
-
import { getLocalNodeId } from '../utils/node';
|
|
4
|
-
|
|
5
|
-
export class RedisNodeRegistry {
|
|
6
|
-
private timer: NodeJS.Timeout | null = null;
|
|
7
|
-
private readonly ttlSecs = 30; // 30 seconds TTL
|
|
8
|
-
private readonly intervalMs = 10000; // Heartbeat every 10 seconds
|
|
9
|
-
private readonly keyPrefix = 'cluster-manager:nodes:';
|
|
10
|
-
|
|
11
|
-
constructor(private app: any) {}
|
|
12
|
-
|
|
13
|
-
public start() {
|
|
14
|
-
if (this.timer) {
|
|
15
|
-
clearInterval(this.timer);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Initial heartbeat
|
|
19
|
-
this.heartbeat();
|
|
20
|
-
|
|
21
|
-
// Loop
|
|
22
|
-
this.timer = setInterval(() => {
|
|
23
|
-
this.heartbeat();
|
|
24
|
-
}, this.intervalMs);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public stop() {
|
|
28
|
-
if (this.timer) {
|
|
29
|
-
clearInterval(this.timer);
|
|
30
|
-
this.timer = null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
private async heartbeat() {
|
|
35
|
-
const redis = getRedisClient(this.app);
|
|
36
|
-
if (!redis) return;
|
|
37
|
-
|
|
38
|
-
// Unique identifier combining hostname, port, pid, mode, and appName to handle multiple workers on the same host
|
|
39
|
-
const port = process.env.APP_PORT || 'unknown';
|
|
40
|
-
const mode = process.env.WORKER_MODE || 'main';
|
|
41
|
-
const appName = process.env.APP_NAME || this.app.name || 'main';
|
|
42
|
-
const nodeId = getLocalNodeId(this.app);
|
|
43
|
-
const key = `${this.keyPrefix}${nodeId}`;
|
|
44
|
-
|
|
45
|
-
// Collect process-level metrics so any node can read another node's full info from Redis
|
|
46
|
-
const mem = process.memoryUsage();
|
|
47
|
-
|
|
48
|
-
const metadata = {
|
|
49
|
-
id: nodeId,
|
|
50
|
-
name: `${appName} (${os.hostname()})`,
|
|
51
|
-
hostname: os.hostname(),
|
|
52
|
-
appVersion: process.env.NOCOBASE_VERSION || process.version,
|
|
53
|
-
workerMode: mode,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.app.logger.error(`[RedisNodeRegistry] Error fetching nodes: ${err.message}`);
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { scanKeys, getRedisClient } from '../utils/redis';
|
|
3
|
+
import { getLocalNodeId } from '../utils/node';
|
|
4
|
+
|
|
5
|
+
export class RedisNodeRegistry {
|
|
6
|
+
private timer: NodeJS.Timeout | null = null;
|
|
7
|
+
private readonly ttlSecs = 30; // 30 seconds TTL
|
|
8
|
+
private readonly intervalMs = 10000; // Heartbeat every 10 seconds
|
|
9
|
+
private readonly keyPrefix = 'cluster-manager:nodes:';
|
|
10
|
+
|
|
11
|
+
constructor(private app: any) {}
|
|
12
|
+
|
|
13
|
+
public start() {
|
|
14
|
+
if (this.timer) {
|
|
15
|
+
clearInterval(this.timer);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Initial heartbeat
|
|
19
|
+
this.heartbeat();
|
|
20
|
+
|
|
21
|
+
// Loop
|
|
22
|
+
this.timer = setInterval(() => {
|
|
23
|
+
this.heartbeat();
|
|
24
|
+
}, this.intervalMs);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public stop() {
|
|
28
|
+
if (this.timer) {
|
|
29
|
+
clearInterval(this.timer);
|
|
30
|
+
this.timer = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async heartbeat() {
|
|
35
|
+
const redis = getRedisClient(this.app);
|
|
36
|
+
if (!redis) return;
|
|
37
|
+
|
|
38
|
+
// Unique identifier combining hostname, port, pid, mode, and appName to handle multiple workers on the same host
|
|
39
|
+
const port = process.env.APP_PORT || 'unknown';
|
|
40
|
+
const mode = process.env.WORKER_MODE || 'main';
|
|
41
|
+
const appName = process.env.APP_NAME || this.app.name || 'main';
|
|
42
|
+
const nodeId = getLocalNodeId(this.app);
|
|
43
|
+
const key = `${this.keyPrefix}${nodeId}`;
|
|
44
|
+
|
|
45
|
+
// Collect process-level metrics so any node can read another node's full info from Redis
|
|
46
|
+
const mem = process.memoryUsage();
|
|
47
|
+
|
|
48
|
+
const metadata = {
|
|
49
|
+
id: nodeId,
|
|
50
|
+
name: `${appName} (${os.hostname()})`,
|
|
51
|
+
hostname: os.hostname(),
|
|
52
|
+
appVersion: process.env.NOCOBASE_VERSION || process.version,
|
|
53
|
+
workerMode: mode,
|
|
54
|
+
appRole: process.env.APP_ROLE,
|
|
55
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
56
|
+
pid: process.pid,
|
|
57
|
+
url: process.env.APP_PUBLIC_URL || null,
|
|
58
|
+
available: true,
|
|
59
|
+
lastHeartbeatAt: Date.now(),
|
|
60
|
+
status: 'online', // Implicitly online since it just reported
|
|
61
|
+
// Full node details (replicated from the `current` action shape)
|
|
62
|
+
// so that any node can serve the "current" endpoint for the APP node
|
|
63
|
+
nodeDetails: {
|
|
64
|
+
node: {
|
|
65
|
+
hostname: os.hostname(),
|
|
66
|
+
pid: process.pid,
|
|
67
|
+
nodeVersion: process.version,
|
|
68
|
+
platform: process.platform,
|
|
69
|
+
arch: process.arch,
|
|
70
|
+
uptime: process.uptime(),
|
|
71
|
+
workerMode: mode,
|
|
72
|
+
appPort: port,
|
|
73
|
+
clusterMode: process.env.CLUSTER_MODE || '',
|
|
74
|
+
},
|
|
75
|
+
memory: {
|
|
76
|
+
rss: mem.rss,
|
|
77
|
+
heapUsed: mem.heapUsed,
|
|
78
|
+
heapTotal: mem.heapTotal,
|
|
79
|
+
external: mem.external,
|
|
80
|
+
arrayBuffers: mem.arrayBuffers || 0,
|
|
81
|
+
},
|
|
82
|
+
os: {
|
|
83
|
+
totalMemory: os.totalmem(),
|
|
84
|
+
freeMemory: os.freemem(),
|
|
85
|
+
cpuCount: os.cpus().length,
|
|
86
|
+
loadAvg: os.loadavg(),
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await redis.sendCommand(['SET', key, JSON.stringify(metadata), 'EX', this.ttlSecs.toString()]);
|
|
93
|
+
} catch (err: any) {
|
|
94
|
+
this.app.logger.error(`[RedisNodeRegistry] Heartbeat failed: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async getNodes(): Promise<any[]> {
|
|
99
|
+
const redis = getRedisClient(this.app);
|
|
100
|
+
if (!redis) return [];
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const rawKeys = await scanKeys(redis, `${this.keyPrefix}*`);
|
|
104
|
+
if (rawKeys.length === 0) return [];
|
|
105
|
+
|
|
106
|
+
const values = await redis.sendCommand(['MGET', ...rawKeys]);
|
|
107
|
+
|
|
108
|
+
const nodes: any[] = [];
|
|
109
|
+
if (Array.isArray(values)) {
|
|
110
|
+
for (const val of values) {
|
|
111
|
+
if (val) {
|
|
112
|
+
try {
|
|
113
|
+
nodes.push(JSON.parse(val));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// bad JSON, ignore
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return nodes;
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
this.app.logger.error(`[RedisNodeRegistry] Error fetching nodes: ${err.message}`);
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection: workerQueueMappings
|
|
3
|
+
*
|
|
4
|
+
* Maps queue names to worker stacks (orchestratorStacks).
|
|
5
|
+
* When a stack has assigned queues, the orchestrator adapter sets
|
|
6
|
+
* WORKER_MODE=<comma-separated-queue-names> on new containers.
|
|
7
|
+
*
|
|
8
|
+
* If no mappings exist for a stack, WORKER_MODE=* is preserved
|
|
9
|
+
* (backwards compatibility).
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: 'workerQueueMappings',
|
|
13
|
+
autoGenId: true,
|
|
14
|
+
createdAt: true,
|
|
15
|
+
updatedAt: true,
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
name: 'queueName',
|
|
19
|
+
type: 'string',
|
|
20
|
+
unique: true,
|
|
21
|
+
interface: 'input',
|
|
22
|
+
uiSchema: {
|
|
23
|
+
title: 'Queue Name',
|
|
24
|
+
'x-component': 'Input',
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'label',
|
|
30
|
+
type: 'string',
|
|
31
|
+
interface: 'input',
|
|
32
|
+
uiSchema: {
|
|
33
|
+
title: 'Label',
|
|
34
|
+
'x-component': 'Input',
|
|
35
|
+
description: 'Human-readable display name',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'description',
|
|
40
|
+
type: 'text',
|
|
41
|
+
interface: 'textarea',
|
|
42
|
+
uiSchema: {
|
|
43
|
+
title: 'Description',
|
|
44
|
+
'x-component': 'Input.TextArea',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'type',
|
|
49
|
+
type: 'string',
|
|
50
|
+
interface: 'select',
|
|
51
|
+
defaultValue: 'event-queue',
|
|
52
|
+
uiSchema: {
|
|
53
|
+
title: 'Source Type',
|
|
54
|
+
'x-component': 'Select',
|
|
55
|
+
enum: [
|
|
56
|
+
{ value: 'event-queue', label: 'EventQueue' },
|
|
57
|
+
{ value: 'redis-list', label: 'Redis List' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'stackId',
|
|
63
|
+
type: 'integer',
|
|
64
|
+
interface: 'select',
|
|
65
|
+
uiSchema: {
|
|
66
|
+
title: 'Assigned Stack',
|
|
67
|
+
'x-component': 'Select',
|
|
68
|
+
'x-component-props': {
|
|
69
|
+
allowClear: true,
|
|
70
|
+
placeholder: 'Unassigned (worker runs all queues)',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: 'enabled',
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
defaultValue: true,
|
|
78
|
+
interface: 'checkbox',
|
|
79
|
+
uiSchema: {
|
|
80
|
+
title: 'Enabled',
|
|
81
|
+
'x-component': 'Checkbox',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { getRedisClient } from '../utils/redis';
|
|
3
|
-
import { getLocalNodeId } from '../utils/node';
|
|
3
|
+
import { getLocalNodeId, getLocalRole } from '../utils/node';
|
|
4
4
|
import { promises as fsp } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import Application from '@nocobase/server';
|
|
@@ -67,15 +67,7 @@ function redactUrl(value: string): string {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function getCurrentRole(): Exclude<TargetRole, 'all'> {
|
|
70
|
-
|
|
71
|
-
return process.env.APP_ROLE;
|
|
72
|
-
}
|
|
73
|
-
if (process.env.SKILL_HUB_SANDBOX === 'true') {
|
|
74
|
-
return 'sandbox';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const workerMode = process.env.WORKER_MODE || 'main';
|
|
78
|
-
return workerMode === 'worker' || workerMode === 'task' || workerMode === '*' ? 'worker' : 'app';
|
|
70
|
+
return getLocalRole();
|
|
79
71
|
}
|
|
80
72
|
|
|
81
73
|
function formatCommand(command: string, args: string[]): string {
|
|
@@ -17,9 +17,7 @@ function getDockerode() {
|
|
|
17
17
|
try {
|
|
18
18
|
Dockerode = require('dockerode');
|
|
19
19
|
} catch {
|
|
20
|
-
throw new Error(
|
|
21
|
-
'[DockerAdapter] "dockerode" package not found. Install it: yarn add dockerode',
|
|
22
|
-
);
|
|
20
|
+
throw new Error('[DockerAdapter] "dockerode" package not found. Install it: yarn add dockerode');
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
23
|
return Dockerode;
|
|
@@ -58,11 +56,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
58
56
|
const containers = await this.docker.listContainers({
|
|
59
57
|
all: true,
|
|
60
58
|
filters: {
|
|
61
|
-
label: [
|
|
62
|
-
`${LABEL_STACK}=${stack.name}`,
|
|
63
|
-
`${LABEL_MANAGED}=true`,
|
|
64
|
-
...this.buildLabelFilters(this.workerLabels),
|
|
65
|
-
],
|
|
59
|
+
label: [`${LABEL_STACK}=${stack.name}`, `${LABEL_MANAGED}=true`, ...this.buildLabelFilters(this.workerLabels)],
|
|
66
60
|
},
|
|
67
61
|
});
|
|
68
62
|
|
|
@@ -105,9 +99,20 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
105
99
|
if (diff > 0) {
|
|
106
100
|
// Scale UP
|
|
107
101
|
let targetNetworks = stack.networks && stack.networks.length > 0 ? stack.networks : [];
|
|
108
|
-
|
|
102
|
+
const targetNetworkMode = stack.networkMode;
|
|
109
103
|
let targetEnvVars = this.buildEnvArray(stack.envVars);
|
|
110
104
|
let targetVolumes = stack.volumes || [];
|
|
105
|
+
// Default the worker image to whatever the app container is running, so
|
|
106
|
+
// workers stay version-locked with the app even when the stack record
|
|
107
|
+
// has a stale/empty image. An explicit stack.image still wins.
|
|
108
|
+
let targetImage = stack.image;
|
|
109
|
+
// Inherit the app container's startup command/entrypoint so workers boot
|
|
110
|
+
// identically (e.g. source-tarball extraction + `yarn start`). Without
|
|
111
|
+
// this, a worker created from the bare image runs the image default
|
|
112
|
+
// command, skips the app's bootstrap, never finishes booting, and never
|
|
113
|
+
// registers a heartbeat — so it never appears in Cluster Nodes.
|
|
114
|
+
let inheritedCmd: string[] | undefined;
|
|
115
|
+
let inheritedEntrypoint: string[] | undefined;
|
|
111
116
|
|
|
112
117
|
// Auto-detect current container's configuration to inherit networks and env vars
|
|
113
118
|
try {
|
|
@@ -115,50 +120,70 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
115
120
|
const myContainerId = os.hostname();
|
|
116
121
|
const myContainer = this.docker.getContainer(myContainerId);
|
|
117
122
|
const myInfo = await myContainer.inspect();
|
|
118
|
-
|
|
123
|
+
|
|
124
|
+
// Inherit the app container's image when the stack does not pin one
|
|
125
|
+
if (!targetImage && myInfo?.Config?.Image) {
|
|
126
|
+
targetImage = myInfo.Config.Image;
|
|
127
|
+
console.log('[DockerAdapter] Inherited image from app container:', targetImage);
|
|
128
|
+
}
|
|
129
|
+
|
|
119
130
|
// Always inherit Networks so worker can communicate with main app
|
|
120
131
|
if (myInfo?.NetworkSettings?.Networks) {
|
|
121
132
|
const inheritedNetworks = Object.keys(myInfo.NetworkSettings.Networks);
|
|
122
133
|
targetNetworks = Array.from(new Set([...inheritedNetworks, ...targetNetworks]));
|
|
123
134
|
console.log('[DockerAdapter] Inherited networks:', targetNetworks);
|
|
124
135
|
}
|
|
125
|
-
|
|
136
|
+
|
|
126
137
|
// Inherit Environment Variables and merge with stack.envVars
|
|
127
138
|
if (myInfo?.Config?.Env) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
const envDict: Record<string, string> = {};
|
|
140
|
+
myInfo.Config.Env.forEach((e: string) => {
|
|
141
|
+
const idx = e.indexOf('=');
|
|
142
|
+
if (idx !== -1) {
|
|
143
|
+
envDict[e.substring(0, idx)] = e.substring(idx + 1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
// Overwrite with explicitly defined env vars
|
|
147
|
+
Object.assign(envDict, stack.envVars || {});
|
|
148
|
+
|
|
149
|
+
targetEnvVars = Object.entries(envDict).map(([k, v]) => `${k}=${v}`);
|
|
139
150
|
}
|
|
140
151
|
// Inherit Volumes (Binds)
|
|
141
152
|
if (myInfo?.HostConfig?.Binds) {
|
|
142
153
|
const inheritedBinds = myInfo.HostConfig.Binds as string[];
|
|
143
154
|
targetVolumes = Array.from(new Set([...inheritedBinds, ...targetVolumes]));
|
|
144
155
|
}
|
|
156
|
+
// Inherit the startup Cmd/Entrypoint so the worker runs the same bootstrap
|
|
157
|
+
// as the app container (used only when the stack pins no explicit command).
|
|
158
|
+
if (Array.isArray(myInfo?.Config?.Cmd) && myInfo.Config.Cmd.length > 0) {
|
|
159
|
+
inheritedCmd = myInfo.Config.Cmd as string[];
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(myInfo?.Config?.Entrypoint) && myInfo.Config.Entrypoint.length > 0) {
|
|
162
|
+
inheritedEntrypoint = myInfo.Config.Entrypoint as string[];
|
|
163
|
+
}
|
|
145
164
|
} catch (e: any) {
|
|
146
165
|
// Ignore error if not running in a container or cannot inspect
|
|
147
166
|
console.error('[DockerAdapter] Failed to inherit container config:', e.message);
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
// Automatically separate logs for workers to prevent log interleaving with the main app
|
|
151
|
-
const hasLoggerBase = targetEnvVars.some(e => e.startsWith('LOGGER_BASE_PATH='));
|
|
170
|
+
const hasLoggerBase = targetEnvVars.some((e) => e.startsWith('LOGGER_BASE_PATH='));
|
|
152
171
|
if (!hasLoggerBase) {
|
|
153
172
|
targetEnvVars.push(`LOGGER_BASE_PATH=/app/nocobase/storage/logs/${stack.name}`);
|
|
154
173
|
}
|
|
155
174
|
|
|
175
|
+
if (!targetImage) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`[DockerAdapter] No image configured for stack "${stack.name}" and the app container image could not be determined.`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
156
181
|
for (let i = 0; i < diff; i++) {
|
|
157
182
|
const suffix = `${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
|
|
158
183
|
const containerName = `${stack.name}-${suffix}`;
|
|
159
184
|
|
|
160
185
|
const createOpts: any = {
|
|
161
|
-
Image:
|
|
186
|
+
Image: targetImage,
|
|
162
187
|
name: containerName,
|
|
163
188
|
Env: targetEnvVars,
|
|
164
189
|
Labels: {
|
|
@@ -174,6 +199,15 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
174
199
|
|
|
175
200
|
if (stack.command) {
|
|
176
201
|
createOpts.Cmd = ['/bin/sh', '-c', stack.command];
|
|
202
|
+
} else {
|
|
203
|
+
// No explicit command: replay the app container's bootstrap so the
|
|
204
|
+
// worker boots NocoBase the same way and registers a heartbeat.
|
|
205
|
+
if (inheritedEntrypoint) {
|
|
206
|
+
createOpts.Entrypoint = inheritedEntrypoint;
|
|
207
|
+
}
|
|
208
|
+
if (inheritedCmd) {
|
|
209
|
+
createOpts.Cmd = inheritedCmd;
|
|
210
|
+
}
|
|
177
211
|
}
|
|
178
212
|
|
|
179
213
|
if (stack.resourceLimits?.memory) {
|
|
@@ -190,7 +224,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
190
224
|
createOpts.HostConfig.SecurityOpt = ['no-new-privileges:true'];
|
|
191
225
|
|
|
192
226
|
const container = await this.docker.createContainer(createOpts);
|
|
193
|
-
|
|
227
|
+
|
|
194
228
|
// Connect to additional networks before starting
|
|
195
229
|
if (targetNetworks.length > 0) {
|
|
196
230
|
const startIndex = targetNetworkMode ? 0 : 1;
|
|
@@ -199,7 +233,9 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
199
233
|
const net = this.docker.getNetwork(targetNetworks[i]);
|
|
200
234
|
await net.connect({ Container: container.id });
|
|
201
235
|
} catch (err: any) {
|
|
202
|
-
console.warn(
|
|
236
|
+
console.warn(
|
|
237
|
+
`[DockerAdapter] Failed to connect container ${container.id} to network ${targetNetworks[i]}: ${err.message}`,
|
|
238
|
+
);
|
|
203
239
|
}
|
|
204
240
|
}
|
|
205
241
|
}
|
|
@@ -209,9 +245,7 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
209
245
|
}
|
|
210
246
|
} else if (diff < 0) {
|
|
211
247
|
// Scale DOWN — remove newest first (LIFO)
|
|
212
|
-
const sorted = running.sort(
|
|
213
|
-
(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
|
|
214
|
-
);
|
|
248
|
+
const sorted = running.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
215
249
|
const toRemove = sorted.slice(0, Math.abs(diff));
|
|
216
250
|
|
|
217
251
|
for (const c of toRemove) {
|
|
@@ -334,14 +368,17 @@ export class DockerAdapter implements IOrchestratorAdapter {
|
|
|
334
368
|
.split(',')
|
|
335
369
|
.map((part) => part.trim())
|
|
336
370
|
.filter(Boolean)
|
|
337
|
-
.reduce(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
371
|
+
.reduce(
|
|
372
|
+
(acc, part) => {
|
|
373
|
+
const [key, ...valueParts] = part.split('=');
|
|
374
|
+
const value = valueParts.join('=');
|
|
375
|
+
if (key?.trim() && value?.trim()) {
|
|
376
|
+
acc[key.trim()] = value.trim();
|
|
377
|
+
}
|
|
378
|
+
return acc;
|
|
379
|
+
},
|
|
380
|
+
{} as Record<string, string>,
|
|
381
|
+
);
|
|
345
382
|
}
|
|
346
383
|
|
|
347
384
|
private labelsMatch(labels: Record<string, string>, expected: Record<string, string>): boolean {
|
package/src/server/plugin.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { redisActions } from './actions/redis-monitor';
|
|
|
7
7
|
import { aclCacheActions, createAclCacheMiddleware } from './actions/acl-cache';
|
|
8
8
|
import { clusterActions, readLocalLogs } from './actions/cluster-nodes';
|
|
9
9
|
import { getRedisClient } from './utils/redis';
|
|
10
|
-
import { getLocalNodeId } from './utils/node';
|
|
10
|
+
import { getLocalNodeId, isWorkerMode } from './utils/node';
|
|
11
11
|
import { eventQueueActions } from './actions/event-queue-monitor';
|
|
12
12
|
import { lockActions } from './actions/lock-monitor';
|
|
13
13
|
import { cacheMonitorActions } from './actions/cache-monitor';
|
|
@@ -16,6 +16,7 @@ import { RedisNodeRegistry } from './adapters/redis-node-registry';
|
|
|
16
16
|
import { RedisLockAdapter } from './adapters/redis-lock-adapter';
|
|
17
17
|
import { orchestratorActions } from './actions/orchestrator';
|
|
18
18
|
import { pluginOperationsActions } from './actions/plugin-operations';
|
|
19
|
+
import { queueMappingsActions } from './actions/queue-mappings';
|
|
19
20
|
import type { IOrchestratorAdapter } from './orchestrator/types';
|
|
20
21
|
import { DockerAdapter } from './orchestrator/docker-adapter';
|
|
21
22
|
import { K8sAdapter } from './orchestrator/k8s-adapter';
|
|
@@ -56,14 +57,12 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
56
57
|
(this.app as any).on('afterStart', () => {
|
|
57
58
|
this.nodeRegistry?.start();
|
|
58
59
|
|
|
59
|
-
// Automatically install packages on boot for worker nodes
|
|
60
|
-
const mode = process.env.WORKER_MODE || 'main';
|
|
60
|
+
// Automatically install packages on boot for worker / sandbox nodes
|
|
61
61
|
const isWorker =
|
|
62
|
-
|
|
63
|
-
mode === 'task' ||
|
|
64
|
-
mode === '*' ||
|
|
62
|
+
isWorkerMode(process.env.WORKER_MODE) ||
|
|
65
63
|
process.env.APP_ROLE === 'worker' ||
|
|
66
|
-
process.env.APP_ROLE === 'sandbox'
|
|
64
|
+
process.env.APP_ROLE === 'sandbox' ||
|
|
65
|
+
process.env.SKILL_HUB_SANDBOX === 'true';
|
|
67
66
|
if (isWorker) {
|
|
68
67
|
setTimeout(async () => {
|
|
69
68
|
try {
|
|
@@ -125,9 +124,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
125
124
|
|
|
126
125
|
// Workflow hook to trace executing node
|
|
127
126
|
this.app.db.on('executions.afterSave', async (model: any) => {
|
|
128
|
-
|
|
129
|
-
const isWorker = mode === 'worker' || mode === 'task' || mode === '*';
|
|
130
|
-
if (isWorker) {
|
|
127
|
+
if (isWorkerMode(process.env.WORKER_MODE)) {
|
|
131
128
|
const id = model.get('id');
|
|
132
129
|
const redis = getRedisClient(this.app);
|
|
133
130
|
if (id && redis) {
|
|
@@ -332,6 +329,12 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
332
329
|
actions: pluginOperationsActions,
|
|
333
330
|
});
|
|
334
331
|
|
|
332
|
+
// Queue Mappings (queue-to-worker-stack assignments)
|
|
333
|
+
this.app.resourcer.define({
|
|
334
|
+
name: 'workerQueueMappings',
|
|
335
|
+
actions: queueMappingsActions,
|
|
336
|
+
});
|
|
337
|
+
|
|
335
338
|
// Install ACL cache middleware inside the ACL chain so cached permissions are not overwritten.
|
|
336
339
|
const aclCacheMiddleware = createAclCacheMiddleware(this.app);
|
|
337
340
|
(this.app as any).acl.use(aclCacheMiddleware, {
|
|
@@ -380,6 +383,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
380
383
|
'orchestratorStacks:*',
|
|
381
384
|
'workerPackages:*',
|
|
382
385
|
'clusterManagerPlugins:*',
|
|
386
|
+
'workerQueueMappings:*',
|
|
383
387
|
],
|
|
384
388
|
});
|
|
385
389
|
|
|
@@ -558,8 +562,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
558
562
|
}
|
|
559
563
|
|
|
560
564
|
private isWorkerOnlyNode(): boolean {
|
|
561
|
-
|
|
562
|
-
return workerMode === 'worker' || workerMode === 'task' || workerMode === '*';
|
|
565
|
+
return isWorkerMode(process.env.WORKER_MODE);
|
|
563
566
|
}
|
|
564
567
|
}
|
|
565
568
|
|