plugin-cluster-manager 1.1.10 → 1.1.11
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.js +1 -0
- package/dist/client/Doctor.d.ts +2 -0
- package/dist/client/NginxCacheManager.d.ts +2 -0
- package/dist/client/index.js +1 -1
- package/dist/client/utils/clientSafeCache.d.ts +3 -0
- package/dist/client/utils/requestDedupInterceptor.d.ts +2 -0
- package/dist/externalVersion.js +5 -5
- package/dist/locale/en-US.json +97 -1
- package/dist/locale/vi-VN.json +98 -1
- package/dist/locale/zh-CN.json +98 -1
- package/dist/server/actions/cache-monitor.d.ts +10 -0
- package/dist/server/actions/cache-monitor.js +301 -0
- package/dist/server/actions/cluster-nodes.d.ts +15 -0
- package/dist/server/actions/cluster-nodes.js +394 -10
- package/dist/server/actions/doctor.d.ts +82 -0
- package/dist/server/actions/doctor.js +1250 -0
- package/dist/server/collections/cluster-manager-doctor-runs.d.ts +3 -0
- package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
- package/dist/server/collections/cluster-manager-doctor.d.ts +18 -0
- package/dist/server/collections/cluster-manager-doctor.js +44 -0
- package/dist/server/hooks/cacheInvalidationHooks.d.ts +1 -0
- package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +2 -0
- package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
- package/dist/server/orchestrator/PackageManager.js +20 -16
- package/dist/server/plugin.js +61 -8
- package/dist/server/utils/versionManager.d.ts +10 -0
- package/dist/server/utils/versionManager.js +91 -0
- package/package.json +41 -41
- package/server.js +1 -0
- package/src/client/CacheMonitor.tsx +166 -179
- package/src/client/ClusterManagerLayout.tsx +48 -42
- package/src/client/ClusterNodes.tsx +691 -418
- package/src/client/Doctor.tsx +559 -0
- package/src/client/NginxCacheManager.tsx +415 -0
- package/src/client/PluginOperations.tsx +234 -234
- 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/locale/en-US.json +97 -1
- package/src/locale/vi-VN.json +98 -1
- package/src/locale/zh-CN.json +98 -1
- 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 +882 -378
- package/src/server/actions/doctor.ts +1540 -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/hooks/cacheInvalidationHooks.ts +58 -0
- package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
- package/src/server/orchestrator/PackageManager.ts +19 -15
- package/src/server/plugin.ts +338 -263
- package/src/server/utils/versionManager.ts +69 -0
|
@@ -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,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 } from '../utils/node';
|
|
3
4
|
import { promises as fsp } from 'fs';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import Application from '@nocobase/server';
|
|
@@ -451,21 +452,24 @@ export class PackageManager {
|
|
|
451
452
|
const redisClient = getRedisClient(this.app);
|
|
452
453
|
const podName = process.env.POD_NAME || require('os').hostname();
|
|
453
454
|
if (redisClient) {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
455
|
+
const statusPayload = JSON.stringify({
|
|
456
|
+
initStatus,
|
|
457
|
+
initProgressPercent,
|
|
458
|
+
initProgressLog,
|
|
459
|
+
lastInitAt: new Date(),
|
|
460
|
+
lastInitLog: logs.join('\n'),
|
|
461
|
+
...extraValues,
|
|
462
|
+
});
|
|
463
|
+
const keys = [`orchestrator:pkg-status:${podName}`, `cluster-manager:pkg-status:${getLocalNodeId(this.app)}`];
|
|
464
|
+
for (const key of keys) {
|
|
465
|
+
await redisClient.sendCommand([
|
|
466
|
+
'SET',
|
|
467
|
+
key,
|
|
468
|
+
statusPayload,
|
|
469
|
+
'EX',
|
|
470
|
+
'86400', // expire after 1 day
|
|
471
|
+
]);
|
|
472
|
+
}
|
|
469
473
|
}
|
|
470
474
|
|
|
471
475
|
// We can also keep the global DB record as a fallback/historical record
|