@zintrust/workers 0.1.28 → 0.1.30
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/README.md +16 -1
- package/dist/AnomalyDetection.d.ts +4 -0
- package/dist/AnomalyDetection.js +8 -0
- package/dist/BroadcastWorker.d.ts +2 -0
- package/dist/CanaryController.js +49 -5
- package/dist/ChaosEngineering.js +13 -0
- package/dist/ClusterLock.js +21 -10
- package/dist/DeadLetterQueue.js +12 -8
- package/dist/MultiQueueWorker.d.ts +1 -1
- package/dist/MultiQueueWorker.js +12 -7
- package/dist/NotificationWorker.d.ts +2 -0
- package/dist/PriorityQueue.d.ts +2 -2
- package/dist/PriorityQueue.js +20 -21
- package/dist/ResourceMonitor.js +65 -38
- package/dist/WorkerFactory.d.ts +23 -3
- package/dist/WorkerFactory.js +420 -40
- package/dist/WorkerInit.js +8 -3
- package/dist/WorkerMetrics.d.ts +2 -1
- package/dist/WorkerMetrics.js +152 -93
- package/dist/WorkerRegistry.d.ts +6 -0
- package/dist/WorkerRegistry.js +70 -1
- package/dist/WorkerShutdown.d.ts +21 -0
- package/dist/WorkerShutdown.js +82 -9
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +171 -99
- package/dist/createQueueWorker.d.ts +2 -0
- package/dist/createQueueWorker.js +42 -27
- package/dist/dashboard/types.d.ts +5 -0
- package/dist/dashboard/workers-api.js +136 -43
- package/dist/http/WorkerApiController.js +1 -0
- package/dist/http/WorkerController.js +133 -85
- package/dist/http/WorkerMonitoringService.d.ts +11 -0
- package/dist/http/WorkerMonitoringService.js +62 -0
- package/dist/http/middleware/CustomValidation.js +1 -1
- package/dist/http/middleware/EditWorkerValidation.d.ts +1 -1
- package/dist/http/middleware/EditWorkerValidation.js +7 -6
- package/dist/http/middleware/ProcessorPathSanitizer.js +101 -35
- package/dist/http/middleware/WorkerValidationChain.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/routes/workers.js +48 -6
- package/dist/storage/WorkerStore.d.ts +4 -1
- package/dist/storage/WorkerStore.js +55 -7
- package/dist/telemetry/api/TelemetryAPI.d.ts +46 -0
- package/dist/telemetry/api/TelemetryAPI.js +219 -0
- package/dist/telemetry/api/TelemetryMonitoringService.d.ts +17 -0
- package/dist/telemetry/api/TelemetryMonitoringService.js +113 -0
- package/dist/telemetry/components/AlertPanel.d.ts +1 -0
- package/dist/telemetry/components/AlertPanel.js +13 -0
- package/dist/telemetry/components/CostTracking.d.ts +1 -0
- package/dist/telemetry/components/CostTracking.js +14 -0
- package/dist/telemetry/components/ResourceUsageChart.d.ts +1 -0
- package/dist/telemetry/components/ResourceUsageChart.js +11 -0
- package/dist/telemetry/components/WorkerHealthChart.d.ts +1 -0
- package/dist/telemetry/components/WorkerHealthChart.js +11 -0
- package/dist/telemetry/index.d.ts +15 -0
- package/dist/telemetry/index.js +60 -0
- package/dist/telemetry/routes/dashboard.d.ts +6 -0
- package/dist/telemetry/routes/dashboard.js +608 -0
- package/dist/ui/router/EmbeddedAssets.d.ts +4 -0
- package/dist/ui/router/EmbeddedAssets.js +13 -0
- package/dist/ui/router/ui.js +100 -4
- package/package.json +9 -5
- package/src/AnomalyDetection.ts +9 -0
- package/src/CanaryController.ts +41 -5
- package/src/ChaosEngineering.ts +14 -0
- package/src/ClusterLock.ts +22 -9
- package/src/DeadLetterQueue.ts +13 -8
- package/src/MultiQueueWorker.ts +15 -8
- package/src/PriorityQueue.ts +21 -22
- package/src/ResourceMonitor.ts +72 -40
- package/src/WorkerFactory.ts +545 -49
- package/src/WorkerInit.ts +8 -3
- package/src/WorkerMetrics.ts +183 -105
- package/src/WorkerRegistry.ts +80 -1
- package/src/WorkerShutdown.ts +115 -9
- package/src/WorkerShutdownDurableObject.ts +64 -0
- package/src/createQueueWorker.ts +73 -30
- package/src/dashboard/types.ts +5 -0
- package/src/dashboard/workers-api.ts +165 -52
- package/src/http/WorkerApiController.ts +1 -0
- package/src/http/WorkerController.ts +167 -90
- package/src/http/WorkerMonitoringService.ts +77 -0
- package/src/http/middleware/CustomValidation.ts +1 -1
- package/src/http/middleware/EditWorkerValidation.ts +7 -6
- package/src/http/middleware/ProcessorPathSanitizer.ts +123 -36
- package/src/http/middleware/WorkerValidationChain.ts +1 -0
- package/src/index.ts +6 -1
- package/src/routes/workers.ts +66 -9
- package/src/storage/WorkerStore.ts +59 -9
- package/src/telemetry/api/TelemetryAPI.ts +292 -0
- package/src/telemetry/api/TelemetryMonitoringService.ts +149 -0
- package/src/telemetry/components/AlertPanel.ts +13 -0
- package/src/telemetry/components/CostTracking.ts +14 -0
- package/src/telemetry/components/ResourceUsageChart.ts +11 -0
- package/src/telemetry/components/WorkerHealthChart.ts +11 -0
- package/src/telemetry/index.ts +121 -0
- package/src/telemetry/public/assets/zintrust-logo.svg +15 -0
- package/src/telemetry/routes/dashboard.ts +638 -0
- package/src/telemetry/styles/tailwind.css +1 -0
- package/src/telemetry/styles/zintrust-theme.css +8 -0
- package/src/ui/router/EmbeddedAssets.ts +13 -0
- package/src/ui/router/ui.ts +112 -5
- package/src/ui/workers/index.html +2 -2
- package/src/ui/workers/main.js +232 -61
- package/src/ui/workers/zintrust.svg +30 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +0 -3
- package/dist/dashboard/workers-dashboard-ui.js +0 -1026
- package/dist/dashboard/workers-dashboard.d.ts +0 -4
- package/dist/dashboard/workers-dashboard.js +0 -904
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* HTTP handlers for worker management API
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Logger, getValidatedBody, type IRequest, type IResponse } from '@zintrust/core';
|
|
7
|
+
import { Env, Logger, getValidatedBody, type IRequest, type IResponse } from '@zintrust/core';
|
|
8
8
|
import type { Job } from 'bullmq';
|
|
9
9
|
import { CanaryController } from '../CanaryController';
|
|
10
|
-
import { getWorkers } from '../dashboard/workers-api';
|
|
11
10
|
import { HealthMonitor } from '../HealthMonitor';
|
|
12
11
|
import { getParam } from '../helper';
|
|
13
12
|
import { SLAMonitor } from '../index';
|
|
@@ -19,6 +18,7 @@ import { WorkerRegistry } from '../WorkerRegistry';
|
|
|
19
18
|
import { WorkerShutdown } from '../WorkerShutdown';
|
|
20
19
|
import { WorkerVersioning } from '../WorkerVersioning';
|
|
21
20
|
import type { InfrastructureConfig } from './middleware/InfrastructureValidator';
|
|
21
|
+
import { WorkerMonitoringService } from './WorkerMonitoringService';
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Helper to get request body
|
|
@@ -62,13 +62,13 @@ async function create(req: IRequest, res: IResponse): Promise<void> {
|
|
|
62
62
|
|
|
63
63
|
const rawProcessor = body.processor;
|
|
64
64
|
let processor: (job: Job) => Promise<unknown>;
|
|
65
|
-
let
|
|
65
|
+
let processorSpec: string | undefined;
|
|
66
66
|
|
|
67
67
|
if (typeof rawProcessor === 'string') {
|
|
68
|
-
|
|
69
|
-
const resolved = await WorkerFactory.
|
|
68
|
+
processorSpec = rawProcessor;
|
|
69
|
+
const resolved = await WorkerFactory.resolveProcessorSpec(rawProcessor);
|
|
70
70
|
if (!resolved) {
|
|
71
|
-
res.setStatus(400).json({ error: 'Processor
|
|
71
|
+
res.setStatus(400).json({ error: 'Processor spec could not be resolved' });
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
processor = resolved;
|
|
@@ -84,7 +84,7 @@ async function create(req: IRequest, res: IResponse): Promise<void> {
|
|
|
84
84
|
const config = {
|
|
85
85
|
...(body as WorkerFactoryConfig),
|
|
86
86
|
processor,
|
|
87
|
-
|
|
87
|
+
processorSpec,
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
await WorkerFactory.create(config);
|
|
@@ -114,6 +114,8 @@ async function start(req: IRequest, res: IResponse): Promise<void> {
|
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
117
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
118
|
+
if (!isActive) return;
|
|
117
119
|
const registered = WorkerRegistry.list().includes(name);
|
|
118
120
|
|
|
119
121
|
if (!registered) {
|
|
@@ -138,6 +140,8 @@ async function stop(req: IRequest, res: IResponse): Promise<void> {
|
|
|
138
140
|
try {
|
|
139
141
|
const name = getParam(req, 'name');
|
|
140
142
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
143
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
144
|
+
if (!isActive) return;
|
|
141
145
|
await WorkerFactory.stop(name, persistenceOverride);
|
|
142
146
|
res.json({ ok: true, message: `Worker ${name} stopped` });
|
|
143
147
|
} catch (error) {
|
|
@@ -155,6 +159,8 @@ async function restart(req: IRequest, res: IResponse): Promise<void> {
|
|
|
155
159
|
try {
|
|
156
160
|
const name = getParam(req, 'name');
|
|
157
161
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
162
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
163
|
+
if (!isActive) return;
|
|
158
164
|
await WorkerFactory.restart(name, persistenceOverride);
|
|
159
165
|
res.json({ ok: true, message: `Worker ${name} restarted` });
|
|
160
166
|
} catch (error) {
|
|
@@ -190,6 +196,8 @@ async function setAutoStart(req: IRequest, res: IResponse): Promise<void> {
|
|
|
190
196
|
}
|
|
191
197
|
|
|
192
198
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
199
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
200
|
+
if (!isActive) return;
|
|
193
201
|
|
|
194
202
|
await WorkerFactory.setAutoStart(name, enabled, persistenceOverride);
|
|
195
203
|
|
|
@@ -209,6 +217,8 @@ async function pause(req: IRequest, res: IResponse): Promise<void> {
|
|
|
209
217
|
try {
|
|
210
218
|
const name = getParam(req, 'name');
|
|
211
219
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
220
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
221
|
+
if (!isActive) return;
|
|
212
222
|
await WorkerFactory.pause(name, persistenceOverride);
|
|
213
223
|
res.json({ ok: true, message: `Worker ${name} paused` });
|
|
214
224
|
} catch (error) {
|
|
@@ -226,6 +236,8 @@ async function resume(req: IRequest, res: IResponse): Promise<void> {
|
|
|
226
236
|
try {
|
|
227
237
|
const name = getParam(req, 'name');
|
|
228
238
|
const persistenceOverride = resolvePersistenceOverride(req);
|
|
239
|
+
const isActive = await ensureActiveWorker(name, persistenceOverride, res);
|
|
240
|
+
if (!isActive) return;
|
|
229
241
|
await WorkerFactory.resume(name, persistenceOverride);
|
|
230
242
|
res.json({ ok: true, message: `Worker ${name} resumed` });
|
|
231
243
|
} catch (error) {
|
|
@@ -305,6 +317,32 @@ const resolvePersistenceOverride = (
|
|
|
305
317
|
return undefined;
|
|
306
318
|
};
|
|
307
319
|
|
|
320
|
+
const ensureActiveWorker = async (
|
|
321
|
+
name: string | undefined,
|
|
322
|
+
persistenceOverride:
|
|
323
|
+
| { driver: 'memory' }
|
|
324
|
+
| { driver: 'redis'; redis: { env: true }; keyPrefix?: string }
|
|
325
|
+
| { driver: 'database'; connection?: string; table?: string }
|
|
326
|
+
| undefined,
|
|
327
|
+
res: IResponse
|
|
328
|
+
): Promise<boolean> => {
|
|
329
|
+
if (!name) return false;
|
|
330
|
+
|
|
331
|
+
const instance = WorkerFactory.get(name);
|
|
332
|
+
if (instance?.config?.activeStatus === false) {
|
|
333
|
+
res.setStatus(410).json({ error: 'Worker is inactive', code: 'WORKER_INACTIVE' });
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const persisted = await WorkerFactory.getPersisted(name, persistenceOverride);
|
|
338
|
+
if (persisted?.activeStatus === false) {
|
|
339
|
+
res.setStatus(410).json({ error: 'Worker is inactive', code: 'WORKER_INACTIVE' });
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return true;
|
|
344
|
+
};
|
|
345
|
+
|
|
308
346
|
/**
|
|
309
347
|
* Get a specific worker instance
|
|
310
348
|
* @param req.params.name - Worker name
|
|
@@ -354,9 +392,15 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
|
|
|
354
392
|
return;
|
|
355
393
|
}
|
|
356
394
|
|
|
357
|
-
//
|
|
395
|
+
// Remove immutable fields and prepare updates
|
|
358
396
|
const { name: _name, driver: _driver, ...updateData } = reqData; // Remove immutable fields
|
|
359
397
|
|
|
398
|
+
const processorValid = await validateProcessorSpecIfNeeded(updateData);
|
|
399
|
+
if (!processorValid) {
|
|
400
|
+
res.setStatus(400).json({ error: 'Processor spec could not be resolved' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
360
404
|
// Note: driver is determined by persistence configuration, not stored in worker record
|
|
361
405
|
const updatedRecord = {
|
|
362
406
|
...currentRecord,
|
|
@@ -367,39 +411,16 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
|
|
|
367
411
|
|
|
368
412
|
(updatedRecord.infrastructure as unknown as InfrastructureConfig).persistence.driver = driver;
|
|
369
413
|
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
// Persist merged record via WorkerFactory API
|
|
373
|
-
await WorkerFactory.update(
|
|
374
|
-
name,
|
|
375
|
-
updatedRecord as unknown as WorkerRecord,
|
|
376
|
-
persistenceOverride
|
|
377
|
-
);
|
|
378
|
-
Logger.info(`Worker ${name} persistence updated with fields:`, Object.keys(updateData));
|
|
379
|
-
} catch (persistError) {
|
|
380
|
-
Logger.warn(`Failed to persist some updates for ${name}`, persistError as Error);
|
|
381
|
-
// Continue with restart even if persistence update partially fails
|
|
382
|
-
}
|
|
414
|
+
await persistUpdatedRecord(name, updatedRecord, persistenceOverride, updateData);
|
|
383
415
|
|
|
384
|
-
// If worker is currently running, restart it to apply new configuration changes
|
|
385
|
-
// This ensures new concurrency, queue settings, and other config take effect
|
|
386
416
|
const currentInstance = WorkerFactory.get(name);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
restartError = (error as Error).message;
|
|
395
|
-
Logger.warn(`Failed to restart worker ${name} after update`, error as Error);
|
|
396
|
-
// Don't fail the update, but warn about restart failure
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
Logger.info(
|
|
400
|
-
`Worker ${name} is not running (status: ${currentInstance?.status || 'not found'}), skipping restart`
|
|
401
|
-
);
|
|
402
|
-
}
|
|
417
|
+
const restartError = await restartIfNeeded(
|
|
418
|
+
name,
|
|
419
|
+
currentInstance,
|
|
420
|
+
updatedRecord,
|
|
421
|
+
currentRecord,
|
|
422
|
+
persistenceOverride
|
|
423
|
+
);
|
|
403
424
|
|
|
404
425
|
// Worker configuration updated in persistence and memory
|
|
405
426
|
Logger.info(`Worker configuration updated: ${name}`, {
|
|
@@ -420,6 +441,64 @@ async function update(req: IRequest, res: IResponse): Promise<void> {
|
|
|
420
441
|
}
|
|
421
442
|
}
|
|
422
443
|
|
|
444
|
+
// Helpers extracted from update() to reduce complexity
|
|
445
|
+
async function validateProcessorSpecIfNeeded(
|
|
446
|
+
updateData: Record<string, unknown>
|
|
447
|
+
): Promise<boolean> {
|
|
448
|
+
if (typeof updateData['processorSpec'] === 'string') {
|
|
449
|
+
const resolved = await WorkerFactory.resolveProcessorSpec(
|
|
450
|
+
updateData['processorSpec'] as string
|
|
451
|
+
);
|
|
452
|
+
return Boolean(resolved);
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function persistUpdatedRecord(
|
|
458
|
+
name: string,
|
|
459
|
+
updatedRecord: WorkerRecord | unknown,
|
|
460
|
+
persistenceOverride: ReturnType<typeof resolvePersistenceOverride> | undefined,
|
|
461
|
+
updateData: Record<string, unknown>
|
|
462
|
+
): Promise<void> {
|
|
463
|
+
try {
|
|
464
|
+
await WorkerFactory.update(name, updatedRecord as unknown as WorkerRecord, persistenceOverride);
|
|
465
|
+
Logger.info(`Worker ${name} persistence updated with fields:`, Object.keys(updateData));
|
|
466
|
+
} catch (persistError) {
|
|
467
|
+
Logger.warn(`Failed to persist some updates for ${name}`, persistError as Error);
|
|
468
|
+
// Continue execution even if persistence update partially fails
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function restartIfNeeded(
|
|
473
|
+
name: string,
|
|
474
|
+
currentInstance: ReturnType<typeof WorkerFactory.get> | undefined,
|
|
475
|
+
updatedRecord: WorkerRecord,
|
|
476
|
+
currentRecord: WorkerRecord,
|
|
477
|
+
persistenceOverride: ReturnType<typeof resolvePersistenceOverride> | undefined
|
|
478
|
+
): Promise<string | undefined> {
|
|
479
|
+
if (
|
|
480
|
+
!currentInstance ||
|
|
481
|
+
currentInstance.status !== 'running' ||
|
|
482
|
+
updatedRecord.activeStatus === false ||
|
|
483
|
+
currentRecord.activeStatus === false
|
|
484
|
+
) {
|
|
485
|
+
Logger.info(
|
|
486
|
+
`Worker ${name} is not running (status: ${currentInstance?.status || 'not found'}), skipping restart`
|
|
487
|
+
);
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
Logger.info(`Restarting worker ${name} to apply configuration changes`);
|
|
493
|
+
await WorkerFactory.restart(name, persistenceOverride);
|
|
494
|
+
return undefined;
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const restartError = (err as Error).message;
|
|
497
|
+
Logger.warn(`Failed to restart worker ${name} after update`, err as Error);
|
|
498
|
+
return restartError;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
423
502
|
/**
|
|
424
503
|
* Get worker status
|
|
425
504
|
* @param req.params.name - Worker name
|
|
@@ -590,7 +669,7 @@ async function getSlaStatus(req: IRequest, res: IResponse): Promise<void> {
|
|
|
590
669
|
} catch (error) {
|
|
591
670
|
Logger.error('WorkerController.getSlaStatus failed', error);
|
|
592
671
|
if ((error as Error).message.includes('SLA config not found')) {
|
|
593
|
-
res.setStatus(
|
|
672
|
+
res.setStatus(400).json({ error: 'SLA config not found for worker' });
|
|
594
673
|
} else {
|
|
595
674
|
res.setStatus(500).json({ error: (error as Error).message });
|
|
596
675
|
}
|
|
@@ -1257,65 +1336,63 @@ async function monitoringSummary(_req: IRequest, res: IResponse): Promise<void>
|
|
|
1257
1336
|
}
|
|
1258
1337
|
}
|
|
1259
1338
|
|
|
1339
|
+
const SSE_HEARTBEAT_INTERVAL = Env.SSE_HEARTBEAT_INTERVAL;
|
|
1340
|
+
|
|
1260
1341
|
/**
|
|
1261
1342
|
* SSE endpoint: stream worker and monitoring events
|
|
1262
1343
|
* GET /api/workers/events
|
|
1263
1344
|
*/
|
|
1264
1345
|
const eventsStream = async (_req: IRequest, res: IResponse): Promise<void> => {
|
|
1265
|
-
|
|
1346
|
+
try {
|
|
1347
|
+
const raw = res.getRaw();
|
|
1348
|
+
raw.writeHead(200, {
|
|
1349
|
+
'Content-Type': 'text/event-stream',
|
|
1350
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
1351
|
+
Connection: 'keep-alive',
|
|
1352
|
+
'X-Accel-Buffering': 'no',
|
|
1353
|
+
});
|
|
1266
1354
|
|
|
1267
|
-
|
|
1268
|
-
'Content-Type': 'text/event-stream',
|
|
1269
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
1270
|
-
Connection: 'keep-alive',
|
|
1271
|
-
'X-Accel-Buffering': 'no',
|
|
1272
|
-
});
|
|
1355
|
+
let closed = false;
|
|
1273
1356
|
|
|
1274
|
-
|
|
1357
|
+
const send = (payload: unknown) => {
|
|
1358
|
+
if (closed) return;
|
|
1359
|
+
try {
|
|
1360
|
+
const data = JSON.stringify(payload);
|
|
1361
|
+
raw.write(`data: ${data}\n\n`);
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
Logger.error('WorkerController.eventsStream serialization failed', err);
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1275
1366
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
// send error event
|
|
1304
|
-
await send({ type: 'error', ts: new Date().toISOString(), message: (err as Error).message });
|
|
1367
|
+
// Send hello immediately
|
|
1368
|
+
send({ type: 'hello', ts: new Date().toISOString() });
|
|
1369
|
+
|
|
1370
|
+
// Defined subscription callback
|
|
1371
|
+
const onSnapshot = (data: unknown) => {
|
|
1372
|
+
send(data);
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
// Subscribe to centralized service
|
|
1376
|
+
WorkerMonitoringService.subscribe(onSnapshot);
|
|
1377
|
+
|
|
1378
|
+
// Heartbeat to keep connection alive
|
|
1379
|
+
const hb = setInterval(() => {
|
|
1380
|
+
if (!closed) raw.write(': ping\n\n');
|
|
1381
|
+
}, SSE_HEARTBEAT_INTERVAL);
|
|
1382
|
+
|
|
1383
|
+
// Clean up when client disconnects
|
|
1384
|
+
raw.on('close', () => {
|
|
1385
|
+
closed = true;
|
|
1386
|
+
clearInterval(hb);
|
|
1387
|
+
WorkerMonitoringService.unsubscribe(onSnapshot);
|
|
1388
|
+
});
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
Logger.error('WorkerController.eventsStream failed', error);
|
|
1391
|
+
const raw = res.getRaw && typeof res.getRaw === 'function' ? res.getRaw() : null;
|
|
1392
|
+
if (!raw?.headersSent) {
|
|
1393
|
+
res.setStatus(500).json({ error: (error as Error).message });
|
|
1305
1394
|
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
// Heartbeat to keep connection alive
|
|
1309
|
-
const hb = setInterval(() => {
|
|
1310
|
-
if (!closed) raw.write(': ping\n\n');
|
|
1311
|
-
}, 15000);
|
|
1312
|
-
|
|
1313
|
-
// Clean up when client disconnects
|
|
1314
|
-
raw.on('close', () => {
|
|
1315
|
-
closed = true;
|
|
1316
|
-
clearInterval(interval);
|
|
1317
|
-
clearInterval(hb);
|
|
1318
|
-
});
|
|
1395
|
+
}
|
|
1319
1396
|
};
|
|
1320
1397
|
|
|
1321
1398
|
/**
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Logger, NodeSingletons, workersConfig } from '@zintrust/core';
|
|
2
|
+
import { HealthMonitor } from '../HealthMonitor';
|
|
3
|
+
import { getWorkers } from '../dashboard/workers-api';
|
|
4
|
+
|
|
5
|
+
type SnapshotData = {
|
|
6
|
+
type: string;
|
|
7
|
+
ts: string;
|
|
8
|
+
monitoring: unknown;
|
|
9
|
+
workers: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Internal state
|
|
13
|
+
const emitter = new NodeSingletons.EventEmitter();
|
|
14
|
+
emitter.setMaxListeners(Infinity);
|
|
15
|
+
let interval: NodeJS.Timeout | null = null;
|
|
16
|
+
let subscribers = 0;
|
|
17
|
+
const INTERVAL_MS = workersConfig?.intervalMs || 5000;
|
|
18
|
+
|
|
19
|
+
const broadcastSnapshot = async (): Promise<void> => {
|
|
20
|
+
try {
|
|
21
|
+
if (subscribers <= 0) return;
|
|
22
|
+
|
|
23
|
+
const monitoring = await HealthMonitor.getSummary();
|
|
24
|
+
// Fetch full workers listing optimized for dashboard
|
|
25
|
+
const workersPayload = await getWorkers({ page: 1, limit: 200 });
|
|
26
|
+
|
|
27
|
+
const payload: SnapshotData = {
|
|
28
|
+
type: 'snapshot',
|
|
29
|
+
ts: new Date().toISOString(),
|
|
30
|
+
monitoring,
|
|
31
|
+
workers: workersPayload,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
emitter.emit('snapshot', payload);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
Logger.error('WorkerMonitoringService.broadcastSnapshot failed', err);
|
|
37
|
+
emitter.emit('error', err);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const startPolling = (): void => {
|
|
42
|
+
if (interval) return;
|
|
43
|
+
|
|
44
|
+
Logger.debug('Starting WorkerMonitoringService polling');
|
|
45
|
+
// Initial fetch
|
|
46
|
+
void broadcastSnapshot();
|
|
47
|
+
|
|
48
|
+
interval = setInterval(() => {
|
|
49
|
+
void broadcastSnapshot();
|
|
50
|
+
}, INTERVAL_MS);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const stopPolling = (): void => {
|
|
54
|
+
if (interval) {
|
|
55
|
+
Logger.debug('Stopping WorkerMonitoringService polling');
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
interval = null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WorkerMonitoringService = Object.freeze({
|
|
62
|
+
subscribe(callback: (data: SnapshotData) => void): void {
|
|
63
|
+
emitter.on('snapshot', callback);
|
|
64
|
+
subscribers++;
|
|
65
|
+
if (subscribers === 1) {
|
|
66
|
+
startPolling();
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
unsubscribe(callback: (data: SnapshotData) => void): void {
|
|
71
|
+
emitter.off('snapshot', callback);
|
|
72
|
+
subscribers--;
|
|
73
|
+
if (subscribers <= 0) {
|
|
74
|
+
stopPolling();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -14,7 +14,7 @@ import { withWorkerNameValidation } from './WorkerNameSanitizer';
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Composite middleware for worker edit validation
|
|
17
|
-
* Maps
|
|
17
|
+
* Maps processorSpec to processor for validation and validates all editable fields
|
|
18
18
|
*/
|
|
19
19
|
export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler => {
|
|
20
20
|
return async (req: IRequest, res: IResponse): Promise<void> => {
|
|
@@ -22,12 +22,12 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
|
|
|
22
22
|
const data = req.data();
|
|
23
23
|
const currentBody = req.getBody() as Record<string, unknown>;
|
|
24
24
|
|
|
25
|
-
// Map
|
|
25
|
+
// Map processorSpec/processorSpec to processor for validation if provided
|
|
26
26
|
let mappedBody = { ...currentBody };
|
|
27
|
-
if (data['
|
|
27
|
+
if (data['processorSpec'] && !data['processor']) {
|
|
28
28
|
mappedBody = {
|
|
29
29
|
...mappedBody,
|
|
30
|
-
processor: data['
|
|
30
|
+
processor: data['processorSpec'],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -39,8 +39,8 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
|
|
|
39
39
|
[
|
|
40
40
|
'name',
|
|
41
41
|
'queueName',
|
|
42
|
-
'processor', // Validated field (mapped from
|
|
43
|
-
'
|
|
42
|
+
'processor', // Validated field (mapped from processorSpec)
|
|
43
|
+
'processorSpec',
|
|
44
44
|
'version',
|
|
45
45
|
'options', // Skip strict validation for editing
|
|
46
46
|
'infrastructure',
|
|
@@ -49,6 +49,7 @@ export const withEditWorkerValidation = (handler: RouteHandler): RouteHandler =>
|
|
|
49
49
|
'concurrency', // Original field
|
|
50
50
|
'region',
|
|
51
51
|
'autoStart',
|
|
52
|
+
'activeStatus',
|
|
52
53
|
'status',
|
|
53
54
|
],
|
|
54
55
|
withProcessorPathValidation(
|