@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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<svg width="120" height="120" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="zt-g2d" x1="10" y1="50" x2="90" y2="50" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#22c55e" />
|
|
5
|
+
<stop offset="1" stop-color="#38bdf8" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<circle cx="50" cy="50" r="34" stroke="rgba(255,255,255,0.16)" stroke-width="4" />
|
|
9
|
+
<ellipse cx="50" cy="50" rx="40" ry="18" stroke="url(#zt-g2d)" stroke-width="4" />
|
|
10
|
+
<ellipse cx="50" cy="50" rx="18" ry="40" stroke="url(#zt-g2d)" stroke-width="4" opacity="0.75" />
|
|
11
|
+
<circle cx="50" cy="50" r="6" fill="url(#zt-g2d)" />
|
|
12
|
+
<path
|
|
13
|
+
d="M40 52C35 52 32 49 32 44C32 39 35 36 40 36H48"
|
|
14
|
+
stroke="white"
|
|
15
|
+
stroke-width="6"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
d="M60 48C65 48 68 51 68 56C68 61 65 64 60 64H52"
|
|
20
|
+
stroke="white"
|
|
21
|
+
stroke-width="6"
|
|
22
|
+
stroke-linecap="round"
|
|
23
|
+
/>
|
|
24
|
+
<path
|
|
25
|
+
d="M44 50H56"
|
|
26
|
+
stroke="rgba(255,255,255,0.22)"
|
|
27
|
+
stroke-width="6"
|
|
28
|
+
stroke-linecap="round"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/workers",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.60",
|
|
4
4
|
"description": "Worker orchestration and background job management for ZinTrust.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"node": ">=20.0.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@zintrust/core": "
|
|
43
|
+
"@zintrust/core": ">=0.4.0 <0.5.0",
|
|
44
44
|
"@zintrust/queue-monitor": "*",
|
|
45
45
|
"@zintrust/queue-redis": "*"
|
|
46
46
|
},
|
package/src/WorkerFactory.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Cloudflare,
|
|
10
10
|
createRedisConnection,
|
|
11
11
|
databaseConfig,
|
|
12
|
+
DatabaseConnectionRegistry,
|
|
12
13
|
Env,
|
|
13
14
|
ErrorFactory,
|
|
14
15
|
generateUuid,
|
|
@@ -46,6 +47,7 @@ import { WorkerMetrics } from './WorkerMetrics';
|
|
|
46
47
|
import { WorkerRegistry, type WorkerInstance as RegistryWorkerInstance } from './WorkerRegistry';
|
|
47
48
|
import { WorkerVersioning } from './WorkerVersioning';
|
|
48
49
|
import { keyPrefix } from './config/workerConfig';
|
|
50
|
+
import { recordQueueMonitorJob } from './queueMonitorHistory';
|
|
49
51
|
import {
|
|
50
52
|
DbWorkerStore,
|
|
51
53
|
InMemoryWorkerStore,
|
|
@@ -108,7 +110,7 @@ const resolveRuntimeBridgeUrl = (specifier: string): string | null => {
|
|
|
108
110
|
const filePath = path.join(dir, `${safeName}.bridge.mjs`);
|
|
109
111
|
const exportLines = Object.keys(bridgeModule)
|
|
110
112
|
.filter((key) => key !== 'default' && isValidBridgeExportName(key))
|
|
111
|
-
.sort()
|
|
113
|
+
.sort((a, b) => a.localeCompare(b))
|
|
112
114
|
.map((key) => `export const ${key} = bridge[${JSON.stringify(key)}];`);
|
|
113
115
|
|
|
114
116
|
const code = [
|
|
@@ -2226,20 +2228,27 @@ const resolvePersistenceConfig = (
|
|
|
2226
2228
|
};
|
|
2227
2229
|
|
|
2228
2230
|
const resolveDbClientFromEnv = async (connectionName = 'default'): Promise<IDatabase> => {
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2231
|
+
// Eagerly populate the registry when the requested connection is not yet
|
|
2232
|
+
// registered. On both the Cloudflare Workers runtime and Node, the registry
|
|
2233
|
+
// may be empty when called from the workers-persistence path because
|
|
2234
|
+
// registerDatabasesFromRuntimeConfig has not yet run for this connection. The
|
|
2235
|
+
// old pattern of connecting first (which always fails) then registering as a
|
|
2236
|
+
// fallback produced spurious [DEBUG] noise on every fresh start.
|
|
2237
|
+
if (DatabaseConnectionRegistry.get(connectionName) === undefined) {
|
|
2238
|
+
try {
|
|
2239
|
+
registerDatabasesFromRuntimeConfig(databaseConfig);
|
|
2240
|
+
} catch (registrationError) {
|
|
2241
|
+
Logger.warn(
|
|
2242
|
+
`[WorkerPersistence] Runtime database registration failed for connection '${connectionName}'`,
|
|
2243
|
+
registrationError
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2236
2246
|
}
|
|
2237
2247
|
|
|
2238
2248
|
try {
|
|
2239
|
-
|
|
2240
|
-
return await connect();
|
|
2249
|
+
return await useEnsureDbConnected(undefined, connectionName);
|
|
2241
2250
|
} catch (error) {
|
|
2242
|
-
Logger.error('Worker persistence failed
|
|
2251
|
+
Logger.error('Worker persistence failed to resolve database connection', error);
|
|
2243
2252
|
throw ErrorFactory.createConfigError(
|
|
2244
2253
|
`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`
|
|
2245
2254
|
);
|
|
@@ -2677,12 +2686,18 @@ const initializeDatacenter = (config: WorkerFactoryConfig): void => {
|
|
|
2677
2686
|
const setupWorkerEventListeners = (
|
|
2678
2687
|
worker: Worker,
|
|
2679
2688
|
workerName: string,
|
|
2689
|
+
queueName: string,
|
|
2680
2690
|
workerVersion: string,
|
|
2681
2691
|
features?: WorkerFactoryConfig['features']
|
|
2682
2692
|
): void => {
|
|
2683
2693
|
worker.on('completed', (job: Job) => {
|
|
2684
2694
|
try {
|
|
2685
2695
|
Logger.debug(`Job completed: ${workerName}`, { jobId: job.id });
|
|
2696
|
+
void recordQueueMonitorJob({
|
|
2697
|
+
queueName,
|
|
2698
|
+
status: 'completed',
|
|
2699
|
+
job,
|
|
2700
|
+
});
|
|
2686
2701
|
|
|
2687
2702
|
if (features?.observability === true) {
|
|
2688
2703
|
Observability.incrementCounter('worker.jobs.completed', 1, {
|
|
@@ -2699,6 +2714,14 @@ const setupWorkerEventListeners = (
|
|
|
2699
2714
|
worker.on('failed', (job: Job | undefined, error: Error) => {
|
|
2700
2715
|
try {
|
|
2701
2716
|
Logger.error(`Job failed: ${workerName}`, { error, jobId: job?.id }, 'workers');
|
|
2717
|
+
if (job) {
|
|
2718
|
+
void recordQueueMonitorJob({
|
|
2719
|
+
queueName,
|
|
2720
|
+
status: 'failed',
|
|
2721
|
+
job,
|
|
2722
|
+
error,
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2702
2725
|
|
|
2703
2726
|
if (features?.observability === true) {
|
|
2704
2727
|
Observability.incrementCounter('worker.jobs.failed', 1, {
|
|
@@ -2892,7 +2915,7 @@ export const WorkerFactory = Object.freeze({
|
|
|
2892
2915
|
const resolvedOptions = resolveWorkerOptions(config, autoStart);
|
|
2893
2916
|
const worker = new Worker(queueName, enhancedProcessor, resolvedOptions);
|
|
2894
2917
|
|
|
2895
|
-
setupWorkerEventListeners(worker, name, workerVersion, features);
|
|
2918
|
+
setupWorkerEventListeners(worker, name, queueName, workerVersion, features);
|
|
2896
2919
|
|
|
2897
2920
|
// Update status to "starting"
|
|
2898
2921
|
await store.update(name, {
|
package/src/createQueueWorker.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BullMQPayload, QueueMessage } from '@zintrust/core';
|
|
2
2
|
import * as Core from '@zintrust/core';
|
|
3
3
|
import { Env, Logger, Queue } from '@zintrust/core';
|
|
4
|
+
import { recordQueueMonitorJob } from './queueMonitorHistory';
|
|
4
5
|
|
|
5
6
|
type QueueApi = Readonly<{
|
|
6
7
|
enqueue: (queue: string, payload: BullMQPayload, driverName?: string) => Promise<string>;
|
|
@@ -320,6 +321,19 @@ const onProcessSuccess = async <TPayload>(input: {
|
|
|
320
321
|
}): Promise<boolean> => {
|
|
321
322
|
await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
|
|
322
323
|
|
|
324
|
+
await recordQueueMonitorJob({
|
|
325
|
+
queueName: input.queueName,
|
|
326
|
+
status: 'completed',
|
|
327
|
+
job: {
|
|
328
|
+
id: input.message.id,
|
|
329
|
+
name: `${input.queueName}-job`,
|
|
330
|
+
data: input.message.payload,
|
|
331
|
+
attemptsMade: getAttemptsFromMessage(input.message),
|
|
332
|
+
processedOn: input.startedAtMs,
|
|
333
|
+
finishedOn: Date.now(),
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
323
337
|
if (typeof input.trackerApi.completed === 'function') {
|
|
324
338
|
await input.trackerApi.completed({
|
|
325
339
|
queueName: input.queueName,
|
|
@@ -386,6 +400,22 @@ const onProcessFailure = async <TPayload>(input: {
|
|
|
386
400
|
}
|
|
387
401
|
|
|
388
402
|
await TypedQueue.ack(input.queueName, input.message.id, input.driverName);
|
|
403
|
+
|
|
404
|
+
await recordQueueMonitorJob({
|
|
405
|
+
queueName: input.queueName,
|
|
406
|
+
status: 'failed',
|
|
407
|
+
job: {
|
|
408
|
+
id: input.message.id,
|
|
409
|
+
name: `${input.queueName}-job`,
|
|
410
|
+
data: input.message.payload,
|
|
411
|
+
attemptsMade: nextAttempts,
|
|
412
|
+
failedReason: failure.message,
|
|
413
|
+
processedOn: Date.now(),
|
|
414
|
+
finishedOn: Date.now(),
|
|
415
|
+
},
|
|
416
|
+
error: failure,
|
|
417
|
+
});
|
|
418
|
+
|
|
389
419
|
await removeHeartbeatIfSupported(input.queueName, input.message.id);
|
|
390
420
|
|
|
391
421
|
if (typeof input.trackerApi.failed === 'function') {
|
|
@@ -591,6 +591,16 @@ async function getRedisQueueData(): Promise<QueueData> {
|
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
const monitor = QueueMonitor.create({
|
|
594
|
+
knownQueues: async () => {
|
|
595
|
+
const records = await WorkerFactory.listPersistedRecords();
|
|
596
|
+
return Array.from(
|
|
597
|
+
new Set(
|
|
598
|
+
records
|
|
599
|
+
.map((record) => record.queueName)
|
|
600
|
+
.filter((queueName) => typeof queueName === 'string')
|
|
601
|
+
)
|
|
602
|
+
).sort((left, right) => left.localeCompare(right));
|
|
603
|
+
},
|
|
594
604
|
redis: {
|
|
595
605
|
host: redisConfig.host || 'localhost',
|
|
596
606
|
port: redisConfig.port || 6379,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { isNonEmptyString, isObject, Logger, queueConfig } from '@zintrust/core';
|
|
2
|
+
|
|
3
|
+
type QueueMonitorStatus = 'completed' | 'failed';
|
|
4
|
+
|
|
5
|
+
type QueueMonitorJobLike = {
|
|
6
|
+
id?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
attemptsMade?: number;
|
|
10
|
+
failedReason?: string;
|
|
11
|
+
processedOn?: number;
|
|
12
|
+
finishedOn?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type QueueMonitorMetrics = {
|
|
16
|
+
recordJob: (
|
|
17
|
+
queueName: string,
|
|
18
|
+
status: QueueMonitorStatus,
|
|
19
|
+
job: QueueMonitorJobLike,
|
|
20
|
+
error?: Error
|
|
21
|
+
) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type QueueMonitorModule = {
|
|
25
|
+
createMetrics?: (config: {
|
|
26
|
+
host: string;
|
|
27
|
+
port: number;
|
|
28
|
+
password?: string;
|
|
29
|
+
db: number;
|
|
30
|
+
}) => QueueMonitorMetrics;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let queueMonitorMetricsPromise: Promise<QueueMonitorMetrics | null> | null = null;
|
|
34
|
+
|
|
35
|
+
const toFiniteInteger = (value: unknown, fallback: number): number => {
|
|
36
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
37
|
+
return Math.floor(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
41
|
+
const parsed = Number(value);
|
|
42
|
+
if (Number.isFinite(parsed)) {
|
|
43
|
+
return Math.floor(parsed);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const resolveQueueMonitorRedisConfig = (): {
|
|
51
|
+
host: string;
|
|
52
|
+
port: number;
|
|
53
|
+
password?: string;
|
|
54
|
+
db: number;
|
|
55
|
+
} | null => {
|
|
56
|
+
const redisConfig = queueConfig?.drivers?.redis;
|
|
57
|
+
|
|
58
|
+
if (!isObject(redisConfig) || redisConfig['driver'] !== 'redis') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const host = isNonEmptyString(redisConfig['host']) ? redisConfig['host'].trim() : '127.0.0.1';
|
|
63
|
+
const port = toFiniteInteger(redisConfig['port'], 6379);
|
|
64
|
+
const db = toFiniteInteger(redisConfig['database'], 0);
|
|
65
|
+
const password = isNonEmptyString(redisConfig['password']) ? redisConfig['password'] : undefined;
|
|
66
|
+
|
|
67
|
+
return { host, port, password, db };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const loadQueueMonitorMetrics = async (): Promise<QueueMonitorMetrics | null> => {
|
|
71
|
+
const redisConfig = resolveQueueMonitorRedisConfig();
|
|
72
|
+
if (redisConfig === null) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const module = (await import('@zintrust/queue-monitor')) as QueueMonitorModule;
|
|
78
|
+
if (typeof module.createMetrics !== 'function') {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return module.createMetrics(redisConfig);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
Logger.debug('Queue monitor metrics are unavailable for worker history recording', error);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const getQueueMonitorMetrics = async (): Promise<QueueMonitorMetrics | null> => {
|
|
90
|
+
queueMonitorMetricsPromise ??= loadQueueMonitorMetrics();
|
|
91
|
+
return queueMonitorMetricsPromise;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const recordQueueMonitorJob = async (input: {
|
|
95
|
+
queueName: string;
|
|
96
|
+
status: QueueMonitorStatus;
|
|
97
|
+
job: QueueMonitorJobLike;
|
|
98
|
+
error?: Error;
|
|
99
|
+
}): Promise<void> => {
|
|
100
|
+
const metrics = await getQueueMonitorMetrics();
|
|
101
|
+
if (metrics === null) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await metrics.recordJob(input.queueName, input.status, input.job, input.error);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
Logger.debug('Queue monitor history write failed', {
|
|
109
|
+
queueName: input.queueName,
|
|
110
|
+
status: input.status,
|
|
111
|
+
error,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -1,4 +1,43 @@
|
|
|
1
1
|
declare module '@zintrust/queue-monitor' {
|
|
2
|
+
export type JobStatus = 'completed' | 'failed';
|
|
3
|
+
|
|
4
|
+
export type JobSummary = {
|
|
5
|
+
id: string | undefined;
|
|
6
|
+
name: string;
|
|
7
|
+
queue?: string;
|
|
8
|
+
data: unknown;
|
|
9
|
+
attempts: number;
|
|
10
|
+
status?: string;
|
|
11
|
+
failedReason?: string;
|
|
12
|
+
timestamp: number;
|
|
13
|
+
processedOn?: number;
|
|
14
|
+
finishedOn?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Metrics = {
|
|
18
|
+
recordJob: (
|
|
19
|
+
queue: string,
|
|
20
|
+
status: JobStatus,
|
|
21
|
+
job: {
|
|
22
|
+
id?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
attemptsMade?: number;
|
|
26
|
+
failedReason?: string;
|
|
27
|
+
processedOn?: number;
|
|
28
|
+
finishedOn?: number;
|
|
29
|
+
},
|
|
30
|
+
error?: Error
|
|
31
|
+
) => Promise<void>;
|
|
32
|
+
getStats: (
|
|
33
|
+
queue: string,
|
|
34
|
+
minutes?: number
|
|
35
|
+
) => Promise<Array<{ time: string; completed: number; failed: number }>>;
|
|
36
|
+
getRecentJobs: (queue: string) => Promise<JobSummary[]>;
|
|
37
|
+
getFailedJobs: (queue: string) => Promise<JobSummary[]>;
|
|
38
|
+
close: () => Promise<void>;
|
|
39
|
+
};
|
|
40
|
+
|
|
2
41
|
export type QueueCounts = {
|
|
3
42
|
waiting: number;
|
|
4
43
|
active: number;
|
|
@@ -24,6 +63,9 @@ declare module '@zintrust/queue-monitor' {
|
|
|
24
63
|
autoRefresh?: boolean;
|
|
25
64
|
refreshIntervalMs?: number;
|
|
26
65
|
redis?: Record<string, unknown>;
|
|
66
|
+
knownQueues?:
|
|
67
|
+
| ReadonlyArray<string>
|
|
68
|
+
| (() => Promise<ReadonlyArray<string>> | ReadonlyArray<string>);
|
|
27
69
|
};
|
|
28
70
|
|
|
29
71
|
export type QueueMonitorApi = {
|
|
@@ -31,6 +73,13 @@ declare module '@zintrust/queue-monitor' {
|
|
|
31
73
|
getSnapshot: () => Promise<QueueMonitorSnapshot>;
|
|
32
74
|
};
|
|
33
75
|
|
|
76
|
+
export const createMetrics: (config: {
|
|
77
|
+
host: string;
|
|
78
|
+
port: number;
|
|
79
|
+
password?: string;
|
|
80
|
+
db: number;
|
|
81
|
+
}) => Metrics;
|
|
82
|
+
|
|
34
83
|
export const QueueMonitor: Readonly<{
|
|
35
84
|
create: (config: QueueMonitorConfig) => QueueMonitorApi;
|
|
36
85
|
}>;
|