@zintrust/workers 0.4.4 → 0.4.27
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 +26 -0
- package/dist/BroadcastWorker.d.ts +5 -0
- package/dist/NotificationWorker.d.ts +5 -0
- package/dist/WorkerFactory.d.ts +8 -0
- package/dist/WorkerFactory.js +305 -27
- package/dist/WorkerInit.d.ts +17 -0
- package/dist/WorkerInit.js +54 -2
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +558 -0
- package/dist/createQueueWorker.d.ts +5 -0
- package/dist/createQueueWorker.js +13 -2
- package/dist/dashboard/workers-api.js +46 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/register.d.ts +3 -3
- package/dist/register.js +10 -4
- 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 +11 -4
- package/src/WorkerFactory.ts +439 -30
- package/src/WorkerInit.ts +81 -3
- package/src/createQueueWorker.ts +18 -2
- package/src/dashboard/workers-api.ts +60 -13
- package/src/index.ts +6 -1
- package/src/register.ts +13 -8
package/README.md
CHANGED
|
@@ -659,6 +659,27 @@ registerWorkerRoutes(Router);
|
|
|
659
659
|
Processor specs can be file paths or URL specs (recommended for production). Remote processors
|
|
660
660
|
must export a named `ZinTrustProcessor` function.
|
|
661
661
|
|
|
662
|
+
For file-backed worker discovery, export a `workerDefinition` object from the worker module. This
|
|
663
|
+
lets fresh projects surface worker metadata before any persistence record exists.
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
export const workerDefinition = Object.freeze({
|
|
667
|
+
name: 'example-worker',
|
|
668
|
+
queueName: 'example-worker',
|
|
669
|
+
version: '1.0.0',
|
|
670
|
+
autoStart: false,
|
|
671
|
+
activeStatus: true,
|
|
672
|
+
concurrency: 1,
|
|
673
|
+
processorSpec: 'app/Workers/ExampleWorker.ts',
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
export async function ZinTrustProcessor(payload: unknown): Promise<void> {
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export default ZinTrustProcessor;
|
|
681
|
+
```
|
|
682
|
+
|
|
662
683
|
Workers support `activeStatus` to pause without deletion; inactive workers do not auto-start.
|
|
663
684
|
|
|
664
685
|
See the [API Reference](#api-reference) section for all available endpoints.
|
|
@@ -683,6 +704,11 @@ await WorkerInit.initialize({
|
|
|
683
704
|
});
|
|
684
705
|
```
|
|
685
706
|
|
|
707
|
+
When `WORKER_AUTO_START=true`, ZinTrust first auto-starts persisted workers. If persisted discovery
|
|
708
|
+
finds no auto-start candidates, `WorkerInit.autoStartPersistedWorkers()` falls back to file-backed
|
|
709
|
+
worker definitions discovered from project files. This keeps existing persisted deployments stable
|
|
710
|
+
while allowing fresh apps to boot workers from code-first definitions.
|
|
711
|
+
|
|
686
712
|
### Graceful Shutdown
|
|
687
713
|
|
|
688
714
|
Shutdown workers gracefully:
|
|
@@ -19,5 +19,10 @@ export declare const BroadcastWorker: Readonly<{
|
|
|
19
19
|
signal?: AbortSignal;
|
|
20
20
|
maxDurationMs?: number;
|
|
21
21
|
}) => Promise<number>;
|
|
22
|
+
__zintrustQueueWorkerMeta?: Readonly<{
|
|
23
|
+
kindLabel: string;
|
|
24
|
+
defaultQueueName: string;
|
|
25
|
+
maxAttempts: number;
|
|
26
|
+
}>;
|
|
22
27
|
}>;
|
|
23
28
|
export default BroadcastWorker;
|
|
@@ -19,5 +19,10 @@ export declare const NotificationWorker: Readonly<{
|
|
|
19
19
|
signal?: AbortSignal;
|
|
20
20
|
maxDurationMs?: number;
|
|
21
21
|
}) => Promise<number>;
|
|
22
|
+
__zintrustQueueWorkerMeta?: Readonly<{
|
|
23
|
+
kindLabel: string;
|
|
24
|
+
defaultQueueName: string;
|
|
25
|
+
maxAttempts: number;
|
|
26
|
+
}>;
|
|
22
27
|
}>;
|
|
23
28
|
export default NotificationWorker;
|
package/dist/WorkerFactory.d.ts
CHANGED
|
@@ -188,6 +188,14 @@ export declare const WorkerFactory: Readonly<{
|
|
|
188
188
|
* Start a worker from persisted storage when it is not registered.
|
|
189
189
|
*/
|
|
190
190
|
startFromPersisted(name: string, persistenceOverride?: WorkerPersistenceConfig): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* List worker definitions discovered from project files.
|
|
193
|
+
*/
|
|
194
|
+
listFileBackedRecords(): Promise<WorkerRecord[]>;
|
|
195
|
+
/**
|
|
196
|
+
* Get a file-backed worker definition by name.
|
|
197
|
+
*/
|
|
198
|
+
getFileBackedRecord(name: string): Promise<WorkerRecord | null>;
|
|
191
199
|
/**
|
|
192
200
|
* Get persisted worker record
|
|
193
201
|
*/
|
package/dist/WorkerFactory.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Central factory for creating workers with all advanced features
|
|
4
4
|
* Sealed namespace for immutability
|
|
5
5
|
*/
|
|
6
|
-
import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
|
|
6
|
+
import { Cloudflare, createRedisConnection, databaseConfig, Env, ErrorFactory, generateUuid, getBullMQSafeQueueName, isFunction, isNonEmptyString, isObject, JobStateTracker, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, ZintrustLang, } from '@zintrust/core';
|
|
7
7
|
import { Worker } from 'bullmq';
|
|
8
8
|
import { AutoScaler } from './AutoScaler.js';
|
|
9
9
|
import { CanaryController } from './CanaryController.js';
|
|
@@ -241,6 +241,24 @@ const processorRegistry = new Map();
|
|
|
241
241
|
const processorPathRegistry = new Map();
|
|
242
242
|
const processorResolvers = [];
|
|
243
243
|
const processorSpecRegistry = new Map();
|
|
244
|
+
const queueWorkerMetaKey = '__zintrustQueueWorkerMeta';
|
|
245
|
+
const fileWorkerDefinitionExportKeys = Object.freeze([
|
|
246
|
+
'workerDefinition',
|
|
247
|
+
'workerConfig',
|
|
248
|
+
'zintrustWorker',
|
|
249
|
+
'ZinTrustWorker',
|
|
250
|
+
'worker',
|
|
251
|
+
'defaultWorkerDefinition',
|
|
252
|
+
]);
|
|
253
|
+
const workerDiscoveryDirectories = Object.freeze([
|
|
254
|
+
['dist', 'app', 'Workers'],
|
|
255
|
+
['dist', 'src', 'workers'],
|
|
256
|
+
['dist', 'src', 'Workers'],
|
|
257
|
+
['app', 'Workers'],
|
|
258
|
+
['src', 'workers'],
|
|
259
|
+
['src', 'Workers'],
|
|
260
|
+
]);
|
|
261
|
+
const workerDiscoveryExtensions = new Set(['.js', '.mjs', '.cjs', '.ts']);
|
|
244
262
|
const processorCache = new Map();
|
|
245
263
|
let processorCacheSize = 0;
|
|
246
264
|
const buildPersistenceBootstrapConfig = () => {
|
|
@@ -311,6 +329,270 @@ const parseCacheControl = (value) => {
|
|
|
311
329
|
return Number.isFinite(parsed) ? { maxAge: parsed } : {};
|
|
312
330
|
};
|
|
313
331
|
const getProcessorSpecConfig = () => workersConfig.processorSpec;
|
|
332
|
+
const toPosixPath = (value) => value.split(path.sep).join('/');
|
|
333
|
+
const normalizeWorkerFileName = (fileName) => {
|
|
334
|
+
const baseName = fileName.replaceAll(/\.[^.]+$/, '');
|
|
335
|
+
return baseName
|
|
336
|
+
.replaceAll(/([a-z\d])([A-Z])/g, '$1-$2')
|
|
337
|
+
.replaceAll(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2')
|
|
338
|
+
.replaceAll(/[\s_]+/g, '-')
|
|
339
|
+
.replaceAll(/-+/g, '-')
|
|
340
|
+
.toLowerCase();
|
|
341
|
+
};
|
|
342
|
+
const supportsWorkerFileDiscovery = () => {
|
|
343
|
+
return (isNodeRuntime() &&
|
|
344
|
+
canUseProjectFileImports() &&
|
|
345
|
+
typeof NodeSingletons.fs.readdirSync === 'function' &&
|
|
346
|
+
typeof NodeSingletons.fs.statSync === 'function');
|
|
347
|
+
};
|
|
348
|
+
const isSupportedWorkerModuleFile = (fileName) => {
|
|
349
|
+
if (!isNonEmptyString(fileName))
|
|
350
|
+
return false;
|
|
351
|
+
if (fileName.endsWith('.d.ts'))
|
|
352
|
+
return false;
|
|
353
|
+
if (fileName.includes('.test.') || fileName.includes('.spec.'))
|
|
354
|
+
return false;
|
|
355
|
+
return workerDiscoveryExtensions.has(path.extname(fileName));
|
|
356
|
+
};
|
|
357
|
+
const getWorkerDiscoveryDirectories = () => {
|
|
358
|
+
if (!supportsWorkerFileDiscovery())
|
|
359
|
+
return [];
|
|
360
|
+
const root = resolveProjectRoot();
|
|
361
|
+
return workerDiscoveryDirectories.map((segments) => path.join(root, ...segments));
|
|
362
|
+
};
|
|
363
|
+
const listWorkerDefinitionFiles = () => {
|
|
364
|
+
if (!supportsWorkerFileDiscovery())
|
|
365
|
+
return [];
|
|
366
|
+
const discovered = new Set();
|
|
367
|
+
for (const directory of getWorkerDiscoveryDirectories()) {
|
|
368
|
+
try {
|
|
369
|
+
if (!NodeSingletons.fs.existsSync(directory))
|
|
370
|
+
continue;
|
|
371
|
+
const stats = NodeSingletons.fs.statSync(directory);
|
|
372
|
+
if (!stats.isDirectory())
|
|
373
|
+
continue;
|
|
374
|
+
const entries = NodeSingletons.fs.readdirSync(directory, { withFileTypes: true });
|
|
375
|
+
for (const entry of entries) {
|
|
376
|
+
if (!entry.isFile() || !isSupportedWorkerModuleFile(entry.name))
|
|
377
|
+
continue;
|
|
378
|
+
discovered.add(path.join(directory, entry.name));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
Logger.debug(`Worker file discovery failed for directory: ${directory}`, error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return Array.from(discovered);
|
|
386
|
+
};
|
|
387
|
+
const getProjectRelativeWorkerSpec = (sourcePath) => {
|
|
388
|
+
const relativePath = path.relative(resolveProjectRoot(), sourcePath);
|
|
389
|
+
return isNonEmptyString(relativePath) ? toPosixPath(relativePath) : toPosixPath(sourcePath);
|
|
390
|
+
};
|
|
391
|
+
const isQueueWorkerMeta = (value) => {
|
|
392
|
+
return (isObject(value) &&
|
|
393
|
+
isNonEmptyString(value['kindLabel']) &&
|
|
394
|
+
isNonEmptyString(value['defaultQueueName']) &&
|
|
395
|
+
typeof value['maxAttempts'] === 'number');
|
|
396
|
+
};
|
|
397
|
+
const getExportedQueueWorkerMeta = (mod) => {
|
|
398
|
+
for (const value of Object.values(mod)) {
|
|
399
|
+
if (!isObject(value))
|
|
400
|
+
continue;
|
|
401
|
+
const meta = value[queueWorkerMetaKey];
|
|
402
|
+
if (isQueueWorkerMeta(meta))
|
|
403
|
+
return meta;
|
|
404
|
+
}
|
|
405
|
+
return undefined;
|
|
406
|
+
};
|
|
407
|
+
const assignStringDefinitionField = (definition, key, value) => {
|
|
408
|
+
if (isNonEmptyString(value)) {
|
|
409
|
+
definition[key] = value;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const assignBooleanDefinitionField = (definition, key, value) => {
|
|
413
|
+
if (typeof value === 'boolean') {
|
|
414
|
+
definition[key] = value;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const assignObjectDefinitionField = (definition, key, value) => {
|
|
418
|
+
if (isObject(value)) {
|
|
419
|
+
definition[key] = value;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
const assignConcurrencyDefinitionField = (definition, value) => {
|
|
423
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
424
|
+
definition.concurrency = Math.max(1, Math.floor(value));
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const assignProcessorDefinitionField = (definition, value) => {
|
|
428
|
+
if (isFunction(value)) {
|
|
429
|
+
definition.processor = value;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const normalizeFileWorkerDefinition = (value) => {
|
|
433
|
+
if (!isObject(value))
|
|
434
|
+
return undefined;
|
|
435
|
+
const definition = {};
|
|
436
|
+
assignStringDefinitionField(definition, 'name', value['name']);
|
|
437
|
+
assignStringDefinitionField(definition, 'queueName', value['queueName']);
|
|
438
|
+
assignStringDefinitionField(definition, 'version', value['version']);
|
|
439
|
+
assignStringDefinitionField(definition, 'status', value['status']);
|
|
440
|
+
assignStringDefinitionField(definition, 'region', value['region']);
|
|
441
|
+
assignStringDefinitionField(definition, 'processorSpec', value['processorSpec']);
|
|
442
|
+
assignBooleanDefinitionField(definition, 'autoStart', value['autoStart']);
|
|
443
|
+
assignBooleanDefinitionField(definition, 'activeStatus', value['activeStatus']);
|
|
444
|
+
assignObjectDefinitionField(definition, 'features', value['features']);
|
|
445
|
+
assignObjectDefinitionField(definition, 'infrastructure', value['infrastructure']);
|
|
446
|
+
assignObjectDefinitionField(definition, 'datacenter', value['datacenter']);
|
|
447
|
+
assignConcurrencyDefinitionField(definition, value['concurrency']);
|
|
448
|
+
assignProcessorDefinitionField(definition, value['processor']);
|
|
449
|
+
return definition;
|
|
450
|
+
};
|
|
451
|
+
const getExportedFileWorkerDefinition = (mod) => {
|
|
452
|
+
for (const key of fileWorkerDefinitionExportKeys) {
|
|
453
|
+
const normalized = normalizeFileWorkerDefinition(mod[key]);
|
|
454
|
+
if (normalized)
|
|
455
|
+
return normalized;
|
|
456
|
+
}
|
|
457
|
+
const defaultExport = mod['default'];
|
|
458
|
+
if (!isFunction(defaultExport)) {
|
|
459
|
+
const normalized = normalizeFileWorkerDefinition(defaultExport);
|
|
460
|
+
if (normalized)
|
|
461
|
+
return normalized;
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
};
|
|
465
|
+
const importWorkerDefinitionModule = async (sourcePath) => {
|
|
466
|
+
if (!supportsWorkerFileDiscovery())
|
|
467
|
+
return undefined;
|
|
468
|
+
try {
|
|
469
|
+
return (await import(NodeSingletons.url.pathToFileURL(sourcePath).href));
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
Logger.debug(`Failed to import worker definition module: ${sourcePath}`, error);
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
const resolveDiscoveredWorkerProcessor = (definition, mod, sourcePath) => {
|
|
477
|
+
if (definition?.processor)
|
|
478
|
+
return definition.processor;
|
|
479
|
+
return extractZinTrustProcessor(mod, sourcePath) ?? pickProcessorFromModule(mod, sourcePath);
|
|
480
|
+
};
|
|
481
|
+
const resolveDiscoveredWorkerName = (definition, sourcePath) => {
|
|
482
|
+
return (definition?.name ?? normalizeWorkerFileName(path.basename(sourcePath, path.extname(sourcePath))));
|
|
483
|
+
};
|
|
484
|
+
const resolveDiscoveredQueueName = (definition, queueWorkerMeta, recordName) => {
|
|
485
|
+
if (definition?.queueName)
|
|
486
|
+
return definition.queueName;
|
|
487
|
+
if (queueWorkerMeta?.defaultQueueName)
|
|
488
|
+
return queueWorkerMeta.defaultQueueName;
|
|
489
|
+
return `${recordName}-queue`;
|
|
490
|
+
};
|
|
491
|
+
const resolveDiscoveredProcessorSpec = (definition, sourcePath) => {
|
|
492
|
+
return definition?.processorSpec ?? getProjectRelativeWorkerSpec(sourcePath);
|
|
493
|
+
};
|
|
494
|
+
const resolveDiscoveredVersion = (definition) => {
|
|
495
|
+
return definition?.version ?? '1.0.0';
|
|
496
|
+
};
|
|
497
|
+
const resolveDiscoveredStatus = (definition) => {
|
|
498
|
+
return definition?.status ?? WorkerCreationStatus.STOPPED;
|
|
499
|
+
};
|
|
500
|
+
const resolveDiscoveredAutoStart = (definition) => {
|
|
501
|
+
return definition?.autoStart ?? false;
|
|
502
|
+
};
|
|
503
|
+
const resolveDiscoveredConcurrency = (definition) => {
|
|
504
|
+
return definition?.concurrency ?? 1;
|
|
505
|
+
};
|
|
506
|
+
const resolveDiscoveredActiveStatus = (definition) => {
|
|
507
|
+
return definition?.activeStatus ?? true;
|
|
508
|
+
};
|
|
509
|
+
const buildDiscoveredWorkerRecord = (definition, queueWorkerMeta, sourcePath) => {
|
|
510
|
+
const recordName = resolveDiscoveredWorkerName(definition, sourcePath);
|
|
511
|
+
if (!isNonEmptyString(recordName))
|
|
512
|
+
return undefined;
|
|
513
|
+
return {
|
|
514
|
+
name: recordName,
|
|
515
|
+
queueName: resolveDiscoveredQueueName(definition, queueWorkerMeta, recordName),
|
|
516
|
+
version: resolveDiscoveredVersion(definition),
|
|
517
|
+
status: resolveDiscoveredStatus(definition),
|
|
518
|
+
autoStart: resolveDiscoveredAutoStart(definition),
|
|
519
|
+
concurrency: resolveDiscoveredConcurrency(definition),
|
|
520
|
+
region: definition?.region ?? null,
|
|
521
|
+
processorSpec: resolveDiscoveredProcessorSpec(definition, sourcePath),
|
|
522
|
+
activeStatus: resolveDiscoveredActiveStatus(definition),
|
|
523
|
+
features: definition?.features ?? null,
|
|
524
|
+
infrastructure: definition?.infrastructure ?? null,
|
|
525
|
+
datacenter: definition?.datacenter ?? null,
|
|
526
|
+
createdAt: new Date(),
|
|
527
|
+
updatedAt: new Date(),
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
const buildDiscoveredWorker = (mod, sourcePath) => {
|
|
531
|
+
const definition = getExportedFileWorkerDefinition(mod);
|
|
532
|
+
const queueWorkerMeta = getExportedQueueWorkerMeta(mod);
|
|
533
|
+
const processor = resolveDiscoveredWorkerProcessor(definition, mod, sourcePath);
|
|
534
|
+
const record = buildDiscoveredWorkerRecord(definition, queueWorkerMeta, sourcePath);
|
|
535
|
+
if (!record)
|
|
536
|
+
return undefined;
|
|
537
|
+
return {
|
|
538
|
+
record,
|
|
539
|
+
processor,
|
|
540
|
+
sourcePath,
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
const resolveStartFromPersistedRecord = async (name, persistenceOverride) => {
|
|
544
|
+
const persistedRecord = await getPersistedRecord(name, persistenceOverride);
|
|
545
|
+
const discovered = persistedRecord ? null : await getDiscoveredFileWorker(name);
|
|
546
|
+
const effectiveRecord = persistedRecord ?? discovered?.record ?? null;
|
|
547
|
+
if (!effectiveRecord) {
|
|
548
|
+
throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
|
|
549
|
+
}
|
|
550
|
+
return { record: effectiveRecord, discovered };
|
|
551
|
+
};
|
|
552
|
+
const resolveStartFromPersistedProcessor = async (name, record, discovered) => {
|
|
553
|
+
let processor = await resolveProcessor(name);
|
|
554
|
+
if (!processor && discovered?.processor) {
|
|
555
|
+
processor = discovered.processor;
|
|
556
|
+
}
|
|
557
|
+
const spec = record.processorSpec ?? undefined;
|
|
558
|
+
if (!processor && spec) {
|
|
559
|
+
try {
|
|
560
|
+
processor = await resolveProcessorSpec(spec);
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
Logger.error(`Failed to resolve processor module for "${name}"`, error);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!processor) {
|
|
567
|
+
throw ErrorFactory.createConfigError(`Worker "${name}" processor is not registered or resolvable. Register the processor at startup or persist a processorSpec.`);
|
|
568
|
+
}
|
|
569
|
+
return processor;
|
|
570
|
+
};
|
|
571
|
+
const discoverFileBackedWorkers = async () => {
|
|
572
|
+
const files = listWorkerDefinitionFiles();
|
|
573
|
+
if (files.length === 0)
|
|
574
|
+
return [];
|
|
575
|
+
const discovered = new Map();
|
|
576
|
+
for (const filePath of files) {
|
|
577
|
+
// eslint-disable-next-line no-await-in-loop
|
|
578
|
+
const mod = await importWorkerDefinitionModule(filePath);
|
|
579
|
+
if (!mod)
|
|
580
|
+
continue;
|
|
581
|
+
const discoveredWorker = buildDiscoveredWorker(mod, filePath);
|
|
582
|
+
if (!discoveredWorker)
|
|
583
|
+
continue;
|
|
584
|
+
if (discovered.has(discoveredWorker.record.name)) {
|
|
585
|
+
Logger.warn(`Duplicate file-backed worker definition detected for "${discoveredWorker.record.name}". Keeping the first discovered module.`);
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
discovered.set(discoveredWorker.record.name, discoveredWorker);
|
|
589
|
+
}
|
|
590
|
+
return Array.from(discovered.values());
|
|
591
|
+
};
|
|
592
|
+
const getDiscoveredFileWorker = async (name) => {
|
|
593
|
+
const discovered = await discoverFileBackedWorkers();
|
|
594
|
+
return discovered.find((entry) => entry.record.name === name) ?? null;
|
|
595
|
+
};
|
|
314
596
|
const computeSha256 = async (value) => {
|
|
315
597
|
if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
|
|
316
598
|
const data = new TextEncoder().encode(value);
|
|
@@ -384,7 +666,7 @@ const waitForWorkerConnection = async (worker, name, _queueName, timeoutMs) => {
|
|
|
384
666
|
const checkConnection = async () => {
|
|
385
667
|
try {
|
|
386
668
|
// Check if worker is actually running
|
|
387
|
-
const isRunning = await worker.isRunning();
|
|
669
|
+
const isRunning = await worker.isRunning(); // NOSONAR - BullMQ's isRunning method
|
|
388
670
|
if (!isRunning) {
|
|
389
671
|
throw ErrorFactory.createWorkerError('Worker not running');
|
|
390
672
|
}
|
|
@@ -1082,12 +1364,13 @@ const resolveRedisConfigFromEnv = (config, context) => {
|
|
|
1082
1364
|
};
|
|
1083
1365
|
const resolveRedisConfigFromDirect = (config, context) => {
|
|
1084
1366
|
const fallbackDb = Env.getInt('REDIS_QUEUE_DB', ZintrustLang.REDIS_DEFAULT_DB);
|
|
1367
|
+
const redisConfigWithDatabase = config;
|
|
1085
1368
|
let normalizedDb = fallbackDb;
|
|
1086
1369
|
if (typeof config.db === 'number') {
|
|
1087
1370
|
normalizedDb = config.db;
|
|
1088
1371
|
}
|
|
1089
|
-
else if (typeof
|
|
1090
|
-
normalizedDb =
|
|
1372
|
+
else if (typeof redisConfigWithDatabase.database === 'number') {
|
|
1373
|
+
normalizedDb = redisConfigWithDatabase.database;
|
|
1091
1374
|
}
|
|
1092
1375
|
return {
|
|
1093
1376
|
host: requireRedisHost(config.host, context),
|
|
@@ -1398,12 +1681,8 @@ const resolveAutoScalerConfig = (input) => {
|
|
|
1398
1681
|
};
|
|
1399
1682
|
const resolveWorkerOptions = (config, autoStart) => {
|
|
1400
1683
|
const options = config.options ? { ...config.options } : {};
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
}
|
|
1404
|
-
if (options.autorun === undefined) {
|
|
1405
|
-
options.autorun = autoStart;
|
|
1406
|
-
}
|
|
1684
|
+
options.prefix ??= getBullMQSafeQueueName();
|
|
1685
|
+
options.autorun ??= autoStart;
|
|
1407
1686
|
if (options.connection)
|
|
1408
1687
|
return options;
|
|
1409
1688
|
const redisConfig = resolveRedisConfigWithFallback(config.infrastructure?.redis, undefined, 'Worker requires a connection. Provide options.connection or infrastructure.redis config', 'infrastructure.redis');
|
|
@@ -2098,26 +2377,11 @@ export const WorkerFactory = Object.freeze({
|
|
|
2098
2377
|
* Start a worker from persisted storage when it is not registered.
|
|
2099
2378
|
*/
|
|
2100
2379
|
async startFromPersisted(name, persistenceOverride) {
|
|
2101
|
-
const record = await
|
|
2102
|
-
if (!record) {
|
|
2103
|
-
throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
|
|
2104
|
-
}
|
|
2380
|
+
const { record, discovered } = await resolveStartFromPersistedRecord(name, persistenceOverride);
|
|
2105
2381
|
if (record.activeStatus === false) {
|
|
2106
2382
|
throw ErrorFactory.createConfigError(`Worker "${name}" is inactive`);
|
|
2107
2383
|
}
|
|
2108
|
-
|
|
2109
|
-
const spec = record.processorSpec ?? undefined;
|
|
2110
|
-
if (!processor && spec) {
|
|
2111
|
-
try {
|
|
2112
|
-
processor = await resolveProcessorSpec(spec);
|
|
2113
|
-
}
|
|
2114
|
-
catch (error) {
|
|
2115
|
-
Logger.error(`Failed to resolve processor module for "${name}"`, error);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (!processor) {
|
|
2119
|
-
throw ErrorFactory.createConfigError(`Worker "${name}" processor is not registered or resolvable. Register the processor at startup or persist a processorSpec.`);
|
|
2120
|
-
}
|
|
2384
|
+
const processor = await resolveStartFromPersistedProcessor(name, record, discovered);
|
|
2121
2385
|
await WorkerFactory.create({
|
|
2122
2386
|
name: record.name,
|
|
2123
2387
|
queueName: record.queueName,
|
|
@@ -2132,6 +2396,20 @@ export const WorkerFactory = Object.freeze({
|
|
|
2132
2396
|
datacenter: record.datacenter,
|
|
2133
2397
|
});
|
|
2134
2398
|
},
|
|
2399
|
+
/**
|
|
2400
|
+
* List worker definitions discovered from project files.
|
|
2401
|
+
*/
|
|
2402
|
+
async listFileBackedRecords() {
|
|
2403
|
+
const discovered = await discoverFileBackedWorkers();
|
|
2404
|
+
return discovered.map((entry) => entry.record);
|
|
2405
|
+
},
|
|
2406
|
+
/**
|
|
2407
|
+
* Get a file-backed worker definition by name.
|
|
2408
|
+
*/
|
|
2409
|
+
async getFileBackedRecord(name) {
|
|
2410
|
+
const discovered = await getDiscoveredFileWorker(name);
|
|
2411
|
+
return discovered?.record ?? null;
|
|
2412
|
+
},
|
|
2135
2413
|
/**
|
|
2136
2414
|
* Get persisted worker record
|
|
2137
2415
|
*/
|
package/dist/WorkerInit.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Sets up auto-scaling and health checks
|
|
8
8
|
* - Ensures graceful startup and shutdown
|
|
9
9
|
*/
|
|
10
|
+
import type { WorkerPersistenceConfig } from './WorkerFactory';
|
|
10
11
|
export interface IWorkerInitOptions {
|
|
11
12
|
/**
|
|
12
13
|
* Whether to start resource monitoring on initialization
|
|
@@ -42,6 +43,22 @@ interface IInitState {
|
|
|
42
43
|
autoScaling: boolean;
|
|
43
44
|
shutdownHandlersRegistered: boolean;
|
|
44
45
|
}
|
|
46
|
+
type AutoStartCandidate = {
|
|
47
|
+
name: string;
|
|
48
|
+
autoStart: boolean;
|
|
49
|
+
activeStatus?: boolean;
|
|
50
|
+
};
|
|
51
|
+
type PersistenceOverride = WorkerPersistenceConfig;
|
|
52
|
+
type AutoStartTask = AutoStartCandidate & {
|
|
53
|
+
persistenceOverride: PersistenceOverride;
|
|
54
|
+
source: 'database' | 'redis' | 'memory' | 'file';
|
|
55
|
+
};
|
|
56
|
+
export declare const buildFileBackedAutoStartTasks: (records: AutoStartCandidate[], warn?: (message: string) => void) => AutoStartTask[];
|
|
57
|
+
export declare const selectAutoStartTasks: (persistedTasks: AutoStartTask[], fileRecords: AutoStartCandidate[], warn?: (message: string) => void) => AutoStartTask[];
|
|
58
|
+
export declare const selectAutoStartNames: (persistedRecords: AutoStartCandidate[], fileRecords: AutoStartCandidate[], warn?: (message: string) => void) => {
|
|
59
|
+
names: string[];
|
|
60
|
+
source: "persisted" | "file" | "none";
|
|
61
|
+
};
|
|
45
62
|
/**
|
|
46
63
|
* Initialize the worker management system
|
|
47
64
|
*/
|
package/dist/WorkerInit.js
CHANGED
|
@@ -141,6 +141,54 @@ const collectAutoStartTasks = async () => {
|
|
|
141
141
|
}
|
|
142
142
|
return tasks;
|
|
143
143
|
};
|
|
144
|
+
export const buildFileBackedAutoStartTasks = (records, warn = Logger.warn) => {
|
|
145
|
+
const tasks = [];
|
|
146
|
+
const seenWorkerNames = new Set();
|
|
147
|
+
const candidates = resolveAutoStartCandidates(records);
|
|
148
|
+
for (const record of candidates) {
|
|
149
|
+
if (seenWorkerNames.has(record.name)) {
|
|
150
|
+
warn(`Worker ${record.name} appears multiple times in file-backed discovery; keeping the first definition and skipping duplicates.`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
seenWorkerNames.add(record.name);
|
|
154
|
+
tasks.push({
|
|
155
|
+
...record,
|
|
156
|
+
persistenceOverride: { driver: 'memory' },
|
|
157
|
+
source: 'file',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return tasks;
|
|
161
|
+
};
|
|
162
|
+
export const selectAutoStartTasks = (persistedTasks, fileRecords, warn = Logger.warn) => {
|
|
163
|
+
if (persistedTasks.length > 0) {
|
|
164
|
+
return persistedTasks;
|
|
165
|
+
}
|
|
166
|
+
return buildFileBackedAutoStartTasks(fileRecords, warn);
|
|
167
|
+
};
|
|
168
|
+
export const selectAutoStartNames = (persistedRecords, fileRecords, warn = Logger.warn) => {
|
|
169
|
+
const persistedNames = resolveAutoStartCandidates(persistedRecords).map((record) => record.name);
|
|
170
|
+
if (persistedNames.length > 0) {
|
|
171
|
+
return { names: persistedNames, source: 'persisted' };
|
|
172
|
+
}
|
|
173
|
+
const fileNames = buildFileBackedAutoStartTasks(fileRecords, warn).map((record) => record.name);
|
|
174
|
+
return { names: fileNames, source: fileNames.length > 0 ? 'file' : 'none' };
|
|
175
|
+
};
|
|
176
|
+
const collectFileBackedAutoStartTasks = async () => {
|
|
177
|
+
try {
|
|
178
|
+
const records = await WorkerFactory.listFileBackedRecords();
|
|
179
|
+
const tasks = buildFileBackedAutoStartTasks(records);
|
|
180
|
+
Logger.debug('File-backed auto-start discovery', {
|
|
181
|
+
totalRecords: records.length,
|
|
182
|
+
candidateCount: tasks.length,
|
|
183
|
+
});
|
|
184
|
+
return tasks;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
Logger.warn(`File-backed auto-start discovery failed: ${message}`);
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
};
|
|
144
192
|
const isWorkerTrulyRunning = async (name) => {
|
|
145
193
|
const existing = WorkerFactory.get(name);
|
|
146
194
|
if (!existing)
|
|
@@ -238,14 +286,18 @@ async function autoStartPersistedWorkers() {
|
|
|
238
286
|
return;
|
|
239
287
|
}
|
|
240
288
|
try {
|
|
241
|
-
|
|
289
|
+
let candidates = await collectAutoStartTasks();
|
|
290
|
+
if (candidates.length === 0) {
|
|
291
|
+
candidates = await collectFileBackedAutoStartTasks();
|
|
292
|
+
}
|
|
242
293
|
const results = await Promise.all(candidates.map(async (record) => autoStartOneWorker(record)));
|
|
243
294
|
const startedCount = results.filter((item) => item.started).length;
|
|
244
295
|
const skippedCount = results.filter((item) => item.skipped).length;
|
|
245
|
-
Logger.info('Auto-started
|
|
296
|
+
Logger.info('Auto-started workers', {
|
|
246
297
|
total: candidates.length,
|
|
247
298
|
started: startedCount,
|
|
248
299
|
skipped: skippedCount,
|
|
300
|
+
source: candidates[0]?.source ?? 'none',
|
|
249
301
|
});
|
|
250
302
|
}
|
|
251
303
|
catch (error) {
|
|
@@ -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
|
+
}
|