@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
package/src/WorkerShutdown.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Coordinates orderly shutdown of all worker modules and the WorkerFactory.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { Logger } from '@zintrust/core';
|
|
8
|
+
import { Cloudflare, Logger } from '@zintrust/core';
|
|
9
9
|
import { WorkerFactory } from './WorkerFactory';
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
@@ -36,6 +36,12 @@ interface IShutdownState {
|
|
|
36
36
|
reason: string | null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
type DurableShutdownState = {
|
|
40
|
+
shuttingDown: boolean;
|
|
41
|
+
startedAt?: string;
|
|
42
|
+
reason?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
39
45
|
// ============================================================================
|
|
40
46
|
// Implementation
|
|
41
47
|
// ============================================================================
|
|
@@ -47,8 +53,67 @@ const state: IShutdownState = {
|
|
|
47
53
|
reason: null,
|
|
48
54
|
};
|
|
49
55
|
|
|
56
|
+
const getDurableShutdownStub = (): {
|
|
57
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
58
|
+
} | null => {
|
|
59
|
+
const env = Cloudflare.getWorkersEnv();
|
|
60
|
+
if (env === null) return null;
|
|
61
|
+
|
|
62
|
+
const namespace = env['WORKER_SHUTDOWN'] as
|
|
63
|
+
| {
|
|
64
|
+
idFromName?: (name: string) => unknown;
|
|
65
|
+
get?: (id: unknown) => {
|
|
66
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
| undefined;
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
!namespace ||
|
|
73
|
+
typeof namespace.idFromName !== 'function' ||
|
|
74
|
+
typeof namespace.get !== 'function'
|
|
75
|
+
) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = namespace.idFromName('zintrust-shutdown');
|
|
80
|
+
return namespace.get(id) ?? null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const requestDurableShutdown = async (reason = 'manual'): Promise<boolean> => {
|
|
84
|
+
const stub = getDurableShutdownStub();
|
|
85
|
+
if (!stub) {
|
|
86
|
+
Logger.warn('Worker shutdown Durable Object binding not configured');
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const res = await stub.fetch('https://worker-shutdown/shutdown', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'content-type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ reason }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return res.ok;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getDurableShutdownState = async (): Promise<DurableShutdownState | null> => {
|
|
100
|
+
const stub = getDurableShutdownStub();
|
|
101
|
+
if (!stub) return null;
|
|
102
|
+
|
|
103
|
+
const res = await stub.fetch('https://worker-shutdown/status');
|
|
104
|
+
if (!res.ok) return null;
|
|
105
|
+
return (await res.json()) as DurableShutdownState;
|
|
106
|
+
};
|
|
107
|
+
|
|
50
108
|
let shutdownHandlersRegistered = false;
|
|
51
109
|
|
|
110
|
+
const signalHandlers: {
|
|
111
|
+
sigterm?: () => Promise<void>;
|
|
112
|
+
sighup?: () => Promise<void>;
|
|
113
|
+
uncaughtException?: (error: Error) => Promise<void>;
|
|
114
|
+
unhandledRejection?: (reason: unknown) => void;
|
|
115
|
+
} = {};
|
|
116
|
+
|
|
52
117
|
/**
|
|
53
118
|
* Perform graceful shutdown of all worker modules
|
|
54
119
|
*/
|
|
@@ -115,14 +180,15 @@ function registerShutdownHandlers(): void {
|
|
|
115
180
|
Logger.debug('Registering worker management system shutdown handlers');
|
|
116
181
|
|
|
117
182
|
// SIGTERM - graceful shutdown (Docker, systemd, etc.)
|
|
118
|
-
|
|
183
|
+
signalHandlers.sigterm = async (): Promise<void> => {
|
|
119
184
|
Logger.info('📨 Received SIGTERM signal');
|
|
120
185
|
try {
|
|
121
186
|
await shutdown({ signal: 'SIGTERM', timeout: 30000, forceExit: true });
|
|
122
187
|
} catch (error) {
|
|
123
188
|
Logger.error('Error during SIGTERM shutdown', error);
|
|
124
189
|
}
|
|
125
|
-
}
|
|
190
|
+
};
|
|
191
|
+
process.on('SIGTERM', signalHandlers.sigterm);
|
|
126
192
|
|
|
127
193
|
// SIGINT - user interrupt (Ctrl+C) - REMOVED: handled by bootstrap.ts to prevent race condition
|
|
128
194
|
// process.on('SIGINT', async () => {
|
|
@@ -135,17 +201,18 @@ function registerShutdownHandlers(): void {
|
|
|
135
201
|
// });
|
|
136
202
|
|
|
137
203
|
// SIGHUP - terminal closed
|
|
138
|
-
|
|
204
|
+
signalHandlers.sighup = async (): Promise<void> => {
|
|
139
205
|
Logger.info('📨 Received SIGHUP signal');
|
|
140
206
|
try {
|
|
141
207
|
await shutdown({ signal: 'SIGHUP', timeout: 30000, forceExit: true });
|
|
142
208
|
} catch (error) {
|
|
143
209
|
Logger.error('Error during SIGHUP shutdown', error);
|
|
144
210
|
}
|
|
145
|
-
}
|
|
211
|
+
};
|
|
212
|
+
process.on('SIGHUP', signalHandlers.sighup);
|
|
146
213
|
|
|
147
214
|
// Handle uncaught errors during shutdown
|
|
148
|
-
|
|
215
|
+
signalHandlers.uncaughtException = async (error: Error): Promise<void> => {
|
|
149
216
|
Logger.error('💥 Uncaught exception during worker operations', error);
|
|
150
217
|
try {
|
|
151
218
|
await shutdown({ signal: 'uncaughtException', timeout: 10000, forceExit: true });
|
|
@@ -153,19 +220,43 @@ function registerShutdownHandlers(): void {
|
|
|
153
220
|
// Ignore errors during emergency shutdown
|
|
154
221
|
}
|
|
155
222
|
process.exit(1);
|
|
156
|
-
}
|
|
223
|
+
};
|
|
224
|
+
process.on('uncaughtException', signalHandlers.uncaughtException);
|
|
157
225
|
|
|
158
|
-
|
|
226
|
+
signalHandlers.unhandledRejection = (reason: unknown): void => {
|
|
159
227
|
// Only log the error - don't shut down the entire application
|
|
160
228
|
Logger.error('💥 Unhandled promise rejection detected', reason);
|
|
161
229
|
Logger.warn('⚠️ This error has been logged but will not shut down the server');
|
|
162
230
|
Logger.warn('⚠️ Check the error context and fix the underlying issue');
|
|
163
|
-
}
|
|
231
|
+
};
|
|
232
|
+
process.on('unhandledRejection', signalHandlers.unhandledRejection);
|
|
164
233
|
|
|
165
234
|
shutdownHandlersRegistered = true;
|
|
166
235
|
Logger.debug('Worker management system shutdown handlers registered');
|
|
167
236
|
}
|
|
168
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Unregister process signal handlers (for hot reload/testing)
|
|
240
|
+
*/
|
|
241
|
+
function unregisterShutdownHandlers(): void {
|
|
242
|
+
if (!shutdownHandlersRegistered) return;
|
|
243
|
+
|
|
244
|
+
if (signalHandlers.sigterm) process.off('SIGTERM', signalHandlers.sigterm);
|
|
245
|
+
if (signalHandlers.sighup) process.off('SIGHUP', signalHandlers.sighup);
|
|
246
|
+
if (signalHandlers.uncaughtException)
|
|
247
|
+
process.off('uncaughtException', signalHandlers.uncaughtException);
|
|
248
|
+
if (signalHandlers.unhandledRejection)
|
|
249
|
+
process.off('unhandledRejection', signalHandlers.unhandledRejection);
|
|
250
|
+
|
|
251
|
+
signalHandlers.sigterm = undefined;
|
|
252
|
+
signalHandlers.sighup = undefined;
|
|
253
|
+
signalHandlers.uncaughtException = undefined;
|
|
254
|
+
signalHandlers.unhandledRejection = undefined;
|
|
255
|
+
|
|
256
|
+
shutdownHandlersRegistered = false;
|
|
257
|
+
Logger.debug('Worker management system shutdown handlers unregistered');
|
|
258
|
+
}
|
|
259
|
+
|
|
169
260
|
/**
|
|
170
261
|
* Check if system is currently shutting down
|
|
171
262
|
*/
|
|
@@ -195,6 +286,11 @@ export const WorkerShutdown = Object.freeze({
|
|
|
195
286
|
*/
|
|
196
287
|
registerShutdownHandlers,
|
|
197
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Unregister process signal handlers (for hot reload/testing)
|
|
291
|
+
*/
|
|
292
|
+
unregisterShutdownHandlers,
|
|
293
|
+
|
|
198
294
|
/**
|
|
199
295
|
* Check if system is currently shutting down
|
|
200
296
|
*/
|
|
@@ -204,4 +300,14 @@ export const WorkerShutdown = Object.freeze({
|
|
|
204
300
|
* Get current shutdown state
|
|
205
301
|
*/
|
|
206
302
|
getShutdownState,
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Request shutdown via Durable Object (Workers)
|
|
306
|
+
*/
|
|
307
|
+
requestDurableShutdown,
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read shutdown state from Durable Object (Workers)
|
|
311
|
+
*/
|
|
312
|
+
getDurableShutdownState,
|
|
207
313
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Logger } from '@zintrust/core';
|
|
2
|
+
|
|
3
|
+
type DurableObjectState = {
|
|
4
|
+
storage: {
|
|
5
|
+
get: (key: string) => Promise<unknown>;
|
|
6
|
+
put: (key: string, value: unknown) => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ShutdownState = {
|
|
11
|
+
shuttingDown: boolean;
|
|
12
|
+
startedAt?: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const loadState = async (state: DurableObjectState): Promise<ShutdownState> => {
|
|
17
|
+
const stored = (await state.storage.get('shutdown')) as ShutdownState | undefined;
|
|
18
|
+
return stored ?? { shuttingDown: false };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const saveState = async (state: DurableObjectState, value: ShutdownState): Promise<void> => {
|
|
22
|
+
await state.storage.put('shutdown', value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
26
|
+
export class ZinTrustWorkerShutdownDurableObject {
|
|
27
|
+
private readonly state: DurableObjectState;
|
|
28
|
+
|
|
29
|
+
constructor(state: DurableObjectState) {
|
|
30
|
+
this.state = state;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async fetch(request: Request): Promise<Response> {
|
|
34
|
+
const url = new URL(request.url);
|
|
35
|
+
const path = url.pathname;
|
|
36
|
+
|
|
37
|
+
if (request.method === 'GET' && path === '/status') {
|
|
38
|
+
const current = await loadState(this.state);
|
|
39
|
+
return new Response(JSON.stringify(current), {
|
|
40
|
+
status: 200,
|
|
41
|
+
headers: { 'content-type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (request.method === 'POST' && path === '/shutdown') {
|
|
46
|
+
const payload = (await request.json().catch(() => ({}))) as { reason?: string };
|
|
47
|
+
const next: ShutdownState = {
|
|
48
|
+
shuttingDown: true,
|
|
49
|
+
startedAt: new Date().toISOString(),
|
|
50
|
+
reason: payload.reason ?? 'manual',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
await saveState(this.state, next);
|
|
54
|
+
Logger.info('Worker shutdown requested via Durable Object', next);
|
|
55
|
+
|
|
56
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
57
|
+
status: 202,
|
|
58
|
+
headers: { 'content-type': 'application/json' },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response('Not Found', { status: 404 });
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/createQueueWorker.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueueMessage } from '@zintrust/core';
|
|
1
|
+
import type { BullMQPayload, QueueMessage } from '@zintrust/core';
|
|
2
2
|
import { Logger, Queue } from '@zintrust/core';
|
|
3
3
|
|
|
4
4
|
type QueueWorker = {
|
|
@@ -8,11 +8,13 @@ type QueueWorker = {
|
|
|
8
8
|
queueName?: string;
|
|
9
9
|
driverName?: string;
|
|
10
10
|
maxItems?: number;
|
|
11
|
+
maxDurationMs?: number;
|
|
11
12
|
}) => Promise<number>;
|
|
12
13
|
startWorker: (opts?: {
|
|
13
14
|
queueName?: string;
|
|
14
15
|
driverName?: string;
|
|
15
16
|
signal?: AbortSignal;
|
|
17
|
+
maxDurationMs?: number;
|
|
16
18
|
}) => Promise<number>;
|
|
17
19
|
};
|
|
18
20
|
|
|
@@ -54,7 +56,7 @@ const createProcessOne = <TPayload>(
|
|
|
54
56
|
dueAt: new Date(timestamp).toISOString(),
|
|
55
57
|
});
|
|
56
58
|
// Re-queue original payload
|
|
57
|
-
await Queue.enqueue(queueName, message.payload, driverName);
|
|
59
|
+
await Queue.enqueue(queueName, message.payload as BullMQPayload, driverName);
|
|
58
60
|
await Queue.ack(queueName, message.id, driverName);
|
|
59
61
|
return false;
|
|
60
62
|
}
|
|
@@ -75,7 +77,7 @@ const createProcessOne = <TPayload>(
|
|
|
75
77
|
});
|
|
76
78
|
|
|
77
79
|
if (attempts < options.maxAttempts) {
|
|
78
|
-
await Queue.enqueue(queueName, message.payload, driverName);
|
|
80
|
+
await Queue.enqueue(queueName, message.payload as BullMQPayload, driverName);
|
|
79
81
|
Logger.info(`${options.kindLabel} re-queued for retry`, {
|
|
80
82
|
...baseLogFields,
|
|
81
83
|
attempts: attempts + 1,
|
|
@@ -83,7 +85,8 @@ const createProcessOne = <TPayload>(
|
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
await Queue.ack(queueName, message.id, driverName);
|
|
86
|
-
return
|
|
88
|
+
// We processed the message (even if it failed), so return true to continue processing
|
|
89
|
+
return true;
|
|
87
90
|
}
|
|
88
91
|
};
|
|
89
92
|
};
|
|
@@ -109,29 +112,49 @@ const createProcessAll = (
|
|
|
109
112
|
const createRunOnce = (
|
|
110
113
|
defaultQueueName: string,
|
|
111
114
|
processOne: (queueName?: string, driverName?: string) => Promise<boolean>
|
|
112
|
-
): ((opts?: {
|
|
115
|
+
): ((opts?: {
|
|
116
|
+
queueName?: string;
|
|
117
|
+
driverName?: string;
|
|
118
|
+
maxItems?: number;
|
|
119
|
+
maxDurationMs?: number;
|
|
120
|
+
concurrency?: number;
|
|
121
|
+
}) => Promise<number>) => {
|
|
113
122
|
return async (opts = {}): Promise<number> => {
|
|
114
|
-
const {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
const {
|
|
124
|
+
queueName = defaultQueueName,
|
|
125
|
+
driverName,
|
|
126
|
+
maxItems,
|
|
127
|
+
maxDurationMs = 30000,
|
|
128
|
+
concurrency = 1,
|
|
129
|
+
} = opts;
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
let totalProcessed = 0;
|
|
132
|
+
|
|
133
|
+
// Helper for single worker loop
|
|
134
|
+
const runWorker = async (): Promise<number> => {
|
|
135
|
+
let workerProcessed = 0;
|
|
118
136
|
while (true) {
|
|
137
|
+
if (maxDurationMs > 0 && Date.now() - startTime > maxDurationMs) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
if (maxItems !== undefined && totalProcessed >= maxItems) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
// eslint-disable-next-line no-await-in-loop
|
|
120
145
|
const didProcess = await processOne(queueName, driverName);
|
|
121
146
|
if (!didProcess) break;
|
|
122
|
-
|
|
147
|
+
|
|
148
|
+
workerProcessed++;
|
|
149
|
+
totalProcessed++; // Shared counter (approximation in parallel)
|
|
123
150
|
}
|
|
124
|
-
return
|
|
125
|
-
}
|
|
151
|
+
return workerProcessed;
|
|
152
|
+
};
|
|
126
153
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const didProcess = await processOne(queueName, driverName);
|
|
130
|
-
if (!didProcess) break;
|
|
131
|
-
processed++;
|
|
132
|
-
}
|
|
154
|
+
// Run workers in parallel
|
|
155
|
+
await Promise.all(Array.from({ length: Math.max(1, concurrency) }).map(() => runWorker()));
|
|
133
156
|
|
|
134
|
-
return
|
|
157
|
+
return totalProcessed;
|
|
135
158
|
};
|
|
136
159
|
};
|
|
137
160
|
|
|
@@ -143,22 +166,42 @@ const createStartWorker = (
|
|
|
143
166
|
queueName?: string;
|
|
144
167
|
driverName?: string;
|
|
145
168
|
signal?: AbortSignal;
|
|
169
|
+
maxDurationMs?: number;
|
|
170
|
+
concurrency?: number;
|
|
146
171
|
}) => Promise<number>) => {
|
|
147
172
|
return async (opts = {}): Promise<number> => {
|
|
148
|
-
const {
|
|
173
|
+
const {
|
|
174
|
+
queueName = defaultQueueName,
|
|
175
|
+
driverName,
|
|
176
|
+
signal,
|
|
177
|
+
maxDurationMs = 300000,
|
|
178
|
+
concurrency = 1,
|
|
179
|
+
} = opts;
|
|
180
|
+
|
|
181
|
+
Logger.info(`Starting ${kindLabel} worker (drain-until-empty)`, { queueName, concurrency });
|
|
182
|
+
|
|
183
|
+
const startTime = Date.now();
|
|
184
|
+
let totalProcessed = 0;
|
|
185
|
+
|
|
186
|
+
const runWorker = async (): Promise<void> => {
|
|
187
|
+
while (signal?.aborted !== true) {
|
|
188
|
+
if (maxDurationMs > 0 && Date.now() - startTime > maxDurationMs) {
|
|
189
|
+
Logger.warn(`${kindLabel} worker timeout reached`, { queueName, totalProcessed });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
149
192
|
|
|
150
|
-
|
|
193
|
+
// eslint-disable-next-line no-await-in-loop
|
|
194
|
+
const didProcess = await processOne(queueName, driverName);
|
|
195
|
+
if (!didProcess) break;
|
|
196
|
+
totalProcessed++;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
151
199
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// eslint-disable-next-line no-await-in-loop
|
|
155
|
-
const didProcess = await processOne(queueName, driverName);
|
|
156
|
-
if (!didProcess) break;
|
|
157
|
-
processedCount++;
|
|
158
|
-
}
|
|
200
|
+
// Run workers in parallel
|
|
201
|
+
await Promise.all(Array.from({ length: Math.max(1, concurrency) }).map(() => runWorker()));
|
|
159
202
|
|
|
160
|
-
Logger.info(`${kindLabel} worker finished (queue drained)`, { queueName,
|
|
161
|
-
return
|
|
203
|
+
Logger.info(`${kindLabel} worker finished (queue drained)`, { queueName, totalProcessed });
|
|
204
|
+
return totalProcessed;
|
|
162
205
|
};
|
|
163
206
|
};
|
|
164
207
|
|
package/src/dashboard/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Worker Configuration Types
|
|
2
2
|
export interface WorkerConfiguration {
|
|
3
|
+
processorSpec?: string | null;
|
|
4
|
+
activeStatus?: boolean | null;
|
|
3
5
|
[key: string]: string | number | boolean | null | undefined | object;
|
|
4
6
|
}
|
|
5
7
|
|
|
@@ -49,6 +51,7 @@ export interface WorkerData {
|
|
|
49
51
|
avgTime: number;
|
|
50
52
|
memory: number;
|
|
51
53
|
autoStart: boolean;
|
|
54
|
+
activeStatus?: boolean;
|
|
52
55
|
details?: {
|
|
53
56
|
configuration: WorkerConfiguration;
|
|
54
57
|
health: WorkerHealth;
|
|
@@ -95,6 +98,7 @@ export type GetWorkersQuery = {
|
|
|
95
98
|
driver?: WorkerDriver;
|
|
96
99
|
search?: string;
|
|
97
100
|
includeDetails?: boolean;
|
|
101
|
+
includeInactive?: boolean;
|
|
98
102
|
};
|
|
99
103
|
|
|
100
104
|
// UI Options Types
|
|
@@ -116,6 +120,7 @@ export type RawWorkerData = {
|
|
|
116
120
|
processed?: number;
|
|
117
121
|
version?: string;
|
|
118
122
|
autoStart?: boolean;
|
|
123
|
+
activeStatus?: boolean;
|
|
119
124
|
queueName?: string;
|
|
120
125
|
details?: {
|
|
121
126
|
configuration: WorkerConfiguration;
|