plugin-cluster-manager 1.1.10 → 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/client.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 -28
- package/dist/locale/vi-VN.json +139 -28
- package/dist/locale/zh-CN.json +140 -28
- package/dist/server/actions/cache-monitor.js +301 -0
- package/dist/server/actions/cluster-nodes.js +391 -11
- package/dist/server/actions/doctor.js +1246 -0
- package/dist/server/actions/orchestrator.js +37 -0
- package/dist/server/actions/queue-mappings.js +107 -0
- package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
- package/dist/server/collections/cluster-manager-doctor.js +44 -0
- package/dist/server/collections/worker-queue-mappings.js +106 -0
- package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
- package/dist/server/orchestrator/PackageManager.js +21 -24
- package/dist/server/orchestrator/docker-adapter.js +49 -27
- package/dist/server/plugin.js +71 -16
- package/dist/server/queue-scanner.js +141 -0
- package/dist/server/utils/node.js +30 -2
- package/dist/server/utils/versionManager.js +91 -0
- package/package.json +9 -5
- package/server.js +1 -0
- package/src/client/AclCacheManager.tsx +292 -287
- package/src/client/CacheMonitor.tsx +166 -179
- package/src/client/ClusterManagerLayout.tsx +54 -42
- package/src/client/ClusterNodes.tsx +698 -418
- package/src/client/ContainerOrchestrator.tsx +184 -102
- package/src/client/Doctor.tsx +559 -0
- package/src/client/NginxCacheManager.tsx +415 -0
- package/src/client/PluginOperations.tsx +234 -234
- package/src/client/QueueAssignment.tsx +355 -0
- package/src/client/TaskManager.tsx +194 -187
- package/src/client/WorkflowExecutions.tsx +243 -238
- package/src/client/index.tsx +22 -14
- package/src/client/utils/clientSafeCache.ts +41 -0
- package/src/client/utils/requestDedupInterceptor.ts +213 -0
- package/src/client-v2/plugin.tsx +24 -0
- package/src/locale/en-US.json +138 -28
- package/src/locale/vi-VN.json +139 -28
- package/src/locale/zh-CN.json +140 -28
- package/src/server/__tests__/doctor.test.ts +53 -0
- package/src/server/actions/acl-cache.ts +272 -272
- package/src/server/actions/cache-monitor.ts +453 -116
- package/src/server/actions/cluster-nodes.ts +878 -378
- package/src/server/actions/doctor.ts +1536 -0
- package/src/server/actions/orchestrator.ts +54 -2
- package/src/server/actions/queue-mappings.ts +94 -0
- package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
- package/src/server/collections/cluster-manager-doctor.ts +19 -0
- package/src/server/collections/worker-queue-mappings.ts +85 -0
- package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
- package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
- package/src/server/orchestrator/PackageManager.ts +20 -24
- package/src/server/orchestrator/docker-adapter.ts +74 -37
- package/src/server/plugin.ts +347 -270
- package/src/server/queue-scanner.ts +154 -0
- package/src/server/utils/node.ts +48 -0
- package/src/server/utils/versionManager.ts +69 -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/EventQueueMonitor.d.ts +0 -2
- package/dist/client/LockMonitor.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.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 -23
- package/dist/server/actions/cluster-nodes.d.ts +0 -49
- 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-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/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/shared/packages.d.ts +0 -23
- /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
|
@@ -41,7 +41,12 @@ async function getStack(ctx: Context, stackId?: number | string): Promise<StackC
|
|
|
41
41
|
return stack.toJSON() as StackConfig;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async function assertManagedContainer(
|
|
44
|
+
async function assertManagedContainer(
|
|
45
|
+
ctx: Context,
|
|
46
|
+
adapter: IOrchestratorAdapter,
|
|
47
|
+
stack: StackConfig,
|
|
48
|
+
containerId: string,
|
|
49
|
+
) {
|
|
45
50
|
try {
|
|
46
51
|
await adapter.assertManagedByStack(stack, containerId);
|
|
47
52
|
} catch (err: any) {
|
|
@@ -87,7 +92,9 @@ export const orchestratorActions = {
|
|
|
87
92
|
if (rawStatus && typeof rawStatus === 'string') {
|
|
88
93
|
(c as any).packageStatus = JSON.parse(rawStatus);
|
|
89
94
|
}
|
|
90
|
-
} catch {
|
|
95
|
+
} catch {
|
|
96
|
+
// Redis may not be configured — skip package status
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -114,6 +121,10 @@ export const orchestratorActions = {
|
|
|
114
121
|
* POST /workerOrchestrator:scale
|
|
115
122
|
* Body: { stackId: 1, replicas: 3 }
|
|
116
123
|
* Leader-only
|
|
124
|
+
*
|
|
125
|
+
* Before scaling, resolves queue-to-stack mappings and injects
|
|
126
|
+
* WORKER_MODE into the stack's envVars so new containers only
|
|
127
|
+
* process assigned queues.
|
|
117
128
|
*/
|
|
118
129
|
async scale(ctx: Context, next: () => Promise<void>) {
|
|
119
130
|
assertLeader(ctx);
|
|
@@ -124,6 +135,47 @@ export const orchestratorActions = {
|
|
|
124
135
|
if (replicas < 0 || replicas > 20) ctx.throw(400, 'replicas must be between 0 and 20');
|
|
125
136
|
|
|
126
137
|
const stack = await getStack(ctx, stackId);
|
|
138
|
+
|
|
139
|
+
// ── Resolve queue assignments for this stack ──
|
|
140
|
+
try {
|
|
141
|
+
const mappingsRepo = ctx.db.getRepository('workerQueueMappings');
|
|
142
|
+
if (mappingsRepo) {
|
|
143
|
+
const assigned = await mappingsRepo.find({
|
|
144
|
+
filter: {
|
|
145
|
+
stackId: stack.id,
|
|
146
|
+
enabled: true,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const queueNames = assigned.map((m: any) => m.get('queueName') as string).filter(Boolean);
|
|
151
|
+
|
|
152
|
+
if (queueNames.length > 0) {
|
|
153
|
+
const workerMode = queueNames.join(',');
|
|
154
|
+
ctx.app.logger.info(
|
|
155
|
+
`[Orchestrator] Injecting WORKER_MODE=${workerMode} for stack "${stack.name}" (${queueNames.length} queue(s) assigned)`,
|
|
156
|
+
);
|
|
157
|
+
// Merge into envVars; adapter code merges envVars over inherited env
|
|
158
|
+
stack.envVars = {
|
|
159
|
+
...(stack.envVars || {}),
|
|
160
|
+
WORKER_MODE: workerMode,
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
// No specific assignment → default to all queues (backwards compatible)
|
|
164
|
+
stack.envVars = {
|
|
165
|
+
...(stack.envVars || {}),
|
|
166
|
+
WORKER_MODE: '*',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (err: any) {
|
|
171
|
+
// If workerQueueMappings table doesn't exist yet, fall back gracefully
|
|
172
|
+
ctx.app.logger.debug(`[Orchestrator] Queue mappings not available: ${err.message}`);
|
|
173
|
+
stack.envVars = {
|
|
174
|
+
...(stack.envVars || {}),
|
|
175
|
+
WORKER_MODE: '*',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
127
179
|
const result = await adapter.scale(stack, Number(replicas));
|
|
128
180
|
|
|
129
181
|
// Update DB
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Mappings Actions
|
|
3
|
+
*
|
|
4
|
+
* CRUD for workerQueueMappings + scanQueues action that discovers
|
|
5
|
+
* all registered queues via QueueScanner.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Context } from '@nocobase/actions';
|
|
9
|
+
import { scanQueues } from '../queue-scanner';
|
|
10
|
+
|
|
11
|
+
export const queueMappingsActions = {
|
|
12
|
+
/**
|
|
13
|
+
* GET /workerQueueMappings:scanQueues
|
|
14
|
+
* Scans all registered queues (EventQueue + Redis) and merges with existing mappings.
|
|
15
|
+
*/
|
|
16
|
+
async scanQueues(ctx: Context, next: () => Promise<void>) {
|
|
17
|
+
const result = await scanQueues(ctx.app);
|
|
18
|
+
|
|
19
|
+
// Load existing mappings from DB
|
|
20
|
+
const repo = ctx.db.getRepository('workerQueueMappings');
|
|
21
|
+
let existingMappings: any[] = [];
|
|
22
|
+
try {
|
|
23
|
+
existingMappings = await repo.find();
|
|
24
|
+
} catch {
|
|
25
|
+
// Table may not exist yet
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const mappedNames = new Set(existingMappings.map((m) => m.get('queueName') as string));
|
|
29
|
+
|
|
30
|
+
ctx.body = {
|
|
31
|
+
discovered: result.queues,
|
|
32
|
+
total: result.total,
|
|
33
|
+
registered: existingMappings.map((m) => ({
|
|
34
|
+
id: m.get('id'),
|
|
35
|
+
queueName: m.get('queueName'),
|
|
36
|
+
label: m.get('label'),
|
|
37
|
+
stackId: m.get('stackId'),
|
|
38
|
+
enabled: m.get('enabled'),
|
|
39
|
+
type: m.get('type'),
|
|
40
|
+
})),
|
|
41
|
+
unmapped: result.queues
|
|
42
|
+
.filter((q) => !mappedNames.has(q.name))
|
|
43
|
+
.map((q) => ({
|
|
44
|
+
name: q.name,
|
|
45
|
+
type: q.type,
|
|
46
|
+
label: q.label,
|
|
47
|
+
description: q.description,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
await next();
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* POST /workerQueueMappings:autoMap
|
|
55
|
+
* Auto-create mappings for any discovered queues that don't have one yet.
|
|
56
|
+
* Body: { stackId?: number } — optional default stack for new mappings
|
|
57
|
+
*/
|
|
58
|
+
async autoMap(ctx: Context, next: () => Promise<void>) {
|
|
59
|
+
const { stackId } = ctx.action.params.values || {};
|
|
60
|
+
const result = await scanQueues(ctx.app);
|
|
61
|
+
const repo = ctx.db.getRepository('workerQueueMappings');
|
|
62
|
+
|
|
63
|
+
let existingMappings: any[] = [];
|
|
64
|
+
try {
|
|
65
|
+
existingMappings = await repo.find();
|
|
66
|
+
} catch {
|
|
67
|
+
// Table may not exist yet
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mappedNames = new Set(existingMappings.map((m) => m.get('queueName') as string));
|
|
71
|
+
const created: string[] = [];
|
|
72
|
+
|
|
73
|
+
for (const q of result.queues) {
|
|
74
|
+
if (mappedNames.has(q.name)) continue;
|
|
75
|
+
await repo.create({
|
|
76
|
+
values: {
|
|
77
|
+
queueName: q.name,
|
|
78
|
+
label: q.label,
|
|
79
|
+
description: q.description,
|
|
80
|
+
type: q.type,
|
|
81
|
+
stackId: stackId || null,
|
|
82
|
+
enabled: true,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
created.push(q.name);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
ctx.body = {
|
|
89
|
+
created,
|
|
90
|
+
count: created.length,
|
|
91
|
+
};
|
|
92
|
+
await next();
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CollectionOptions } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'clusterManagerDoctorRuns',
|
|
5
|
+
title: 'Cluster Manager Doctor Runs',
|
|
6
|
+
fields: [
|
|
7
|
+
{ name: 'id', type: 'bigInt', autoIncrement: true, primaryKey: true },
|
|
8
|
+
{ name: 'runId', type: 'string', length: 64, unique: true, allowNull: false },
|
|
9
|
+
{ name: 'status', type: 'string', length: 20, defaultValue: 'running', allowNull: false },
|
|
10
|
+
{ name: 'durationMs', type: 'integer', defaultValue: 120000, allowNull: false },
|
|
11
|
+
{ name: 'progress', type: 'integer', defaultValue: 0, allowNull: false },
|
|
12
|
+
{ name: 'startedAt', type: 'date', allowNull: false },
|
|
13
|
+
{ name: 'deadlineAt', type: 'date', allowNull: false },
|
|
14
|
+
{ name: 'finishedAt', type: 'date', allowNull: true },
|
|
15
|
+
{ name: 'finishReason', type: 'string', length: 40, allowNull: true },
|
|
16
|
+
{ name: 'startedBy', type: 'string', length: 200, allowNull: true },
|
|
17
|
+
{ name: 'summary', type: 'json', allowNull: true },
|
|
18
|
+
{ name: 'report', type: 'json', allowNull: true },
|
|
19
|
+
{ name: 'error', type: 'text', allowNull: true },
|
|
20
|
+
{ name: 'createdAt', type: 'date' },
|
|
21
|
+
{ name: 'updatedAt', type: 'date' },
|
|
22
|
+
],
|
|
23
|
+
} as CollectionOptions;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DUMMY COLLECTION
|
|
3
|
+
* Keeps the `clusterManagerDoctor` resourcer compatible with NocoBase
|
|
4
|
+
* workflow/ACL collection lookups. Diagnostic run data lives in
|
|
5
|
+
* `clusterManagerDoctorRuns`.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
name: 'clusterManagerDoctor',
|
|
9
|
+
dumpRules: 'skip',
|
|
10
|
+
autoGenId: true,
|
|
11
|
+
createdAt: false,
|
|
12
|
+
updatedAt: false,
|
|
13
|
+
fields: [
|
|
14
|
+
{
|
|
15
|
+
name: 'name',
|
|
16
|
+
type: 'string',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { cacheVersionManager } from '../utils/versionManager';
|
|
2
|
+
|
|
3
|
+
export function registerCacheHooks(app: any) {
|
|
4
|
+
const db = app.db;
|
|
5
|
+
|
|
6
|
+
// 1. Collections & Fields changes (Metadata)
|
|
7
|
+
db.on('collections.afterSave', async () => {
|
|
8
|
+
await cacheVersionManager.incrementCollectionVersion(app);
|
|
9
|
+
});
|
|
10
|
+
db.on('collections.afterDestroy', async () => {
|
|
11
|
+
await cacheVersionManager.incrementCollectionVersion(app);
|
|
12
|
+
});
|
|
13
|
+
db.on('fields.afterSave', async () => {
|
|
14
|
+
await cacheVersionManager.incrementCollectionVersion(app);
|
|
15
|
+
});
|
|
16
|
+
db.on('fields.afterDestroy', async () => {
|
|
17
|
+
await cacheVersionManager.incrementCollectionVersion(app);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// 2. UI Schemas changes (Dynamic Layouts)
|
|
21
|
+
db.on('uiSchemas.afterSave', async () => {
|
|
22
|
+
await cacheVersionManager.incrementSchemaVersion(app);
|
|
23
|
+
});
|
|
24
|
+
db.on('uiSchemas.afterDestroy', async () => {
|
|
25
|
+
await cacheVersionManager.incrementSchemaVersion(app);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 3. ACL, Roles & Scopes changes
|
|
29
|
+
const invalidateRole = async (model: any) => {
|
|
30
|
+
const roleName = model.get?.('roleName') || model.get?.('name');
|
|
31
|
+
if (roleName) {
|
|
32
|
+
await cacheVersionManager.incrementAclVersion(app, roleName);
|
|
33
|
+
} else {
|
|
34
|
+
await cacheVersionManager.incrementAllAclVersions(app);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
db.on('roles.afterSave', invalidateRole);
|
|
39
|
+
db.on('roles.afterDestroy', invalidateRole);
|
|
40
|
+
|
|
41
|
+
db.on('rolesResources.afterSave', invalidateRole);
|
|
42
|
+
db.on('rolesResources.afterDestroy', invalidateRole);
|
|
43
|
+
|
|
44
|
+
db.on('rolesResourcesActions.afterSave', invalidateRole);
|
|
45
|
+
db.on('rolesResourcesActions.afterDestroy', invalidateRole);
|
|
46
|
+
|
|
47
|
+
db.on('rolesUsers.afterSave', invalidateRole);
|
|
48
|
+
db.on('rolesUsers.afterDestroy', invalidateRole);
|
|
49
|
+
|
|
50
|
+
db.on('scopes.afterSave', async () => {
|
|
51
|
+
await cacheVersionManager.incrementAllAclVersions(app);
|
|
52
|
+
});
|
|
53
|
+
db.on('scopes.afterDestroy', async () => {
|
|
54
|
+
await cacheVersionManager.incrementAllAclVersions(app);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
app.logger.info('[ClusterManager] Cache invalidation hooks registered successfully');
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Context } from '@nocobase/actions';
|
|
2
|
+
import { cacheVersionManager } from '../utils/versionManager';
|
|
3
|
+
|
|
4
|
+
const LIST_META_CACHE_TTL = 1000 * 60 * 10; // 10 minutes cache duration
|
|
5
|
+
|
|
6
|
+
function getErrorMessage(error: unknown) {
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createListMetaCacheMiddleware(app: any) {
|
|
11
|
+
return async function listMetaCacheMiddleware(ctx: Context, next: () => Promise<void>) {
|
|
12
|
+
const cache = app.cache;
|
|
13
|
+
// Skip caching if cache manager is not initialized or this is not collections:listMeta
|
|
14
|
+
if (!cache || ctx.action?.resourceName !== 'collections' || ctx.action?.actionName !== 'listMeta') {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const currentRole = ctx.state?.currentRole || 'anonymous';
|
|
19
|
+
const appName = ctx.headers['x-app'] || 'main';
|
|
20
|
+
const dataSource = ctx.headers['x-data-source'] || 'main';
|
|
21
|
+
const locale = ctx.headers['x-locale'] || ctx.headers['accept-language'] || 'en-US';
|
|
22
|
+
let cacheKey = '';
|
|
23
|
+
let version = 0;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
version = await cacheVersionManager.getCollectionVersion(app);
|
|
27
|
+
cacheKey = `nb:cache:${appName}:meta:v${version}:ds:${dataSource}:role:${currentRole}:lang:${locale}`;
|
|
28
|
+
|
|
29
|
+
// Try reading from NocoBase shared cache manager (Redis or Memory)
|
|
30
|
+
const cached = await cache.get(cacheKey);
|
|
31
|
+
if (cached !== undefined && cached !== null) {
|
|
32
|
+
ctx.body = typeof cached === 'string' ? JSON.parse(cached) : cached;
|
|
33
|
+
ctx.set?.('X-Cache', 'HIT');
|
|
34
|
+
ctx.set?.('X-Collection-Version', String(version));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
app.logger.warn(`[ClusterManager] listMeta cache read skipped: ${getErrorMessage(err)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await next();
|
|
42
|
+
|
|
43
|
+
// If the response is valid, write to cache manager. This must not call next() again.
|
|
44
|
+
if (ctx.status === 200 && ctx.body && cacheKey) {
|
|
45
|
+
try {
|
|
46
|
+
const valueToCache = typeof ctx.body === 'string' ? ctx.body : JSON.stringify(ctx.body);
|
|
47
|
+
await cache.set(cacheKey, valueToCache, LIST_META_CACHE_TTL);
|
|
48
|
+
ctx.set?.('X-Cache', 'MISS');
|
|
49
|
+
ctx.set?.('X-Collection-Version', String(version));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
app.logger.warn(`[ClusterManager] listMeta cache write skipped: ${getErrorMessage(err)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { getRedisClient } from '../utils/redis';
|
|
3
|
+
import { getLocalNodeId, getLocalRole } from '../utils/node';
|
|
3
4
|
import { promises as fsp } from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import Application from '@nocobase/server';
|
|
@@ -66,15 +67,7 @@ function redactUrl(value: string): string {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
function getCurrentRole(): Exclude<TargetRole, 'all'> {
|
|
69
|
-
|
|
70
|
-
return process.env.APP_ROLE;
|
|
71
|
-
}
|
|
72
|
-
if (process.env.SKILL_HUB_SANDBOX === 'true') {
|
|
73
|
-
return 'sandbox';
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const workerMode = process.env.WORKER_MODE || 'main';
|
|
77
|
-
return workerMode === 'worker' || workerMode === 'task' || workerMode === '*' ? 'worker' : 'app';
|
|
70
|
+
return getLocalRole();
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
function formatCommand(command: string, args: string[]): string {
|
|
@@ -451,21 +444,24 @@ export class PackageManager {
|
|
|
451
444
|
const redisClient = getRedisClient(this.app);
|
|
452
445
|
const podName = process.env.POD_NAME || require('os').hostname();
|
|
453
446
|
if (redisClient) {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
447
|
+
const statusPayload = JSON.stringify({
|
|
448
|
+
initStatus,
|
|
449
|
+
initProgressPercent,
|
|
450
|
+
initProgressLog,
|
|
451
|
+
lastInitAt: new Date(),
|
|
452
|
+
lastInitLog: logs.join('\n'),
|
|
453
|
+
...extraValues,
|
|
454
|
+
});
|
|
455
|
+
const keys = [`orchestrator:pkg-status:${podName}`, `cluster-manager:pkg-status:${getLocalNodeId(this.app)}`];
|
|
456
|
+
for (const key of keys) {
|
|
457
|
+
await redisClient.sendCommand([
|
|
458
|
+
'SET',
|
|
459
|
+
key,
|
|
460
|
+
statusPayload,
|
|
461
|
+
'EX',
|
|
462
|
+
'86400', // expire after 1 day
|
|
463
|
+
]);
|
|
464
|
+
}
|
|
469
465
|
}
|
|
470
466
|
|
|
471
467
|
// We can also keep the global DB record as a fallback/historical record
|