plugin-cluster-manager 1.1.11 → 1.1.13
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.5dc1105cf3ada6a6.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/ClusterManagerLayout.tsx +6 -0
- package/src/client/ClusterNodes.tsx +8 -1
- package/src/client/ContainerOrchestrator.tsx +184 -102
- package/src/client/QueueAssignment.tsx +355 -0
- package/src/client/TaskManager.tsx +194 -187
- package/src/client/WorkflowExecutions.tsx +243 -238
- 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 +3 -7
- package/src/server/actions/doctor.ts +2 -6
- package/src/server/actions/orchestrator.ts +54 -2
- package/src/server/actions/queue-mappings.ts +94 -0
- 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 +12 -10
- 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
|
@@ -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';
|
|
@@ -57,11 +58,8 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
57
58
|
this.nodeRegistry?.start();
|
|
58
59
|
|
|
59
60
|
// Automatically install packages on boot for worker nodes
|
|
60
|
-
const mode = process.env.WORKER_MODE || 'main';
|
|
61
61
|
const isWorker =
|
|
62
|
-
|
|
63
|
-
mode === 'task' ||
|
|
64
|
-
mode === '*' ||
|
|
62
|
+
isWorkerMode(process.env.WORKER_MODE) ||
|
|
65
63
|
process.env.APP_ROLE === 'worker' ||
|
|
66
64
|
process.env.APP_ROLE === 'sandbox';
|
|
67
65
|
if (isWorker) {
|
|
@@ -125,9 +123,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
125
123
|
|
|
126
124
|
// Workflow hook to trace executing node
|
|
127
125
|
this.app.db.on('executions.afterSave', async (model: any) => {
|
|
128
|
-
|
|
129
|
-
const isWorker = mode === 'worker' || mode === 'task' || mode === '*';
|
|
130
|
-
if (isWorker) {
|
|
126
|
+
if (isWorkerMode(process.env.WORKER_MODE)) {
|
|
131
127
|
const id = model.get('id');
|
|
132
128
|
const redis = getRedisClient(this.app);
|
|
133
129
|
if (id && redis) {
|
|
@@ -332,6 +328,12 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
332
328
|
actions: pluginOperationsActions,
|
|
333
329
|
});
|
|
334
330
|
|
|
331
|
+
// Queue Mappings (queue-to-worker-stack assignments)
|
|
332
|
+
this.app.resourcer.define({
|
|
333
|
+
name: 'workerQueueMappings',
|
|
334
|
+
actions: queueMappingsActions,
|
|
335
|
+
});
|
|
336
|
+
|
|
335
337
|
// Install ACL cache middleware inside the ACL chain so cached permissions are not overwritten.
|
|
336
338
|
const aclCacheMiddleware = createAclCacheMiddleware(this.app);
|
|
337
339
|
(this.app as any).acl.use(aclCacheMiddleware, {
|
|
@@ -380,6 +382,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
380
382
|
'orchestratorStacks:*',
|
|
381
383
|
'workerPackages:*',
|
|
382
384
|
'clusterManagerPlugins:*',
|
|
385
|
+
'workerQueueMappings:*',
|
|
383
386
|
],
|
|
384
387
|
});
|
|
385
388
|
|
|
@@ -558,8 +561,7 @@ export class PluginClusterManagerServer extends Plugin {
|
|
|
558
561
|
}
|
|
559
562
|
|
|
560
563
|
private isWorkerOnlyNode(): boolean {
|
|
561
|
-
|
|
562
|
-
return workerMode === 'worker' || workerMode === 'task' || workerMode === '*';
|
|
564
|
+
return isWorkerMode(process.env.WORKER_MODE);
|
|
563
565
|
}
|
|
564
566
|
}
|
|
565
567
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueScanner — discovers all registered queues in the system.
|
|
3
|
+
*
|
|
4
|
+
* Two sources:
|
|
5
|
+
* 1. EventQueue events (registered via app.eventQueue.subscribe)
|
|
6
|
+
* 2. Redis List-based queues (convention *:plugin-*:queue)
|
|
7
|
+
*
|
|
8
|
+
* Used by the Queue Assignment UI to let admins map queues to worker stacks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Application } from '@nocobase/server';
|
|
12
|
+
import { getRedisClient } from './utils/redis';
|
|
13
|
+
|
|
14
|
+
export type QueueItem = {
|
|
15
|
+
name: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
type: 'event-queue' | 'redis-list';
|
|
19
|
+
pending: number | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const KNOWN_QUEUE_LABELS: Record<string, { label: string; description: string }> = {
|
|
23
|
+
'workflow:process': {
|
|
24
|
+
label: 'Workflow',
|
|
25
|
+
description: 'Process workflow executions (plugin-workflow)',
|
|
26
|
+
},
|
|
27
|
+
'async-task:process': {
|
|
28
|
+
label: 'Async Tasks',
|
|
29
|
+
description: 'Execute async tasks (plugin-async-task-manager)',
|
|
30
|
+
},
|
|
31
|
+
'knowledge-base:document-vectorize': {
|
|
32
|
+
label: 'Document Vectorization',
|
|
33
|
+
description: 'Vectorize knowledge base documents (plugin-knowledge-base)',
|
|
34
|
+
},
|
|
35
|
+
'git-review:process': {
|
|
36
|
+
label: 'Git Review',
|
|
37
|
+
description: 'AI code review jobs (plugin-git-manager)',
|
|
38
|
+
},
|
|
39
|
+
'build-guide:process': {
|
|
40
|
+
label: 'Build Guide',
|
|
41
|
+
description: 'Build user guide pages (plugin-build-guide-block)',
|
|
42
|
+
},
|
|
43
|
+
'build-ui-template:process': {
|
|
44
|
+
label: 'Build UI Template',
|
|
45
|
+
description: 'Build UI template pages (plugin-build-ui-template)',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Redis key patterns for List-based queues (same as event-queue-monitor.ts) */
|
|
50
|
+
const REDIS_QUEUE_PATTERNS = ['*:plugin-git-manager:review:queue', '*:plugin-build-guide-block:build:queue'];
|
|
51
|
+
|
|
52
|
+
function describeRedisQueueKey(key: string): { label: string; description: string } {
|
|
53
|
+
const parts = String(key).split(':');
|
|
54
|
+
const plugin = parts[parts.length - 3] || 'unknown';
|
|
55
|
+
const queue = parts[parts.length - 2] || key;
|
|
56
|
+
return {
|
|
57
|
+
label: `${queue} (${plugin})`,
|
|
58
|
+
description: `Redis List queue from ${plugin}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Discover all queues from EventQueue subscribers.
|
|
64
|
+
*/
|
|
65
|
+
function scanEventQueue(app: Application): QueueItem[] {
|
|
66
|
+
const eq = (app as any).eventQueue;
|
|
67
|
+
if (!eq || !eq.events) return [];
|
|
68
|
+
|
|
69
|
+
const events: Map<string, { concurrency?: number; interval?: number; shared?: boolean }> = eq.events;
|
|
70
|
+
const items: QueueItem[] = [];
|
|
71
|
+
|
|
72
|
+
for (const [channel] of events.entries()) {
|
|
73
|
+
const known = KNOWN_QUEUE_LABELS[channel];
|
|
74
|
+
items.push({
|
|
75
|
+
name: channel,
|
|
76
|
+
label: known?.label ?? channel,
|
|
77
|
+
description: known?.description ?? `EventQueue channel: ${channel}`,
|
|
78
|
+
type: 'event-queue',
|
|
79
|
+
pending: null,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Discover Redis List-based queues via SCAN.
|
|
88
|
+
*/
|
|
89
|
+
async function scanRedisQueues(app: Application): Promise<QueueItem[]> {
|
|
90
|
+
const redis = getRedisClient(app);
|
|
91
|
+
if (!redis) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
const items: QueueItem[] = [];
|
|
97
|
+
|
|
98
|
+
for (const pattern of REDIS_QUEUE_PATTERNS) {
|
|
99
|
+
try {
|
|
100
|
+
const keys: string[] = await redis.sendCommand(['SCAN', '0', 'MATCH', pattern, 'COUNT', '200']);
|
|
101
|
+
const keyList: string[] = typeof keys[1]?.length === 'number' ? keys[1] : [];
|
|
102
|
+
|
|
103
|
+
for (const key of keyList) {
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
|
|
107
|
+
const desc = describeRedisQueueKey(key);
|
|
108
|
+
let pending = 0;
|
|
109
|
+
try {
|
|
110
|
+
pending = Number(await redis.sendCommand(['LLEN', key])) || 0;
|
|
111
|
+
} catch {
|
|
112
|
+
pending = 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
items.push({
|
|
116
|
+
name: key,
|
|
117
|
+
label: desc.label,
|
|
118
|
+
description: desc.description,
|
|
119
|
+
type: 'redis-list',
|
|
120
|
+
pending,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// SCAN not supported or permission denied
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return items;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Full queue scan — merges EventQueue + Redis results.
|
|
133
|
+
*/
|
|
134
|
+
export async function scanQueues(app: Application): Promise<{ queues: QueueItem[]; total: number }> {
|
|
135
|
+
const eventQueues = scanEventQueue(app);
|
|
136
|
+
const redisQueues = await scanRedisQueues(app);
|
|
137
|
+
|
|
138
|
+
// Deduplicate: if a queue name appears in both sources, prefer EventQueue
|
|
139
|
+
const seenNames = new Set<string>();
|
|
140
|
+
const merged: QueueItem[] = [];
|
|
141
|
+
|
|
142
|
+
for (const q of eventQueues) {
|
|
143
|
+
merged.push(q);
|
|
144
|
+
seenNames.add(q.name);
|
|
145
|
+
}
|
|
146
|
+
for (const q of redisQueues) {
|
|
147
|
+
if (!seenNames.has(q.name)) {
|
|
148
|
+
merged.push(q);
|
|
149
|
+
seenNames.add(q.name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { queues: merged, total: merged.length };
|
|
154
|
+
}
|
package/src/server/utils/node.ts
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
|
|
3
|
+
export type NodeRole = 'app' | 'worker' | 'sandbox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determine whether a WORKER_MODE value denotes a queue-processing worker
|
|
7
|
+
* (i.e. a node that does NOT serve HTTP), following NocoBase v2.1.x semantics:
|
|
8
|
+
* '' → app node (serves HTTP + every queue)
|
|
9
|
+
* '!' → app node (serves HTTP only)
|
|
10
|
+
* '*' → worker (serves all queues, no HTTP)
|
|
11
|
+
* 'a:b,c:d' → worker (serves the listed queue topics, no HTTP)
|
|
12
|
+
* '-' → transient subprocess (not a long-lived worker)
|
|
13
|
+
* Legacy literals 'worker' / 'task' are still tolerated for older deployments.
|
|
14
|
+
* A combined value containing '!' (e.g. '!,workflow:process') still serves
|
|
15
|
+
* HTTP, so it is treated as an app node.
|
|
16
|
+
*/
|
|
17
|
+
export function isWorkerMode(workerMode?: string): boolean {
|
|
18
|
+
const mode = (workerMode ?? process.env.WORKER_MODE ?? '').trim();
|
|
19
|
+
if (!mode || mode === 'main' || mode === 'app') return false;
|
|
20
|
+
if (mode === '-') return false;
|
|
21
|
+
const topics = mode
|
|
22
|
+
.split(',')
|
|
23
|
+
.map((t) => t.trim())
|
|
24
|
+
.filter(Boolean);
|
|
25
|
+
if (topics.includes('!')) return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve a node role from its descriptor. APP_ROLE (explicit) wins, then an
|
|
31
|
+
* explicit sandbox flag, then WORKER_MODE parsing. Works for both the local
|
|
32
|
+
* process (pass process.env values) and remote node records read from Redis.
|
|
33
|
+
*/
|
|
34
|
+
export function getNodeRoleFrom(opts: { workerMode?: string; appRole?: string; isSandbox?: boolean }): NodeRole {
|
|
35
|
+
if (opts.appRole === 'app' || opts.appRole === 'worker' || opts.appRole === 'sandbox') {
|
|
36
|
+
return opts.appRole;
|
|
37
|
+
}
|
|
38
|
+
if (opts.isSandbox) return 'sandbox';
|
|
39
|
+
return isWorkerMode(opts.workerMode) ? 'worker' : 'app';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolve the role of the current Node.js process from its environment. */
|
|
43
|
+
export function getLocalRole(): NodeRole {
|
|
44
|
+
return getNodeRoleFrom({
|
|
45
|
+
workerMode: process.env.WORKER_MODE,
|
|
46
|
+
appRole: process.env.APP_ROLE,
|
|
47
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
3
51
|
/**
|
|
4
52
|
* Generate a universally unique identifier for this specific Node.js process.
|
|
5
53
|
* Combines app name, worker mode, hostname, port, and PID to ensure uniqueness
|
package/dist/client/Doctor.d.ts
DELETED
package/dist/client/index.d.ts
DELETED
package/dist/client/utils.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared i18n hook for the cluster-manager plugin.
|
|
3
|
-
*/
|
|
4
|
-
export declare function useT(): (key: string) => string;
|
|
5
|
-
/**
|
|
6
|
-
* Format bytes into human-readable string (B, KB, MB, GB).
|
|
7
|
-
*/
|
|
8
|
-
export declare function formatBytes(bytes: number): string;
|
|
9
|
-
/**
|
|
10
|
-
* Format seconds into human-readable uptime string (e.g., "2d 5h 30m").
|
|
11
|
-
*/
|
|
12
|
-
export declare function formatUptime(seconds: number): string;
|
package/dist/index.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { Context } from '@nocobase/actions';
|
|
2
|
-
/**
|
|
3
|
-
* In-memory ACL stats counter.
|
|
4
|
-
* Tracks total checks, cache hits/misses per role:resource:action.
|
|
5
|
-
*/
|
|
6
|
-
export interface AclCacheStats {
|
|
7
|
-
totalChecks: number;
|
|
8
|
-
cacheHits: number;
|
|
9
|
-
cacheMisses: number;
|
|
10
|
-
startedAt: string;
|
|
11
|
-
detailByRole: Record<string, {
|
|
12
|
-
checks: number;
|
|
13
|
-
hits: number;
|
|
14
|
-
misses: number;
|
|
15
|
-
}>;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Middleware that caches the permission object computed by the ACL middleware.
|
|
19
|
-
* Install via: app.acl.use(aclCacheMiddleware, { tag: 'aclCache', before: 'core', after: 'allow-manager' })
|
|
20
|
-
*
|
|
21
|
-
* FIX: Previously monkey-patched ctx.app.acl.can per-request, which is a race condition
|
|
22
|
-
* because acl is a shared singleton across concurrent requests. Now we use a post-check
|
|
23
|
-
* approach that reads from cache first and writes after the ACL middleware runs, without
|
|
24
|
-
* ever replacing the shared acl.can method.
|
|
25
|
-
*/
|
|
26
|
-
export declare function createAclCacheMiddleware(app: any): (ctx: Context, next: () => Promise<void>) => Promise<void>;
|
|
27
|
-
export declare const aclCacheActions: {
|
|
28
|
-
/**
|
|
29
|
-
* GET /clusterManagerAclCache:stats
|
|
30
|
-
* Returns ACL cache hit/miss statistics
|
|
31
|
-
*/
|
|
32
|
-
stats(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
33
|
-
/**
|
|
34
|
-
* GET /clusterManagerAclCache:listKeys
|
|
35
|
-
* Lists all cached ACL permission keys
|
|
36
|
-
*/
|
|
37
|
-
listKeys(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* POST /clusterManagerAclCache:clear
|
|
40
|
-
* Clear all ACL cache entries and reset stats
|
|
41
|
-
*/
|
|
42
|
-
clear(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
* POST /clusterManagerAclCache:resetStats
|
|
45
|
-
* Reset the in-memory ACL stats counters
|
|
46
|
-
*/
|
|
47
|
-
resetStats(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
48
|
-
/**
|
|
49
|
-
* POST /clusterManagerAclCache:clearRole
|
|
50
|
-
* Clear ACL cache entries for a specific role
|
|
51
|
-
*/
|
|
52
|
-
clearRole(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
53
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Context } from '@nocobase/actions';
|
|
2
|
-
export declare const cacheMonitorActions: {
|
|
3
|
-
/**
|
|
4
|
-
* GET /clusterManagerCacheMgr:stores
|
|
5
|
-
* List all registered cache stores and their config
|
|
6
|
-
*/
|
|
7
|
-
stores(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
8
|
-
/**
|
|
9
|
-
* GET /clusterManagerCacheMgr:caches
|
|
10
|
-
* List all created named caches
|
|
11
|
-
*/
|
|
12
|
-
caches(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* GET /clusterManagerCacheMgr:redisMemory
|
|
15
|
-
* Get Redis memory usage for cache keys
|
|
16
|
-
*/
|
|
17
|
-
redisMemory(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* POST /clusterManagerCacheMgr:flushAll
|
|
20
|
-
* Flush all caches via CacheManager
|
|
21
|
-
*/
|
|
22
|
-
flushAll(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* GET /clusterManagerCacheMgr:nginxCacheStatus
|
|
25
|
-
* Detect if Nginx is installed, locate conf, and auto-load cache paths
|
|
26
|
-
*/
|
|
27
|
-
nginxCacheStatus(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
28
|
-
/**
|
|
29
|
-
* POST /clusterManagerCacheMgr:clearNginxCache
|
|
30
|
-
* Clear physical cache files or send an HTTP Purge request
|
|
31
|
-
*/
|
|
32
|
-
clearNginxCache(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
33
|
-
};
|