plugin-cluster-manager 1.1.15 → 1.1.17
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/dist/client/index.js +1 -1
- package/dist/client-v2/376.cd1d86e85a50088e.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +6 -6
- package/dist/locale/en-US.json +16 -6
- package/dist/locale/vi-VN.json +16 -6
- package/dist/locale/zh-CN.json +16 -6
- package/dist/server/actions/cluster-nodes.js +44 -11
- package/dist/server/actions/doctor.js +73 -7
- package/dist/server/actions/event-queue-monitor.js +33 -3
- package/dist/server/actions/orchestrator.js +48 -32
- package/dist/server/actions/queue-mappings.js +1 -0
- package/dist/server/actions/tasks.js +8 -8
- package/dist/server/adapters/redis-event-queue-adapter.js +188 -0
- package/dist/server/adapters/redis-node-registry.js +44 -10
- package/dist/server/collections/orchestrator-stacks.js +6 -0
- package/dist/server/collections/worker-queue-mappings.js +1 -1
- package/dist/server/orchestrator/PackageManager.js +47 -12
- package/dist/server/plugin.js +37 -6
- package/dist/server/queue-scanner.js +54 -34
- package/dist/server/utils/node.js +3 -7
- package/dist/server/utils/redis.js +37 -9
- package/dist/shared/worker-processes.js +233 -0
- package/package.json +1 -1
- package/src/client/ClusterNodes.tsx +76 -10
- package/src/client/ContainerOrchestrator.tsx +146 -8
- package/src/client/QueueAssignment.tsx +10 -2
- package/src/locale/en-US.json +16 -6
- package/src/locale/vi-VN.json +16 -6
- package/src/locale/zh-CN.json +16 -6
- package/src/server/__tests__/worker-processes.test.ts +42 -0
- package/src/server/actions/cluster-nodes.ts +43 -8
- package/src/server/actions/doctor.ts +77 -0
- package/src/server/actions/event-queue-monitor.ts +34 -3
- package/src/server/actions/orchestrator.ts +58 -38
- package/src/server/actions/queue-mappings.ts +1 -0
- package/src/server/actions/tasks.ts +142 -142
- package/src/server/adapters/redis-event-queue-adapter.ts +189 -0
- package/src/server/adapters/redis-node-registry.ts +44 -4
- package/src/server/collections/orchestrator-stacks.ts +6 -0
- package/src/server/collections/worker-queue-mappings.ts +3 -3
- package/src/server/orchestrator/PackageManager.ts +48 -11
- package/src/server/orchestrator/types.ts +5 -4
- package/src/server/plugin.ts +40 -6
- package/src/server/queue-scanner.ts +65 -51
- package/src/server/utils/node.ts +3 -10
- package/src/server/utils/redis.ts +39 -4
- package/src/shared/worker-processes.ts +216 -0
- package/dist/client-v2/914.c0bce51908fd81d7.js +0 -10
|
@@ -38,6 +38,12 @@ import {
|
|
|
38
38
|
} from '@ant-design/icons';
|
|
39
39
|
import { useApp } from '@nocobase/client-v2';
|
|
40
40
|
import { useT } from './utils';
|
|
41
|
+
import {
|
|
42
|
+
getCommonWorkerProcesses,
|
|
43
|
+
normalizeWorkerMode,
|
|
44
|
+
resolveWorkerProcessName,
|
|
45
|
+
workerModeTokens,
|
|
46
|
+
} from '../shared/worker-processes';
|
|
41
47
|
|
|
42
48
|
const { Text, Title } = Typography;
|
|
43
49
|
|
|
@@ -58,6 +64,7 @@ interface StackInfo {
|
|
|
58
64
|
image: string;
|
|
59
65
|
command?: string;
|
|
60
66
|
envVars?: Record<string, string>;
|
|
67
|
+
workerMode?: string;
|
|
61
68
|
resourceLimits?: Record<string, string>;
|
|
62
69
|
replicas: number;
|
|
63
70
|
desiredReplicas: number;
|
|
@@ -73,6 +80,15 @@ interface StackInfo {
|
|
|
73
80
|
k8sVolumes?: Record<string, any>[];
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
interface DiscoveredQueue {
|
|
84
|
+
name: string;
|
|
85
|
+
label: string;
|
|
86
|
+
description: string;
|
|
87
|
+
type: 'event-queue' | 'redis-list';
|
|
88
|
+
pending: number | null;
|
|
89
|
+
workerProcessName?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
76
92
|
const DEFAULT_WORKER_COMMAND = `export NOCOBASE_RUNNING_IN_DOCKER=true
|
|
77
93
|
if ! command -v git >/dev/null; then echo "Installing git..."; if command -v apt-get >/dev/null; then apt-get update && apt-get install -y git; elif command -v apk >/dev/null; then apk add git; fi; fi
|
|
78
94
|
if [ ! -d /app/nocobase ]; then mkdir -p /app/nocobase; fi
|
|
@@ -98,9 +114,14 @@ while [ $WAITED -lt $MAX_WAIT ]; do
|
|
|
98
114
|
done
|
|
99
115
|
exec yarn --cwd /app/nocobase start`;
|
|
100
116
|
|
|
117
|
+
const DEFAULT_WORKER_MODE = 'workflow:process,async-task:process';
|
|
118
|
+
|
|
119
|
+
const COMMON_WORKER_MODES = getCommonWorkerProcesses();
|
|
120
|
+
const ALL_WORKER_MODE = { name: '*', label: 'All processes', description: 'Consume every worker process' };
|
|
121
|
+
|
|
101
122
|
const DEFAULT_WORKER_ENV = {
|
|
102
123
|
APP_ROLE: 'worker',
|
|
103
|
-
WORKER_MODE:
|
|
124
|
+
WORKER_MODE: DEFAULT_WORKER_MODE,
|
|
104
125
|
APP_PORT: '13000',
|
|
105
126
|
SKILL_HUB_SANDBOX: 'false',
|
|
106
127
|
};
|
|
@@ -161,6 +182,21 @@ function parseJsonText(value: string | undefined, fallback: any, label: string)
|
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
|
|
185
|
+
function splitWorkerMode(value?: string): string[] {
|
|
186
|
+
return workerModeTokens(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getStackWorkerMode(stack?: StackInfo | null, fallback = '*'): string {
|
|
190
|
+
return normalizeWorkerMode(stack?.workerMode || stack?.envVars?.WORKER_MODE || fallback) || '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function shortWorkerMode(workerMode: string): string {
|
|
194
|
+
if (workerMode.length <= 46) {
|
|
195
|
+
return workerMode;
|
|
196
|
+
}
|
|
197
|
+
return `${workerMode.slice(0, 43)}...`;
|
|
198
|
+
}
|
|
199
|
+
|
|
164
200
|
function stackToFormValues(stack?: StackInfo | null) {
|
|
165
201
|
if (!stack) {
|
|
166
202
|
return {
|
|
@@ -168,6 +204,7 @@ function stackToFormValues(stack?: StackInfo | null) {
|
|
|
168
204
|
adapter: 'kubernetes',
|
|
169
205
|
image: 'nocobase/nocobase:2.1.6-full',
|
|
170
206
|
command: DEFAULT_WORKER_COMMAND,
|
|
207
|
+
workerMode: splitWorkerMode(DEFAULT_WORKER_MODE),
|
|
171
208
|
envVars: jsonText(DEFAULT_WORKER_ENV, {}),
|
|
172
209
|
resourceLimits: jsonText({ memory: '1536Mi' }, {}),
|
|
173
210
|
desiredReplicas: 1,
|
|
@@ -187,6 +224,7 @@ function stackToFormValues(stack?: StackInfo | null) {
|
|
|
187
224
|
|
|
188
225
|
return {
|
|
189
226
|
...stack,
|
|
227
|
+
workerMode: splitWorkerMode(getStackWorkerMode(stack)),
|
|
190
228
|
envVars: jsonText(stack.envVars, {}),
|
|
191
229
|
resourceLimits: jsonText(stack.resourceLimits, {}),
|
|
192
230
|
k8sEnv: jsonText(stack.k8sEnv, []),
|
|
@@ -197,9 +235,25 @@ function stackToFormValues(stack?: StackInfo | null) {
|
|
|
197
235
|
}
|
|
198
236
|
|
|
199
237
|
function formValuesToStack(values: any) {
|
|
238
|
+
const workerMode = normalizeWorkerMode(values.workerMode);
|
|
239
|
+
if (!workerMode) {
|
|
240
|
+
throw new Error('Select at least one process or queue');
|
|
241
|
+
}
|
|
242
|
+
if (workerMode.split(',').includes('!')) {
|
|
243
|
+
throw new Error('Container worker stacks cannot include app process "!"');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const envVars = parseJsonText(values.envVars, {}, 'Environment variables');
|
|
247
|
+
envVars.APP_ROLE = envVars.APP_ROLE || 'worker';
|
|
248
|
+
envVars.WORKER_MODE = workerMode;
|
|
249
|
+
if (envVars.SKILL_HUB_SANDBOX === undefined) {
|
|
250
|
+
envVars.SKILL_HUB_SANDBOX = 'false';
|
|
251
|
+
}
|
|
252
|
+
|
|
200
253
|
return {
|
|
201
254
|
...values,
|
|
202
|
-
|
|
255
|
+
workerMode,
|
|
256
|
+
envVars,
|
|
203
257
|
resourceLimits: parseJsonText(values.resourceLimits, {}, 'Resource limits'),
|
|
204
258
|
k8sEnv: parseJsonText(values.k8sEnv, [], 'Kubernetes env'),
|
|
205
259
|
k8sEnvFrom: parseJsonText(values.k8sEnvFrom, [], 'Kubernetes envFrom'),
|
|
@@ -237,6 +291,21 @@ export function ContainerOrchestrator() {
|
|
|
237
291
|
const [settingsForm] = Form.useForm();
|
|
238
292
|
const [stackForm] = Form.useForm();
|
|
239
293
|
const [dockerNetworks, setDockerNetworks] = useState<{ id: string; name: string }[]>([]);
|
|
294
|
+
const [queueOptions, setQueueOptions] = useState<DiscoveredQueue[]>([]);
|
|
295
|
+
const [queuesLoading, setQueuesLoading] = useState(false);
|
|
296
|
+
|
|
297
|
+
const fetchQueues = useCallback(async () => {
|
|
298
|
+
setQueuesLoading(true);
|
|
299
|
+
try {
|
|
300
|
+
const res = await api.request({ url: '/workerQueueMappings:scanQueues' });
|
|
301
|
+
const data = res.data?.data || res.data;
|
|
302
|
+
setQueueOptions(Array.isArray(data?.discovered) ? data.discovered : []);
|
|
303
|
+
} catch {
|
|
304
|
+
setQueueOptions([]);
|
|
305
|
+
} finally {
|
|
306
|
+
setQueuesLoading(false);
|
|
307
|
+
}
|
|
308
|
+
}, [api]);
|
|
240
309
|
|
|
241
310
|
// Fetch Docker networks
|
|
242
311
|
const fetchNetworks = useCallback(async () => {
|
|
@@ -329,6 +398,7 @@ export function ContainerOrchestrator() {
|
|
|
329
398
|
setStackModal({ visible: true, stack: stack || null });
|
|
330
399
|
stackForm.setFieldsValue(stackToFormValues(stack));
|
|
331
400
|
fetchNetworks();
|
|
401
|
+
fetchQueues();
|
|
332
402
|
};
|
|
333
403
|
|
|
334
404
|
const closeStackModal = () => {
|
|
@@ -370,7 +440,8 @@ export function ContainerOrchestrator() {
|
|
|
370
440
|
fetchPing();
|
|
371
441
|
fetchStacks();
|
|
372
442
|
fetchSettings();
|
|
373
|
-
|
|
443
|
+
fetchQueues();
|
|
444
|
+
}, [fetchPing, fetchStacks, fetchSettings, fetchQueues]);
|
|
374
445
|
|
|
375
446
|
// Load containers for each stack and poll every 10s
|
|
376
447
|
useEffect(() => {
|
|
@@ -517,6 +588,8 @@ export function ContainerOrchestrator() {
|
|
|
517
588
|
width: 160,
|
|
518
589
|
render: (_: any, record: ContainerInfo & { _stackId?: number }) => {
|
|
519
590
|
const isK8s = pingResult?.adapter === 'kubernetes';
|
|
591
|
+
const stackId = record._stackId;
|
|
592
|
+
if (!stackId) return null;
|
|
520
593
|
return (
|
|
521
594
|
<Space size="small">
|
|
522
595
|
{!isK8s &&
|
|
@@ -526,7 +599,7 @@ export function ContainerOrchestrator() {
|
|
|
526
599
|
size="small"
|
|
527
600
|
type="text"
|
|
528
601
|
icon={<PauseCircleOutlined />}
|
|
529
|
-
onClick={() => handleAction('stop', record.id,
|
|
602
|
+
onClick={() => handleAction('stop', record.id, stackId)}
|
|
530
603
|
/>
|
|
531
604
|
</Tooltip>
|
|
532
605
|
) : (
|
|
@@ -535,7 +608,7 @@ export function ContainerOrchestrator() {
|
|
|
535
608
|
size="small"
|
|
536
609
|
type="text"
|
|
537
610
|
icon={<PlayCircleOutlined style={{ color: '#52c41a' }} />}
|
|
538
|
-
onClick={() => handleAction('start', record.id,
|
|
611
|
+
onClick={() => handleAction('start', record.id, stackId)}
|
|
539
612
|
/>
|
|
540
613
|
</Tooltip>
|
|
541
614
|
))}
|
|
@@ -544,12 +617,12 @@ export function ContainerOrchestrator() {
|
|
|
544
617
|
size="small"
|
|
545
618
|
type="text"
|
|
546
619
|
icon={<FileTextOutlined />}
|
|
547
|
-
onClick={() => handleViewLogs(record.id,
|
|
620
|
+
onClick={() => handleViewLogs(record.id, stackId)}
|
|
548
621
|
/>
|
|
549
622
|
</Tooltip>
|
|
550
623
|
<Popconfirm
|
|
551
624
|
title={t('Remove this container?')}
|
|
552
|
-
onConfirm={() => handleAction('remove', record.id,
|
|
625
|
+
onConfirm={() => handleAction('remove', record.id, stackId)}
|
|
553
626
|
>
|
|
554
627
|
<Tooltip title={t('Remove')}>
|
|
555
628
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
|
@@ -561,6 +634,23 @@ export function ContainerOrchestrator() {
|
|
|
561
634
|
},
|
|
562
635
|
];
|
|
563
636
|
|
|
637
|
+
const workerModeSelectOptions = Array.from(
|
|
638
|
+
new Map(
|
|
639
|
+
[ALL_WORKER_MODE, ...COMMON_WORKER_MODES, ...queueOptions].map((queue) => {
|
|
640
|
+
const workerProcessName = 'workerProcessName' in queue ? queue.workerProcessName : undefined;
|
|
641
|
+
const processName = workerProcessName || resolveWorkerProcessName(queue.name);
|
|
642
|
+
return [
|
|
643
|
+
processName,
|
|
644
|
+
{
|
|
645
|
+
value: processName,
|
|
646
|
+
label: `${queue.label} (${processName})`,
|
|
647
|
+
title: queue.name === processName ? queue.description : `${queue.description} - alias: ${queue.name}`,
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
}),
|
|
651
|
+
).values(),
|
|
652
|
+
);
|
|
653
|
+
|
|
564
654
|
if (!pingResult) {
|
|
565
655
|
return (
|
|
566
656
|
<Spin
|
|
@@ -646,6 +736,7 @@ export function ContainerOrchestrator() {
|
|
|
646
736
|
{stacks.map((stack) => {
|
|
647
737
|
const stackContainers = (containers[stack.id] || []).map((c) => ({ ...c, _stackId: stack.id }));
|
|
648
738
|
const meta = containerMeta[stack.id] || {};
|
|
739
|
+
const workerMode = getStackWorkerMode(stack);
|
|
649
740
|
|
|
650
741
|
return (
|
|
651
742
|
<Card
|
|
@@ -655,6 +746,9 @@ export function ContainerOrchestrator() {
|
|
|
655
746
|
<CloudServerOutlined />
|
|
656
747
|
<span>{stack.name}</span>
|
|
657
748
|
<Tag>{stack.adapter}</Tag>
|
|
749
|
+
<Tooltip title={workerMode}>
|
|
750
|
+
<Tag color={workerMode === '*' ? 'orange' : 'geekblue'}>{shortWorkerMode(workerMode)}</Tag>
|
|
751
|
+
</Tooltip>
|
|
658
752
|
</Space>
|
|
659
753
|
}
|
|
660
754
|
extra={
|
|
@@ -830,6 +924,46 @@ export function ContainerOrchestrator() {
|
|
|
830
924
|
</Col>
|
|
831
925
|
</Row>
|
|
832
926
|
|
|
927
|
+
<Form.Item
|
|
928
|
+
name="workerMode"
|
|
929
|
+
label={t('Processes / queues')}
|
|
930
|
+
extra={t('Saved as WORKER_MODE. Use tags for custom process keys that are not discovered yet.')}
|
|
931
|
+
rules={[
|
|
932
|
+
{
|
|
933
|
+
validator: (_: unknown, value: string[] | string | undefined) =>
|
|
934
|
+
normalizeWorkerMode(value)
|
|
935
|
+
? Promise.resolve()
|
|
936
|
+
: Promise.reject(new Error(t('Select at least one process or queue'))),
|
|
937
|
+
},
|
|
938
|
+
]}
|
|
939
|
+
>
|
|
940
|
+
<Select
|
|
941
|
+
mode="tags"
|
|
942
|
+
loading={queuesLoading}
|
|
943
|
+
options={workerModeSelectOptions}
|
|
944
|
+
placeholder={DEFAULT_WORKER_MODE}
|
|
945
|
+
tokenSeparators={[',']}
|
|
946
|
+
showSearch
|
|
947
|
+
/>
|
|
948
|
+
</Form.Item>
|
|
949
|
+
|
|
950
|
+
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.workerMode !== curr.workerMode}>
|
|
951
|
+
{() => {
|
|
952
|
+
const workerMode = normalizeWorkerMode(stackForm.getFieldValue('workerMode')) || '';
|
|
953
|
+
if (!workerMode.includes('*')) {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
return (
|
|
957
|
+
<Alert
|
|
958
|
+
type="warning"
|
|
959
|
+
showIcon
|
|
960
|
+
style={{ marginBottom: 16 }}
|
|
961
|
+
message={t('WORKER_MODE=* makes this stack consume every queue. Prefer explicit queues in HA.')}
|
|
962
|
+
/>
|
|
963
|
+
);
|
|
964
|
+
}}
|
|
965
|
+
</Form.Item>
|
|
966
|
+
|
|
833
967
|
{/* Row 3: Deployment name | Service account | Image pull policy */}
|
|
834
968
|
<Row gutter={12}>
|
|
835
969
|
<Col span={8}>
|
|
@@ -899,7 +1033,11 @@ export function ContainerOrchestrator() {
|
|
|
899
1033
|
|
|
900
1034
|
<Row gutter={12}>
|
|
901
1035
|
<Col span={12}>
|
|
902
|
-
<Form.Item
|
|
1036
|
+
<Form.Item
|
|
1037
|
+
name="envVars"
|
|
1038
|
+
label={t('Environment variables JSON')}
|
|
1039
|
+
extra={t('WORKER_MODE is managed by Processes / queues above.')}
|
|
1040
|
+
>
|
|
903
1041
|
<Input.TextArea rows={8} />
|
|
904
1042
|
</Form.Item>
|
|
905
1043
|
</Col>
|
|
@@ -35,6 +35,7 @@ interface DiscoveredQueue {
|
|
|
35
35
|
description: string;
|
|
36
36
|
type: 'event-queue' | 'redis-list';
|
|
37
37
|
pending: number | null;
|
|
38
|
+
workerProcessName?: string;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
interface RegisteredMapping {
|
|
@@ -208,7 +209,12 @@ export function QueueAssignment() {
|
|
|
208
209
|
) : (
|
|
209
210
|
<MinusCircleOutlined style={{ color: '#52c41a' }} />
|
|
210
211
|
)}
|
|
211
|
-
<
|
|
212
|
+
<Space direction="vertical" size={0}>
|
|
213
|
+
<Text code>{name}</Text>
|
|
214
|
+
{record.workerProcessName && record.workerProcessName !== name ? (
|
|
215
|
+
<Text type="secondary">{record.workerProcessName}</Text>
|
|
216
|
+
) : null}
|
|
217
|
+
</Space>
|
|
212
218
|
</Space>
|
|
213
219
|
),
|
|
214
220
|
},
|
|
@@ -306,7 +312,9 @@ export function QueueAssignment() {
|
|
|
306
312
|
<Col>
|
|
307
313
|
<Space>
|
|
308
314
|
<Text strong>{t('Queue Assignment')}</Text>
|
|
309
|
-
<Text type="secondary">
|
|
315
|
+
<Text type="secondary">
|
|
316
|
+
{t('Fallback mapping for legacy stacks without explicit Processes / queues.')}
|
|
317
|
+
</Text>
|
|
310
318
|
</Space>
|
|
311
319
|
</Col>
|
|
312
320
|
<Col>
|
package/src/locale/en-US.json
CHANGED
|
@@ -188,10 +188,16 @@
|
|
|
188
188
|
"Package": "Package",
|
|
189
189
|
"DB Version": "DB Version",
|
|
190
190
|
"Runtime Versions": "Runtime Versions",
|
|
191
|
-
"Queue Assignment": "Queue Assignment",
|
|
192
|
-
"Queue Name": "Queue Name",
|
|
193
|
-
"Map queues to worker stacks. Unassigned queues run on all workers.": "Map queues to worker stacks. Unassigned queues run on all workers.",
|
|
194
|
-
"
|
|
191
|
+
"Queue Assignment": "Queue Assignment",
|
|
192
|
+
"Queue Name": "Queue Name",
|
|
193
|
+
"Map queues to worker stacks. Unassigned queues run on all workers.": "Map queues to worker stacks. Unassigned queues run on all workers.",
|
|
194
|
+
"Fallback mapping for legacy stacks without explicit Processes / queues.": "Fallback mapping for legacy stacks without explicit Processes / queues.",
|
|
195
|
+
"Processes / queues": "Processes / queues",
|
|
196
|
+
"Saved as WORKER_MODE. Use tags for custom process keys that are not discovered yet.": "Saved as WORKER_MODE. Use tags for custom process keys that are not discovered yet.",
|
|
197
|
+
"Select at least one process or queue": "Select at least one process or queue",
|
|
198
|
+
"WORKER_MODE=* makes this stack consume every queue. Prefer explicit queues in HA.": "WORKER_MODE=* makes this stack consume every queue. Prefer explicit queues in HA.",
|
|
199
|
+
"WORKER_MODE is managed by Processes / queues above.": "WORKER_MODE is managed by Processes / queues above.",
|
|
200
|
+
"Scan Queues": "Scan Queues",
|
|
195
201
|
"Auto-map ({count})": "Auto-map ({count})",
|
|
196
202
|
"Register": "Register",
|
|
197
203
|
"Unregister": "Unregister",
|
|
@@ -201,5 +207,9 @@
|
|
|
201
207
|
"No queues discovered. Click \"Scan Queues\" to detect registered queues.": "No queues discovered. Click \"Scan Queues\" to detect registered queues.",
|
|
202
208
|
"Auto-mapped {count} queue(s)": "Auto-mapped {count} queue(s)",
|
|
203
209
|
"Queue Assignment updated": "Queue Assignment updated",
|
|
204
|
-
"Unassigned (worker runs all queues)": "Unassigned (worker runs all queues)"
|
|
205
|
-
|
|
210
|
+
"Unassigned (worker runs all queues)": "Unassigned (worker runs all queues)",
|
|
211
|
+
"Cluster registry has no worker heartbeats": "Cluster registry has no worker heartbeats",
|
|
212
|
+
"Cluster registry Redis is not configured": "Cluster registry Redis is not configured",
|
|
213
|
+
"Cluster Nodes reads Redis heartbeats, not the container runtime. Check worker boot, plugin-cluster-manager, and shared Redis configuration.": "Cluster Nodes reads Redis heartbeats, not the container runtime. Check worker boot, plugin-cluster-manager, and shared Redis configuration.",
|
|
214
|
+
"Set REDIS_URL or CLUSTER_MANAGER_REDIS_URL on every app and worker to enable cluster node discovery.": "Set REDIS_URL or CLUSTER_MANAGER_REDIS_URL on every app and worker to enable cluster node discovery."
|
|
215
|
+
}
|
package/src/locale/vi-VN.json
CHANGED
|
@@ -157,10 +157,16 @@
|
|
|
157
157
|
"Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments.": "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments.",
|
|
158
158
|
"{count} legacy application record(s) were found in the applications collection.": "{count} legacy application record(s) were found in the applications collection.",
|
|
159
159
|
"App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins.": "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins.",
|
|
160
|
-
"Queue Assignment": "Gán hàng đợi",
|
|
161
|
-
"Queue Name": "Tên hàng đợi",
|
|
162
|
-
"Map queues to worker stacks. Unassigned queues run on all workers.": "Gán hàng đợi vào worker stack. Hàng đợi chưa gán sẽ chạy trên tất cả worker.",
|
|
163
|
-
"
|
|
160
|
+
"Queue Assignment": "Gán hàng đợi",
|
|
161
|
+
"Queue Name": "Tên hàng đợi",
|
|
162
|
+
"Map queues to worker stacks. Unassigned queues run on all workers.": "Gán hàng đợi vào worker stack. Hàng đợi chưa gán sẽ chạy trên tất cả worker.",
|
|
163
|
+
"Fallback mapping for legacy stacks without explicit Processes / queues.": "Mapping dự phòng cho các stack cũ chưa cấu hình Processes / queues rõ ràng.",
|
|
164
|
+
"Processes / queues": "Processes / queues",
|
|
165
|
+
"Saved as WORKER_MODE. Use tags for custom process keys that are not discovered yet.": "Được lưu thành WORKER_MODE. Có thể nhập tag cho process key tùy chỉnh chưa được phát hiện.",
|
|
166
|
+
"Select at least one process or queue": "Chọn ít nhất một process hoặc queue",
|
|
167
|
+
"WORKER_MODE=* makes this stack consume every queue. Prefer explicit queues in HA.": "WORKER_MODE=* khiến stack này tiêu thụ mọi queue. Nên chọn queue rõ ràng trong HA.",
|
|
168
|
+
"WORKER_MODE is managed by Processes / queues above.": "WORKER_MODE được quản lý bởi Processes / queues ở trên.",
|
|
169
|
+
"Scan Queues": "Quét hàng đợi",
|
|
164
170
|
"Auto-map ({count})": "Tự động gán ({count})",
|
|
165
171
|
"Register": "Đăng ký",
|
|
166
172
|
"Unregister": "Hủy đăng ký",
|
|
@@ -202,5 +208,9 @@
|
|
|
202
208
|
"Log Files": "Log Files",
|
|
203
209
|
"Package": "Package",
|
|
204
210
|
"DB Version": "DB Version",
|
|
205
|
-
"Runtime Versions": "Runtime Versions"
|
|
206
|
-
|
|
211
|
+
"Runtime Versions": "Runtime Versions",
|
|
212
|
+
"Cluster registry has no worker heartbeats": "Registry cụm chưa có heartbeat từ worker",
|
|
213
|
+
"Cluster registry Redis is not configured": "Chưa cấu hình Redis cho registry cụm",
|
|
214
|
+
"Cluster Nodes reads Redis heartbeats, not the container runtime. Check worker boot, plugin-cluster-manager, and shared Redis configuration.": "Cluster Nodes đọc heartbeat trong Redis, không đọc trực tiếp container runtime. Hãy kiểm tra worker đã boot, plugin-cluster-manager đã load, và cấu hình Redis dùng chung.",
|
|
215
|
+
"Set REDIS_URL or CLUSTER_MANAGER_REDIS_URL on every app and worker to enable cluster node discovery.": "Thiết lập REDIS_URL hoặc CLUSTER_MANAGER_REDIS_URL trên mọi app và worker để bật khám phá node trong cụm."
|
|
216
|
+
}
|
package/src/locale/zh-CN.json
CHANGED
|
@@ -190,10 +190,16 @@
|
|
|
190
190
|
"Package": "Package",
|
|
191
191
|
"DB Version": "DB Version",
|
|
192
192
|
"Runtime Versions": "Runtime Versions",
|
|
193
|
-
"Queue Assignment": "队列分配",
|
|
194
|
-
"Queue Name": "队列名称",
|
|
195
|
-
"Map queues to worker stacks. Unassigned queues run on all workers.": "将队列映射到工作节点栈。未分配的队列将在所有工作节点上运行。",
|
|
196
|
-
"
|
|
193
|
+
"Queue Assignment": "队列分配",
|
|
194
|
+
"Queue Name": "队列名称",
|
|
195
|
+
"Map queues to worker stacks. Unassigned queues run on all workers.": "将队列映射到工作节点栈。未分配的队列将在所有工作节点上运行。",
|
|
196
|
+
"Fallback mapping for legacy stacks without explicit Processes / queues.": "用于没有显式进程/队列配置的旧工作栈的兜底映射。",
|
|
197
|
+
"Processes / queues": "进程 / 队列",
|
|
198
|
+
"Saved as WORKER_MODE. Use tags for custom process keys that are not discovered yet.": "保存为 WORKER_MODE。可用标签输入尚未发现的自定义进程键。",
|
|
199
|
+
"Select at least one process or queue": "请至少选择一个进程或队列",
|
|
200
|
+
"WORKER_MODE=* makes this stack consume every queue. Prefer explicit queues in HA.": "WORKER_MODE=* 会让此工作栈消费所有队列。HA 环境建议显式选择队列。",
|
|
201
|
+
"WORKER_MODE is managed by Processes / queues above.": "WORKER_MODE 由上方的进程 / 队列配置管理。",
|
|
202
|
+
"Scan Queues": "扫描队列",
|
|
197
203
|
"Auto-map ({count})": "自动映射 ({count})",
|
|
198
204
|
"Register": "注册",
|
|
199
205
|
"Unregister": "注销",
|
|
@@ -203,5 +209,9 @@
|
|
|
203
209
|
"No queues discovered. Click \"Scan Queues\" to detect registered queues.": "未发现队列。点击\"扫描队列\"以检测已注册的队列。",
|
|
204
210
|
"Auto-mapped {count} queue(s)": "已自动映射 {count} 个队列",
|
|
205
211
|
"Queue Assignment updated": "队列分配已更新",
|
|
206
|
-
"Unassigned (worker runs all queues)": "未分配(工作节点运行所有队列)"
|
|
207
|
-
|
|
212
|
+
"Unassigned (worker runs all queues)": "未分配(工作节点运行所有队列)",
|
|
213
|
+
"Cluster registry has no worker heartbeats": "集群注册表没有 worker 心跳",
|
|
214
|
+
"Cluster registry Redis is not configured": "未配置集群注册表 Redis",
|
|
215
|
+
"Cluster Nodes reads Redis heartbeats, not the container runtime. Check worker boot, plugin-cluster-manager, and shared Redis configuration.": "Cluster Nodes 读取 Redis 心跳,而不是直接读取容器运行时。请检查 worker 启动、plugin-cluster-manager 加载以及共享 Redis 配置。",
|
|
216
|
+
"Set REDIS_URL or CLUSTER_MANAGER_REDIS_URL on every app and worker to enable cluster node discovery.": "请在每个 app 和 worker 上设置 REDIS_URL 或 CLUSTER_MANAGER_REDIS_URL,以启用集群节点发现。"
|
|
217
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isWorkerOnlyMode,
|
|
3
|
+
normalizeWorkerMode,
|
|
4
|
+
resolveWorkerProcessName,
|
|
5
|
+
workerModeServesProcess,
|
|
6
|
+
} from '../../shared/worker-processes';
|
|
7
|
+
import { getNodeRoleFrom } from '../utils/node';
|
|
8
|
+
|
|
9
|
+
describe('worker process resolver', () => {
|
|
10
|
+
it('normalizes process keys and queue aliases', () => {
|
|
11
|
+
expect(normalizeWorkerMode('workflow.pendingExecution, plugin-git-manager.review')).toBe(
|
|
12
|
+
'workflow:process,git-review:process',
|
|
13
|
+
);
|
|
14
|
+
expect(normalizeWorkerMode('main:plugin-build-guide-block:build:queue')).toBe('build-guide:process');
|
|
15
|
+
expect(normalizeWorkerMode('file-preview-auth.ocr.queue')).toBe('file-preview-auth:ocr');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('lets wildcard win when mixed with explicit processes', () => {
|
|
19
|
+
expect(normalizeWorkerMode('workflow:process,*,git-review:process')).toBe('*');
|
|
20
|
+
expect(workerModeServesProcess('*,workflow:process', 'notification-manager.send')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('routes app and worker modes using NocoBase worker semantics', () => {
|
|
24
|
+
expect(isWorkerOnlyMode('!')).toBe(false);
|
|
25
|
+
expect(isWorkerOnlyMode('workflow:process')).toBe(true);
|
|
26
|
+
expect(isWorkerOnlyMode('workflow.pendingExecution')).toBe(true);
|
|
27
|
+
expect(workerModeServesProcess('workflow.pendingExecution', 'workflow:process')).toBe(true);
|
|
28
|
+
expect(workerModeServesProcess('workflow:process', 'plugin-git-manager.review')).toBe(false);
|
|
29
|
+
expect(workerModeServesProcess('file-preview-auth.ocr.queue', 'file-preview-auth:ocr')).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('keeps unknown custom process keys stable', () => {
|
|
33
|
+
expect(resolveWorkerProcessName('custom-plugin:process')).toBe('custom-plugin:process');
|
|
34
|
+
expect(workerModeServesProcess('custom-plugin:process', 'custom-plugin:process')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('lets explicit APP_ROLE override worker mode for local node classification', () => {
|
|
38
|
+
expect(getNodeRoleFrom({ appRole: 'app', workerMode: '*' })).toBe('app');
|
|
39
|
+
expect(getNodeRoleFrom({ appRole: 'worker', workerMode: '!' })).toBe('worker');
|
|
40
|
+
expect(getNodeRoleFrom({ appRole: 'sandbox', workerMode: '!' })).toBe('sandbox');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -181,10 +181,13 @@ function getReferenceVersion(nodes: ClusterNodeRecord[]) {
|
|
|
181
181
|
return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || null;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
function getClusterRegistry(ctx: Context): RedisNodeRegistry {
|
|
185
185
|
const plugin = (ctx.app as any).pm?.get?.('plugin-cluster-manager') as any;
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
return plugin?.nodeRegistry ?? new RedisNodeRegistry(ctx.app);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function getClusterNodes(ctx: Context): Promise<ClusterNodeRecord[]> {
|
|
190
|
+
return getClusterRegistry(ctx).getNodes();
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
async function getExpectedPackages(ctx: Context): Promise<NormalizedPackages> {
|
|
@@ -267,6 +270,8 @@ export async function readLocalLogs(app: any, maxLines: number) {
|
|
|
267
270
|
hostname: os.hostname(),
|
|
268
271
|
pid: process.pid,
|
|
269
272
|
workerMode: process.env.WORKER_MODE || 'main',
|
|
273
|
+
appRole: process.env.APP_ROLE || '',
|
|
274
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
270
275
|
};
|
|
271
276
|
|
|
272
277
|
if (logFiles.length === 0) {
|
|
@@ -301,7 +306,12 @@ export const clusterActions = {
|
|
|
301
306
|
*/
|
|
302
307
|
async current(ctx: Context, next: () => Promise<void>) {
|
|
303
308
|
const currentMode = process.env.WORKER_MODE || 'main';
|
|
304
|
-
const isApp =
|
|
309
|
+
const isApp =
|
|
310
|
+
getNodeRoleFrom({
|
|
311
|
+
workerMode: process.env.WORKER_MODE,
|
|
312
|
+
appRole: process.env.APP_ROLE,
|
|
313
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
314
|
+
}) === 'app';
|
|
305
315
|
|
|
306
316
|
if (isApp) {
|
|
307
317
|
// This process IS the APP node — return local data directly
|
|
@@ -315,6 +325,8 @@ export const clusterActions = {
|
|
|
315
325
|
arch: process.arch,
|
|
316
326
|
uptime: process.uptime(),
|
|
317
327
|
workerMode: currentMode,
|
|
328
|
+
appRole: process.env.APP_ROLE || '',
|
|
329
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
318
330
|
appPort: process.env.APP_PORT || '',
|
|
319
331
|
clusterMode: process.env.CLUSTER_MODE || '',
|
|
320
332
|
},
|
|
@@ -337,7 +349,9 @@ export const clusterActions = {
|
|
|
337
349
|
const plugin = (ctx.app as any).pm?.get?.('plugin-cluster-manager') as any;
|
|
338
350
|
const registry = plugin?.nodeRegistry ?? new RedisNodeRegistry(ctx.app);
|
|
339
351
|
const nodes = await registry.getNodes();
|
|
340
|
-
const appNode = nodes.find(
|
|
352
|
+
const appNode = nodes.find(
|
|
353
|
+
(n: any) => getNodeRoleFrom({ workerMode: n.workerMode, appRole: n.appRole, isSandbox: n.isSandbox }) === 'app',
|
|
354
|
+
);
|
|
341
355
|
|
|
342
356
|
if (appNode?.nodeDetails) {
|
|
343
357
|
ctx.body = appNode.nodeDetails;
|
|
@@ -353,6 +367,8 @@ export const clusterActions = {
|
|
|
353
367
|
arch: process.arch,
|
|
354
368
|
uptime: process.uptime(),
|
|
355
369
|
workerMode: currentMode,
|
|
370
|
+
appRole: process.env.APP_ROLE || '',
|
|
371
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
356
372
|
appPort: process.env.APP_PORT || '',
|
|
357
373
|
clusterMode: process.env.CLUSTER_MODE || '',
|
|
358
374
|
},
|
|
@@ -385,7 +401,10 @@ export const clusterActions = {
|
|
|
385
401
|
async list(ctx: Context, next: () => Promise<void>) {
|
|
386
402
|
const environments: any[] = [];
|
|
387
403
|
|
|
388
|
-
const
|
|
404
|
+
const registry = getClusterRegistry(ctx);
|
|
405
|
+
const nodes = await registry.getNodes();
|
|
406
|
+
const registryStatus = registry.getStatus();
|
|
407
|
+
let fallback = false;
|
|
389
408
|
|
|
390
409
|
if (nodes && nodes.length > 0) {
|
|
391
410
|
for (const env of nodes) {
|
|
@@ -399,6 +418,7 @@ export const clusterActions = {
|
|
|
399
418
|
lastHeartbeatAt: env.lastHeartbeatAt ? new Date(env.lastHeartbeatAt).toISOString() : null,
|
|
400
419
|
status: env.status || 'online',
|
|
401
420
|
workerMode: env.workerMode,
|
|
421
|
+
appRole: env.appRole,
|
|
402
422
|
isSandbox: env.isSandbox,
|
|
403
423
|
pid: env.pid,
|
|
404
424
|
});
|
|
@@ -407,18 +427,33 @@ export const clusterActions = {
|
|
|
407
427
|
|
|
408
428
|
// If no discovery adapter or empty, at least return current node
|
|
409
429
|
if (environments.length === 0) {
|
|
430
|
+
fallback = true;
|
|
431
|
+
const currentMode = process.env.WORKER_MODE || 'main';
|
|
432
|
+
const appName = process.env.APP_NAME || (ctx.app as any).name || 'main';
|
|
410
433
|
environments.push({
|
|
411
|
-
|
|
434
|
+
id: getLocalNodeId(ctx.app),
|
|
435
|
+
name: `${appName} (${os.hostname()})`,
|
|
412
436
|
hostname: os.hostname(),
|
|
413
437
|
url: null,
|
|
414
438
|
available: true,
|
|
415
439
|
appVersion: null,
|
|
416
440
|
lastHeartbeatAt: new Date().toISOString(),
|
|
417
441
|
status: 'online',
|
|
442
|
+
workerMode: currentMode,
|
|
443
|
+
appRole: process.env.APP_ROLE || '',
|
|
444
|
+
isSandbox: process.env.SKILL_HUB_SANDBOX === 'true',
|
|
445
|
+
pid: process.pid,
|
|
418
446
|
});
|
|
419
447
|
}
|
|
420
448
|
|
|
421
|
-
ctx.body = {
|
|
449
|
+
ctx.body = {
|
|
450
|
+
data: environments,
|
|
451
|
+
meta: {
|
|
452
|
+
count: environments.length,
|
|
453
|
+
fallback,
|
|
454
|
+
registry: registryStatus,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
422
457
|
await next();
|
|
423
458
|
},
|
|
424
459
|
|