@zintrust/workers 0.4.48 → 0.4.60
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/WorkerFactory.js +33 -13
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +50 -14
- package/dist/createQueueWorker.js +27 -0
- package/dist/dashboard/workers-api.js +6 -0
- package/dist/queueMonitorHistory.d.ts +17 -0
- package/dist/queueMonitorHistory.js +62 -0
- package/dist/ui/workers/index.html +202 -0
- package/dist/ui/workers/main.js +1952 -0
- package/dist/ui/workers/styles.css +1350 -0
- package/dist/ui/workers/zintrust.svg +30 -0
- package/package.json +2 -2
- package/src/WorkerFactory.ts +35 -12
- package/src/createQueueWorker.ts +30 -0
- package/src/dashboard/workers-api.ts +10 -0
- package/src/queueMonitorHistory.ts +114 -0
- package/src/types/queue-monitor.d.ts +49 -0
package/dist/WorkerFactory.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Sealed namespace for immutability
|
|
5
5
|
*/
|
|
6
6
|
import * as ZintrustCoreModule from '@zintrust/core';
|
|
7
|
-
import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
|
|
7
|
+
import { Cloudflare, createRedisConnection, databaseConfig, DatabaseConnectionRegistry, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
|
|
8
8
|
import { Worker } from 'bullmq';
|
|
9
9
|
import { AutoScaler } from './AutoScaler.js';
|
|
10
10
|
import { CanaryController } from './CanaryController.js';
|
|
@@ -23,6 +23,7 @@ import { WorkerMetrics } from './WorkerMetrics.js';
|
|
|
23
23
|
import { WorkerRegistry } from './WorkerRegistry.js';
|
|
24
24
|
import { WorkerVersioning } from './WorkerVersioning.js';
|
|
25
25
|
import { keyPrefix } from './config/workerConfig.js';
|
|
26
|
+
import { recordQueueMonitorJob } from './queueMonitorHistory.js';
|
|
26
27
|
import { DbWorkerStore, InMemoryWorkerStore, RedisWorkerStore, } from './storage/WorkerStore.js';
|
|
27
28
|
const path = NodeSingletons.path;
|
|
28
29
|
const isNodeRuntime = () => typeof process !== 'undefined' && Boolean(process.versions?.node);
|
|
@@ -63,7 +64,7 @@ const resolveRuntimeBridgeUrl = (specifier) => {
|
|
|
63
64
|
const filePath = path.join(dir, `${safeName}.bridge.mjs`);
|
|
64
65
|
const exportLines = Object.keys(bridgeModule)
|
|
65
66
|
.filter((key) => key !== 'default' && isValidBridgeExportName(key))
|
|
66
|
-
.sort()
|
|
67
|
+
.sort((a, b) => a.localeCompare(b))
|
|
67
68
|
.map((key) => `export const ${key} = bridge[${JSON.stringify(key)}];`);
|
|
68
69
|
const code = [
|
|
69
70
|
'const bridgeMap = globalThis.__zintrustProcessorPackageBridges__;',
|
|
@@ -1544,19 +1545,25 @@ const resolvePersistenceConfig = (config) => {
|
|
|
1544
1545
|
throw ErrorFactory.createConfigError('WORKER_PERSISTENCE_DRIVER must be one of memory, redis, or database');
|
|
1545
1546
|
};
|
|
1546
1547
|
const resolveDbClientFromEnv = async (connectionName = 'default') => {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1548
|
+
// Eagerly populate the registry when the requested connection is not yet
|
|
1549
|
+
// registered. On both the Cloudflare Workers runtime and Node, the registry
|
|
1550
|
+
// may be empty when called from the workers-persistence path because
|
|
1551
|
+
// registerDatabasesFromRuntimeConfig has not yet run for this connection. The
|
|
1552
|
+
// old pattern of connecting first (which always fails) then registering as a
|
|
1553
|
+
// fallback produced spurious [DEBUG] noise on every fresh start.
|
|
1554
|
+
if (DatabaseConnectionRegistry.get(connectionName) === undefined) {
|
|
1555
|
+
try {
|
|
1556
|
+
registerDatabasesFromRuntimeConfig(databaseConfig);
|
|
1557
|
+
}
|
|
1558
|
+
catch (registrationError) {
|
|
1559
|
+
Logger.warn(`[WorkerPersistence] Runtime database registration failed for connection '${connectionName}'`, registrationError);
|
|
1560
|
+
}
|
|
1553
1561
|
}
|
|
1554
1562
|
try {
|
|
1555
|
-
|
|
1556
|
-
return await connect();
|
|
1563
|
+
return await useEnsureDbConnected(undefined, connectionName);
|
|
1557
1564
|
}
|
|
1558
1565
|
catch (error) {
|
|
1559
|
-
Logger.error('Worker persistence failed
|
|
1566
|
+
Logger.error('Worker persistence failed to resolve database connection', error);
|
|
1560
1567
|
throw ErrorFactory.createConfigError(`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`);
|
|
1561
1568
|
}
|
|
1562
1569
|
};
|
|
@@ -1899,10 +1906,15 @@ const initializeDatacenter = (config) => {
|
|
|
1899
1906
|
},
|
|
1900
1907
|
});
|
|
1901
1908
|
};
|
|
1902
|
-
const setupWorkerEventListeners = (worker, workerName, workerVersion, features) => {
|
|
1909
|
+
const setupWorkerEventListeners = (worker, workerName, queueName, workerVersion, features) => {
|
|
1903
1910
|
worker.on('completed', (job) => {
|
|
1904
1911
|
try {
|
|
1905
1912
|
Logger.debug(`Job completed: ${workerName}`, { jobId: job.id });
|
|
1913
|
+
void recordQueueMonitorJob({
|
|
1914
|
+
queueName,
|
|
1915
|
+
status: 'completed',
|
|
1916
|
+
job,
|
|
1917
|
+
});
|
|
1906
1918
|
if (features?.observability === true) {
|
|
1907
1919
|
Observability.incrementCounter('worker.jobs.completed', 1, {
|
|
1908
1920
|
worker: workerName,
|
|
@@ -1918,6 +1930,14 @@ const setupWorkerEventListeners = (worker, workerName, workerVersion, features)
|
|
|
1918
1930
|
worker.on('failed', (job, error) => {
|
|
1919
1931
|
try {
|
|
1920
1932
|
Logger.error(`Job failed: ${workerName}`, { error, jobId: job?.id }, 'workers');
|
|
1933
|
+
if (job) {
|
|
1934
|
+
void recordQueueMonitorJob({
|
|
1935
|
+
queueName,
|
|
1936
|
+
status: 'failed',
|
|
1937
|
+
job,
|
|
1938
|
+
error,
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1921
1941
|
if (features?.observability === true) {
|
|
1922
1942
|
Observability.incrementCounter('worker.jobs.failed', 1, {
|
|
1923
1943
|
worker: workerName,
|
|
@@ -2081,7 +2101,7 @@ export const WorkerFactory = Object.freeze({
|
|
|
2081
2101
|
// Create BullMQ worker
|
|
2082
2102
|
const resolvedOptions = resolveWorkerOptions(config, autoStart);
|
|
2083
2103
|
const worker = new Worker(queueName, enhancedProcessor, resolvedOptions);
|
|
2084
|
-
setupWorkerEventListeners(worker, name, workerVersion, features);
|
|
2104
|
+
setupWorkerEventListeners(worker, name, queueName, workerVersion, features);
|
|
2085
2105
|
// Update status to "starting"
|
|
2086
2106
|
await store.update(name, {
|
|
2087
2107
|
status: WorkerCreationStatus.STARTING,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type DurableObjectState = {
|
|
2
|
+
storage: {
|
|
3
|
+
get: (key: string) => Promise<unknown>;
|
|
4
|
+
put: (key: string, value: unknown) => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
export declare class ZinTrustWorkerShutdownDurableObject {
|
|
8
|
+
private readonly state;
|
|
9
|
+
constructor(state: DurableObjectState);
|
|
10
|
+
fetch(request: Request): Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Logger } from '@zintrust/core';
|
|
2
|
+
const loadState = async (state) => {
|
|
3
|
+
const stored = (await state.storage.get('shutdown'));
|
|
4
|
+
return stored ?? { shuttingDown: false };
|
|
5
|
+
};
|
|
6
|
+
const saveState = async (state, value) => {
|
|
7
|
+
await state.storage.put('shutdown', value);
|
|
8
|
+
};
|
|
9
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
10
|
+
export class ZinTrustWorkerShutdownDurableObject {
|
|
11
|
+
state;
|
|
12
|
+
constructor(state) {
|
|
13
|
+
this.state = state;
|
|
14
|
+
}
|
|
15
|
+
async fetch(request) {
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
const path = url.pathname;
|
|
18
|
+
if (request.method === 'GET' && path === '/status') {
|
|
19
|
+
const current = await loadState(this.state);
|
|
20
|
+
return new Response(JSON.stringify(current), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
if (request.method === 'POST' && path === '/shutdown') {
|
|
26
|
+
const payload = (await request.json().catch(() => ({})));
|
|
27
|
+
const next = {
|
|
28
|
+
shuttingDown: true,
|
|
29
|
+
startedAt: new Date().toISOString(),
|
|
30
|
+
reason: payload.reason ?? 'manual',
|
|
31
|
+
};
|
|
32
|
+
await saveState(this.state, next);
|
|
33
|
+
Logger.info('Worker shutdown requested via Durable Object', next);
|
|
34
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
35
|
+
status: 202,
|
|
36
|
+
headers: { 'content-type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return new Response('Not Found', { status: 404 });
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/workers",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"buildDate": "2026-04-
|
|
3
|
+
"version": "0.4.60",
|
|
4
|
+
"buildDate": "2026-04-05T07:03:43.939Z",
|
|
5
5
|
"buildEnvironment": {
|
|
6
|
-
"node": "
|
|
7
|
-
"platform": "
|
|
8
|
-
"arch": "
|
|
6
|
+
"node": "v22.22.1",
|
|
7
|
+
"platform": "darwin",
|
|
8
|
+
"arch": "arm64"
|
|
9
9
|
},
|
|
10
10
|
"git": {
|
|
11
|
-
"commit": "
|
|
12
|
-
"branch": "
|
|
11
|
+
"commit": "be058c1d",
|
|
12
|
+
"branch": "release"
|
|
13
13
|
},
|
|
14
14
|
"package": {
|
|
15
15
|
"engines": {
|
|
@@ -178,8 +178,8 @@
|
|
|
178
178
|
"sha256": "3869f960c87260588e40941ff91bffcfa0757be7a04815fd28b57dd4840c51df"
|
|
179
179
|
},
|
|
180
180
|
"WorkerFactory.js": {
|
|
181
|
-
"size":
|
|
182
|
-
"sha256": "
|
|
181
|
+
"size": 105839,
|
|
182
|
+
"sha256": "b26135593d6e3849f5540d74d097082d8186b50c72a456722f83d624141c42c8"
|
|
183
183
|
},
|
|
184
184
|
"WorkerInit.d.ts": {
|
|
185
185
|
"size": 3284,
|
|
@@ -213,6 +213,14 @@
|
|
|
213
213
|
"size": 6643,
|
|
214
214
|
"sha256": "bef3a37ebc8292f4f548d5f66890ef7d6dd386e39c4c1cc61db058fd8815c927"
|
|
215
215
|
},
|
|
216
|
+
"WorkerShutdownDurableObject.d.ts": {
|
|
217
|
+
"size": 354,
|
|
218
|
+
"sha256": "521df11172067bc036bc7b95ff6adf40b7c594afa0454742963317ddfe776d37"
|
|
219
|
+
},
|
|
220
|
+
"WorkerShutdownDurableObject.js": {
|
|
221
|
+
"size": 1547,
|
|
222
|
+
"sha256": "9c7298133dfc4073fda80d3fe9a37ac00d31cacf500ce10c1dd43a70c3a8ddd7"
|
|
223
|
+
},
|
|
216
224
|
"WorkerVersioning.d.ts": {
|
|
217
225
|
"size": 2881,
|
|
218
226
|
"sha256": "a3f1f9e518e8a46201b181679bb5d7a484da7c021b4b7cfd5653cf98fe36e995"
|
|
@@ -221,6 +229,10 @@
|
|
|
221
229
|
"size": 10953,
|
|
222
230
|
"sha256": "8af20d462270e7044c6ea983821f5b6e6ce8a5caf39b6e8fefff07c9a0bf071e"
|
|
223
231
|
},
|
|
232
|
+
"build-manifest.json": {
|
|
233
|
+
"size": 19594,
|
|
234
|
+
"sha256": "7402721e5f5a98fb988fa78f5ee49b6e82f2f3fab1c3cfb8ca2bf8116012169f"
|
|
235
|
+
},
|
|
224
236
|
"config/workerConfig.d.ts": {
|
|
225
237
|
"size": 132,
|
|
226
238
|
"sha256": "577486dd9e0ef5b5c27d070e0f6a383337d9d68725fae0f0bad258254b828a3b"
|
|
@@ -234,8 +246,8 @@
|
|
|
234
246
|
"sha256": "dacd49f6c112eba439bdd9bb457eea90daedbf32efc381cd3189ce562fa5b0a8"
|
|
235
247
|
},
|
|
236
248
|
"createQueueWorker.js": {
|
|
237
|
-
"size":
|
|
238
|
-
"sha256": "
|
|
249
|
+
"size": 15594,
|
|
250
|
+
"sha256": "0710f2fb6936a326cdbe6fa6a2b8acfb60f7c6b27acb2aed0b98bf2402fc67f4"
|
|
239
251
|
},
|
|
240
252
|
"dashboard/index.d.ts": {
|
|
241
253
|
"size": 109,
|
|
@@ -258,8 +270,8 @@
|
|
|
258
270
|
"sha256": "8e0e04329e1119d8ae835dd4458efead084293bcc2c263c09dd5a19d467e5ca4"
|
|
259
271
|
},
|
|
260
272
|
"dashboard/workers-api.js": {
|
|
261
|
-
"size":
|
|
262
|
-
"sha256": "
|
|
273
|
+
"size": 28568,
|
|
274
|
+
"sha256": "c28f0d4b8134337a2cc71f1ce2f1d6195f44ccd9bb0b8fa26a4edaed88ee4fc0"
|
|
263
275
|
},
|
|
264
276
|
"helper/index.d.ts": {
|
|
265
277
|
"size": 159,
|
|
@@ -403,7 +415,15 @@
|
|
|
403
415
|
},
|
|
404
416
|
"index.js": {
|
|
405
417
|
"size": 2337,
|
|
406
|
-
"sha256": "
|
|
418
|
+
"sha256": "85a789d0f2ed71dde649f59658827ff16eaf2c1bc77df99b2297c7eb7e340108"
|
|
419
|
+
},
|
|
420
|
+
"queueMonitorHistory.d.ts": {
|
|
421
|
+
"size": 433,
|
|
422
|
+
"sha256": "1ba25cf47b0cad83e92c7b6481d12de974b921d0e01fc4cf17fc833acdf8b8c8"
|
|
423
|
+
},
|
|
424
|
+
"queueMonitorHistory.js": {
|
|
425
|
+
"size": 2194,
|
|
426
|
+
"sha256": "03d406adc046e18c3526837a654fae93b112fb6010ddc1163b129d10e934c4d4"
|
|
407
427
|
},
|
|
408
428
|
"register.d.ts": {
|
|
409
429
|
"size": 256,
|
|
@@ -524,6 +544,22 @@
|
|
|
524
544
|
"ui/types/worker-ui.js": {
|
|
525
545
|
"size": 94,
|
|
526
546
|
"sha256": "7e9f752c2a8eb29dab365b0c836bac90fa7d3567aaf961537d3aa782e85c884e"
|
|
547
|
+
},
|
|
548
|
+
"ui/workers/index.html": {
|
|
549
|
+
"size": 7940,
|
|
550
|
+
"sha256": "a71e384de568f720ff23bf5c90dca60cc97d30c0bbb4ccf5c3b026a8086381b4"
|
|
551
|
+
},
|
|
552
|
+
"ui/workers/main.js": {
|
|
553
|
+
"size": 60723,
|
|
554
|
+
"sha256": "8437771f28963e1b039bee0b1721a8e83a8c65bed19dbd89af0e37d5a2882bb4"
|
|
555
|
+
},
|
|
556
|
+
"ui/workers/styles.css": {
|
|
557
|
+
"size": 22652,
|
|
558
|
+
"sha256": "6de91f206251b0b69ca1a41b71b454b00e3b1213a9762abd8fd8f0ff0381556b"
|
|
559
|
+
},
|
|
560
|
+
"ui/workers/zintrust.svg": {
|
|
561
|
+
"size": 1035,
|
|
562
|
+
"sha256": "9b791269aad740b4191b0ae9f005b08bb28a3407a79a8ed38c11d225fa443f69"
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Core from '@zintrust/core';
|
|
2
2
|
import { Env, Logger, Queue } from '@zintrust/core';
|
|
3
|
+
import { recordQueueMonitorJob } from './queueMonitorHistory.js';
|
|
3
4
|
const TypedQueue = Queue;
|
|
4
5
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
5
6
|
const RETRY_MAX_DELAY_MS = 30000;
|
|
@@ -168,6 +169,18 @@ const checkAndRequeueIfNotDue = async (options, queueName, driverName, message,
|
|
|
168
169
|
};
|
|
169
170
|
const onProcessSuccess = async (input) => {
|
|
170
171
|
await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
|
|
172
|
+
await recordQueueMonitorJob({
|
|
173
|
+
queueName: input.queueName,
|
|
174
|
+
status: 'completed',
|
|
175
|
+
job: {
|
|
176
|
+
id: input.message.id,
|
|
177
|
+
name: `${input.queueName}-job`,
|
|
178
|
+
data: input.message.payload,
|
|
179
|
+
attemptsMade: getAttemptsFromMessage(input.message),
|
|
180
|
+
processedOn: input.startedAtMs,
|
|
181
|
+
finishedOn: Date.now(),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
171
184
|
if (typeof input.trackerApi.completed === 'function') {
|
|
172
185
|
await input.trackerApi.completed({
|
|
173
186
|
queueName: input.queueName,
|
|
@@ -217,6 +230,20 @@ const onProcessFailure = async (input) => {
|
|
|
217
230
|
});
|
|
218
231
|
}
|
|
219
232
|
await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
|
|
233
|
+
await recordQueueMonitorJob({
|
|
234
|
+
queueName: input.queueName,
|
|
235
|
+
status: 'failed',
|
|
236
|
+
job: {
|
|
237
|
+
id: input.message.id,
|
|
238
|
+
name: `${input.queueName}-job`,
|
|
239
|
+
data: input.message.payload,
|
|
240
|
+
attemptsMade: nextAttempts,
|
|
241
|
+
failedReason: failure.message,
|
|
242
|
+
processedOn: Date.now(),
|
|
243
|
+
finishedOn: Date.now(),
|
|
244
|
+
},
|
|
245
|
+
error: failure,
|
|
246
|
+
});
|
|
220
247
|
await removeHeartbeatIfSupported(input.queueName, input.message.id);
|
|
221
248
|
if (typeof input.trackerApi.failed === 'function') {
|
|
222
249
|
await input.trackerApi.failed({
|
|
@@ -457,6 +457,12 @@ async function getRedisQueueData() {
|
|
|
457
457
|
throw ErrorFactory.createConfigError('Redis driver not configured');
|
|
458
458
|
}
|
|
459
459
|
const monitor = QueueMonitor.create({
|
|
460
|
+
knownQueues: async () => {
|
|
461
|
+
const records = await WorkerFactory.listPersistedRecords();
|
|
462
|
+
return Array.from(new Set(records
|
|
463
|
+
.map((record) => record.queueName)
|
|
464
|
+
.filter((queueName) => typeof queueName === 'string'))).sort((left, right) => left.localeCompare(right));
|
|
465
|
+
},
|
|
460
466
|
redis: {
|
|
461
467
|
host: redisConfig.host || 'localhost',
|
|
462
468
|
port: redisConfig.port || 6379,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type QueueMonitorStatus = 'completed' | 'failed';
|
|
2
|
+
type QueueMonitorJobLike = {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
attemptsMade?: number;
|
|
7
|
+
failedReason?: string;
|
|
8
|
+
processedOn?: number;
|
|
9
|
+
finishedOn?: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const recordQueueMonitorJob: (input: {
|
|
12
|
+
queueName: string;
|
|
13
|
+
status: QueueMonitorStatus;
|
|
14
|
+
job: QueueMonitorJobLike;
|
|
15
|
+
error?: Error;
|
|
16
|
+
}) => Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isNonEmptyString, isObject, Logger, queueConfig } from '@zintrust/core';
|
|
2
|
+
let queueMonitorMetricsPromise = null;
|
|
3
|
+
const toFiniteInteger = (value, fallback) => {
|
|
4
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
5
|
+
return Math.floor(value);
|
|
6
|
+
}
|
|
7
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
if (Number.isFinite(parsed)) {
|
|
10
|
+
return Math.floor(parsed);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return fallback;
|
|
14
|
+
};
|
|
15
|
+
const resolveQueueMonitorRedisConfig = () => {
|
|
16
|
+
const redisConfig = queueConfig?.drivers?.redis;
|
|
17
|
+
if (!isObject(redisConfig) || redisConfig['driver'] !== 'redis') {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const host = isNonEmptyString(redisConfig['host']) ? redisConfig['host'].trim() : '127.0.0.1';
|
|
21
|
+
const port = toFiniteInteger(redisConfig['port'], 6379);
|
|
22
|
+
const db = toFiniteInteger(redisConfig['database'], 0);
|
|
23
|
+
const password = isNonEmptyString(redisConfig['password']) ? redisConfig['password'] : undefined;
|
|
24
|
+
return { host, port, password, db };
|
|
25
|
+
};
|
|
26
|
+
const loadQueueMonitorMetrics = async () => {
|
|
27
|
+
const redisConfig = resolveQueueMonitorRedisConfig();
|
|
28
|
+
if (redisConfig === null) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const module = (await import('@zintrust/queue-monitor'));
|
|
33
|
+
if (typeof module.createMetrics !== 'function') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return module.createMetrics(redisConfig);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
Logger.debug('Queue monitor metrics are unavailable for worker history recording', error);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const getQueueMonitorMetrics = async () => {
|
|
44
|
+
queueMonitorMetricsPromise ??= loadQueueMonitorMetrics();
|
|
45
|
+
return queueMonitorMetricsPromise;
|
|
46
|
+
};
|
|
47
|
+
export const recordQueueMonitorJob = async (input) => {
|
|
48
|
+
const metrics = await getQueueMonitorMetrics();
|
|
49
|
+
if (metrics === null) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await metrics.recordJob(input.queueName, input.status, input.job, input.error);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
Logger.debug('Queue monitor history write failed', {
|
|
57
|
+
queueName: input.queueName,
|
|
58
|
+
status: input.status,
|
|
59
|
+
error,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ZinTrust Workers Dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="workers/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="container">
|
|
11
|
+
<div class="header">
|
|
12
|
+
<div class="header-top">
|
|
13
|
+
<div style="display: flex; align-items: center; gap: 16px">
|
|
14
|
+
<div class="logo-frame">
|
|
15
|
+
<img src="workers/zintrust.svg" alt="ZinTrust" class="logo-img" />
|
|
16
|
+
</div>
|
|
17
|
+
<h1>ZinTrust Workers</h1>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="header-actions">
|
|
20
|
+
<button id="theme-toggle" class="theme-toggle">
|
|
21
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
22
|
+
<circle cx="12" cy="12" r="5" />
|
|
23
|
+
<path
|
|
24
|
+
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
Theme
|
|
28
|
+
</button>
|
|
29
|
+
<button id="auto-refresh-toggle" class="btn" onclick="toggleAutoRefresh()">
|
|
30
|
+
<svg id="auto-refresh-icon" class="icon" viewBox="0 0 24 24">
|
|
31
|
+
<polygon points="5 3 19 12 5 21 5 3" />
|
|
32
|
+
</svg>
|
|
33
|
+
<span id="auto-refresh-label">Auto Refresh</span>
|
|
34
|
+
</button>
|
|
35
|
+
<button class="btn" onclick="fetchData()">
|
|
36
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
37
|
+
<path d="M23 4v6h-6" />
|
|
38
|
+
<path d="M1 20v-6h6" />
|
|
39
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
|
40
|
+
</svg>
|
|
41
|
+
Refresh
|
|
42
|
+
</button>
|
|
43
|
+
<button class="btn btn-primary" onclick="showAddWorkerModal()">
|
|
44
|
+
<svg class="icon" viewBox="0 0 24 24">
|
|
45
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
46
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
47
|
+
</svg>
|
|
48
|
+
Add Worker
|
|
49
|
+
</button>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="nav-bar">
|
|
54
|
+
<nav class="nav-links">
|
|
55
|
+
<a href="/queue-monitor" class="nav-link">Queue Monitor</a>
|
|
56
|
+
<a href="/telemetry" class="nav-link">Telemetry</a>
|
|
57
|
+
<a href="/metrics" class="nav-link">Metrics</a>
|
|
58
|
+
</nav>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="filters-bar">
|
|
62
|
+
<div class="filter-group">
|
|
63
|
+
<span>Status:</span>
|
|
64
|
+
<select id="status-filter">
|
|
65
|
+
<option value="">All Status</option>
|
|
66
|
+
<option value="running">Running</option>
|
|
67
|
+
<option value="stopped">Stopped</option>
|
|
68
|
+
<option value="error">Error</option>
|
|
69
|
+
<option value="paused">Paused</option>
|
|
70
|
+
</select>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="filter-group">
|
|
73
|
+
<span>Driver:</span>
|
|
74
|
+
<select id="driver-filter">
|
|
75
|
+
<option value="">All Drivers</option>
|
|
76
|
+
</select>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="filter-group">
|
|
79
|
+
<span>Sort:</span>
|
|
80
|
+
<select id="sort-select">
|
|
81
|
+
<option value="name">Sort by Name</option>
|
|
82
|
+
<option value="status" selected>Sort by Status</option>
|
|
83
|
+
<option value="driver">Sort by Driver</option>
|
|
84
|
+
<option value="health">Sort by Health</option>
|
|
85
|
+
<option value="version">Sort by Version</option>
|
|
86
|
+
<option value="processed">Sort by Performance</option>
|
|
87
|
+
</select>
|
|
88
|
+
</div>
|
|
89
|
+
<div style="flex-grow: 1"></div>
|
|
90
|
+
<div class="search-box">
|
|
91
|
+
<svg class="search-icon" viewBox="0 0 24 24">
|
|
92
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
93
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
94
|
+
</svg>
|
|
95
|
+
<input type="text" id="search-input" placeholder="Search workers..." />
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div id="loading" style="text-align: center; padding: 40px; color: var(--muted)">
|
|
101
|
+
<div>Loading workers...</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div
|
|
105
|
+
id="error"
|
|
106
|
+
style="display: none; text-align: center; padding: 40px; color: var(--danger)"
|
|
107
|
+
>
|
|
108
|
+
<div>Failed to load workers data</div>
|
|
109
|
+
<button class="btn" onclick="fetchData()" style="margin-top: 16px">Retry</button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div id="workers-content" style="display: none">
|
|
113
|
+
<div class="summary-bar" id="queue-summary">
|
|
114
|
+
<div class="summary-item">
|
|
115
|
+
<span class="summary-label">Queue Driver</span>
|
|
116
|
+
<span class="summary-value" id="queue-driver">-</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="summary-item">
|
|
119
|
+
<span class="summary-label">Queues</span>
|
|
120
|
+
<span class="summary-value" id="queue-total">0</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="summary-item">
|
|
123
|
+
<span class="summary-label">Jobs</span>
|
|
124
|
+
<span class="summary-value" id="queue-jobs">0</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="summary-item">
|
|
127
|
+
<span class="summary-label">Processing</span>
|
|
128
|
+
<span class="summary-value" id="queue-processing">0</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="summary-item">
|
|
131
|
+
<span class="summary-label">Failed</span>
|
|
132
|
+
<span class="summary-value" id="queue-failed">0</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="summary-item">
|
|
135
|
+
<span class="summary-label">Drivers</span>
|
|
136
|
+
<div class="drivers-list" id="drivers-list"></div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="table-container">
|
|
140
|
+
<div class="table-wrapper">
|
|
141
|
+
<table>
|
|
142
|
+
<thead>
|
|
143
|
+
<tr>
|
|
144
|
+
<th style="width: 250px">Worker</th>
|
|
145
|
+
<th style="width: 120px">Status</th>
|
|
146
|
+
<th style="width: 120px">Health</th>
|
|
147
|
+
<th style="width: 100px">Driver</th>
|
|
148
|
+
<th style="width: 100px">Version</th>
|
|
149
|
+
<th style="width: 320px">Performance</th>
|
|
150
|
+
<th style="width: 180px">Actions</th>
|
|
151
|
+
</tr>
|
|
152
|
+
</thead>
|
|
153
|
+
<tbody id="workers-tbody">
|
|
154
|
+
<!-- Workers will be populated here -->
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="pagination">
|
|
160
|
+
<div class="pagination-info" id="pagination-info">Showing 0-0 of 0 workers</div>
|
|
161
|
+
<div class="pagination-controls">
|
|
162
|
+
<button class="page-btn" id="prev-btn" onclick="loadPage('prev')" disabled>
|
|
163
|
+
<svg
|
|
164
|
+
viewBox="0 0 24 24"
|
|
165
|
+
fill="none"
|
|
166
|
+
stroke="currentColor"
|
|
167
|
+
stroke-linecap="round"
|
|
168
|
+
stroke-linejoin="round"
|
|
169
|
+
>
|
|
170
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
171
|
+
</svg>
|
|
172
|
+
</button>
|
|
173
|
+
<div id="page-numbers" style="display: flex; gap: 8px"></div>
|
|
174
|
+
<button class="page-btn" id="next-btn" onclick="loadPage('next')" disabled>
|
|
175
|
+
<svg
|
|
176
|
+
viewBox="0 0 24 24"
|
|
177
|
+
fill="none"
|
|
178
|
+
stroke="currentColor"
|
|
179
|
+
stroke-linecap="round"
|
|
180
|
+
stroke-linejoin="round"
|
|
181
|
+
>
|
|
182
|
+
<polyline points="9 18 15 12 9 6"></polyline>
|
|
183
|
+
</svg>
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<div class="page-size-selector">
|
|
187
|
+
<span>Show:</span>
|
|
188
|
+
<select id="limit-select" onchange="changeLimit(this.value)">
|
|
189
|
+
<option value="10">10</option>
|
|
190
|
+
<option value="25">25</option>
|
|
191
|
+
<option value="50">50</option>
|
|
192
|
+
<option value="100">100</option>
|
|
193
|
+
</select>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<script src="workers/main.js"></script>
|
|
201
|
+
</body>
|
|
202
|
+
</html>
|