@zintrust/workers 0.1.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.
Files changed (178) hide show
  1. package/README.md +861 -0
  2. package/dist/AnomalyDetection.d.ts +102 -0
  3. package/dist/AnomalyDetection.js +321 -0
  4. package/dist/AutoScaler.d.ts +127 -0
  5. package/dist/AutoScaler.js +425 -0
  6. package/dist/BroadcastWorker.d.ts +21 -0
  7. package/dist/BroadcastWorker.js +24 -0
  8. package/dist/CanaryController.d.ts +103 -0
  9. package/dist/CanaryController.js +380 -0
  10. package/dist/ChaosEngineering.d.ts +79 -0
  11. package/dist/ChaosEngineering.js +216 -0
  12. package/dist/CircuitBreaker.d.ts +106 -0
  13. package/dist/CircuitBreaker.js +374 -0
  14. package/dist/ClusterLock.d.ts +90 -0
  15. package/dist/ClusterLock.js +385 -0
  16. package/dist/ComplianceManager.d.ts +177 -0
  17. package/dist/ComplianceManager.js +556 -0
  18. package/dist/DatacenterOrchestrator.d.ts +133 -0
  19. package/dist/DatacenterOrchestrator.js +404 -0
  20. package/dist/DeadLetterQueue.d.ts +122 -0
  21. package/dist/DeadLetterQueue.js +539 -0
  22. package/dist/HealthMonitor.d.ts +42 -0
  23. package/dist/HealthMonitor.js +301 -0
  24. package/dist/MultiQueueWorker.d.ts +89 -0
  25. package/dist/MultiQueueWorker.js +277 -0
  26. package/dist/NotificationWorker.d.ts +21 -0
  27. package/dist/NotificationWorker.js +23 -0
  28. package/dist/Observability.d.ts +153 -0
  29. package/dist/Observability.js +530 -0
  30. package/dist/PluginManager.d.ts +123 -0
  31. package/dist/PluginManager.js +392 -0
  32. package/dist/PriorityQueue.d.ts +117 -0
  33. package/dist/PriorityQueue.js +244 -0
  34. package/dist/ResourceMonitor.d.ts +164 -0
  35. package/dist/ResourceMonitor.js +605 -0
  36. package/dist/SLAMonitor.d.ts +110 -0
  37. package/dist/SLAMonitor.js +274 -0
  38. package/dist/WorkerFactory.d.ts +193 -0
  39. package/dist/WorkerFactory.js +1507 -0
  40. package/dist/WorkerInit.d.ts +85 -0
  41. package/dist/WorkerInit.js +223 -0
  42. package/dist/WorkerMetrics.d.ts +114 -0
  43. package/dist/WorkerMetrics.js +509 -0
  44. package/dist/WorkerRegistry.d.ts +145 -0
  45. package/dist/WorkerRegistry.js +319 -0
  46. package/dist/WorkerShutdown.d.ts +61 -0
  47. package/dist/WorkerShutdown.js +159 -0
  48. package/dist/WorkerVersioning.d.ts +107 -0
  49. package/dist/WorkerVersioning.js +300 -0
  50. package/dist/build-manifest.json +462 -0
  51. package/dist/config/workerConfig.d.ts +3 -0
  52. package/dist/config/workerConfig.js +19 -0
  53. package/dist/createQueueWorker.d.ts +23 -0
  54. package/dist/createQueueWorker.js +113 -0
  55. package/dist/dashboard/index.d.ts +1 -0
  56. package/dist/dashboard/index.js +1 -0
  57. package/dist/dashboard/types.d.ts +117 -0
  58. package/dist/dashboard/types.js +1 -0
  59. package/dist/dashboard/workers-api.d.ts +4 -0
  60. package/dist/dashboard/workers-api.js +638 -0
  61. package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
  62. package/dist/dashboard/workers-dashboard-ui.js +1026 -0
  63. package/dist/dashboard/workers-dashboard.d.ts +4 -0
  64. package/dist/dashboard/workers-dashboard.js +904 -0
  65. package/dist/helper/index.d.ts +5 -0
  66. package/dist/helper/index.js +10 -0
  67. package/dist/http/WorkerApiController.d.ts +38 -0
  68. package/dist/http/WorkerApiController.js +312 -0
  69. package/dist/http/WorkerController.d.ts +374 -0
  70. package/dist/http/WorkerController.js +1351 -0
  71. package/dist/http/middleware/CustomValidation.d.ts +92 -0
  72. package/dist/http/middleware/CustomValidation.js +270 -0
  73. package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
  74. package/dist/http/middleware/DatacenterValidator.js +94 -0
  75. package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
  76. package/dist/http/middleware/EditWorkerValidation.js +55 -0
  77. package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
  78. package/dist/http/middleware/FeaturesValidator.js +60 -0
  79. package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
  80. package/dist/http/middleware/InfrastructureValidator.js +226 -0
  81. package/dist/http/middleware/OptionsValidator.d.ts +3 -0
  82. package/dist/http/middleware/OptionsValidator.js +112 -0
  83. package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
  84. package/dist/http/middleware/PayloadSanitizer.js +42 -0
  85. package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
  86. package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
  87. package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
  88. package/dist/http/middleware/QueueNameSanitizer.js +45 -0
  89. package/dist/http/middleware/ValidateDriver.d.ts +7 -0
  90. package/dist/http/middleware/ValidateDriver.js +20 -0
  91. package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
  92. package/dist/http/middleware/VersionSanitizer.js +25 -0
  93. package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
  94. package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
  95. package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
  96. package/dist/http/middleware/WorkerValidationChain.js +185 -0
  97. package/dist/index.d.ts +46 -0
  98. package/dist/index.js +48 -0
  99. package/dist/routes/workers.d.ts +12 -0
  100. package/dist/routes/workers.js +81 -0
  101. package/dist/storage/WorkerStore.d.ts +45 -0
  102. package/dist/storage/WorkerStore.js +195 -0
  103. package/dist/type.d.ts +76 -0
  104. package/dist/type.js +1 -0
  105. package/dist/ui/router/ui.d.ts +3 -0
  106. package/dist/ui/router/ui.js +83 -0
  107. package/dist/ui/types/worker-ui.d.ts +229 -0
  108. package/dist/ui/types/worker-ui.js +5 -0
  109. package/package.json +53 -0
  110. package/src/AnomalyDetection.ts +434 -0
  111. package/src/AutoScaler.ts +654 -0
  112. package/src/BroadcastWorker.ts +34 -0
  113. package/src/CanaryController.ts +531 -0
  114. package/src/ChaosEngineering.ts +301 -0
  115. package/src/CircuitBreaker.ts +495 -0
  116. package/src/ClusterLock.ts +499 -0
  117. package/src/ComplianceManager.ts +815 -0
  118. package/src/DatacenterOrchestrator.ts +561 -0
  119. package/src/DeadLetterQueue.ts +733 -0
  120. package/src/HealthMonitor.ts +390 -0
  121. package/src/MultiQueueWorker.ts +431 -0
  122. package/src/NotificationWorker.ts +33 -0
  123. package/src/Observability.ts +696 -0
  124. package/src/PluginManager.ts +551 -0
  125. package/src/PriorityQueue.ts +351 -0
  126. package/src/ResourceMonitor.ts +769 -0
  127. package/src/SLAMonitor.ts +408 -0
  128. package/src/WorkerFactory.ts +2108 -0
  129. package/src/WorkerInit.ts +313 -0
  130. package/src/WorkerMetrics.ts +709 -0
  131. package/src/WorkerRegistry.ts +443 -0
  132. package/src/WorkerShutdown.ts +210 -0
  133. package/src/WorkerVersioning.ts +422 -0
  134. package/src/config/workerConfig.ts +25 -0
  135. package/src/createQueueWorker.ts +174 -0
  136. package/src/dashboard/index.ts +6 -0
  137. package/src/dashboard/types.ts +141 -0
  138. package/src/dashboard/workers-api.ts +785 -0
  139. package/src/dashboard/zintrust.svg +30 -0
  140. package/src/helper/index.ts +11 -0
  141. package/src/http/WorkerApiController.ts +369 -0
  142. package/src/http/WorkerController.ts +1512 -0
  143. package/src/http/middleware/CustomValidation.ts +360 -0
  144. package/src/http/middleware/DatacenterValidator.ts +124 -0
  145. package/src/http/middleware/EditWorkerValidation.ts +74 -0
  146. package/src/http/middleware/FeaturesValidator.ts +82 -0
  147. package/src/http/middleware/InfrastructureValidator.ts +295 -0
  148. package/src/http/middleware/OptionsValidator.ts +144 -0
  149. package/src/http/middleware/PayloadSanitizer.ts +52 -0
  150. package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
  151. package/src/http/middleware/QueueNameSanitizer.ts +55 -0
  152. package/src/http/middleware/ValidateDriver.ts +29 -0
  153. package/src/http/middleware/VersionSanitizer.ts +30 -0
  154. package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
  155. package/src/http/middleware/WorkerValidationChain.ts +230 -0
  156. package/src/index.ts +98 -0
  157. package/src/routes/workers.ts +154 -0
  158. package/src/storage/WorkerStore.ts +240 -0
  159. package/src/type.ts +89 -0
  160. package/src/types/queue-monitor.d.ts +38 -0
  161. package/src/types/queue-redis.d.ts +38 -0
  162. package/src/ui/README.md +13 -0
  163. package/src/ui/components/JsonEditor.js +670 -0
  164. package/src/ui/components/JsonViewer.js +387 -0
  165. package/src/ui/components/WorkerCard.js +178 -0
  166. package/src/ui/components/WorkerExpandPanel.js +257 -0
  167. package/src/ui/components/fetcher.js +42 -0
  168. package/src/ui/components/sla-scorecard.js +32 -0
  169. package/src/ui/components/styles.css +30 -0
  170. package/src/ui/components/table-expander.js +34 -0
  171. package/src/ui/integration/worker-ui-integration.js +565 -0
  172. package/src/ui/router/ui.ts +99 -0
  173. package/src/ui/services/workerApi.js +240 -0
  174. package/src/ui/types/worker-ui.ts +283 -0
  175. package/src/ui/utils/jsonValidator.js +444 -0
  176. package/src/ui/workers/index.html +202 -0
  177. package/src/ui/workers/main.js +1781 -0
  178. package/src/ui/workers/styles.css +1350 -0
@@ -0,0 +1,1507 @@
1
+ /**
2
+ * Worker Factory
3
+ * Central factory for creating workers with all advanced features
4
+ * Sealed namespace for immutability
5
+ */
6
+ import { appConfig, createRedisConnection, databaseConfig, Env, ErrorFactory, getBullMQSafeQueueName, Logger, NodeSingletons, queueConfig, registerDatabasesFromRuntimeConfig, useEnsureDbConnected, workersConfig, } from '@zintrust/core';
7
+ import { Worker } from 'bullmq';
8
+ import { AutoScaler } from './AutoScaler';
9
+ import { CanaryController } from './CanaryController';
10
+ import { CircuitBreaker } from './CircuitBreaker';
11
+ import { ClusterLock } from './ClusterLock';
12
+ import { ComplianceManager } from './ComplianceManager';
13
+ import { DatacenterOrchestrator } from './DatacenterOrchestrator';
14
+ import { DeadLetterQueue } from './DeadLetterQueue';
15
+ import { HealthMonitor } from './HealthMonitor';
16
+ import { MultiQueueWorker } from './MultiQueueWorker';
17
+ import { Observability } from './Observability';
18
+ import { PluginManager } from './PluginManager';
19
+ import { PriorityQueue } from './PriorityQueue';
20
+ import { ResourceMonitor } from './ResourceMonitor';
21
+ import { WorkerMetrics } from './WorkerMetrics';
22
+ import { WorkerRegistry } from './WorkerRegistry';
23
+ import { WorkerVersioning } from './WorkerVersioning';
24
+ import { DbWorkerStore, InMemoryWorkerStore, RedisWorkerStore, } from './storage/WorkerStore';
25
+ const path = NodeSingletons.path;
26
+ const getStoreForWorker = async (config, persistenceOverride) => {
27
+ if (persistenceOverride) {
28
+ return resolveWorkerStoreForPersistence(persistenceOverride);
29
+ }
30
+ // If worker has specific configuration, use it
31
+ if (config) {
32
+ const persistence = resolvePersistenceConfig(config);
33
+ if (persistence) {
34
+ return resolveWorkerStoreForPersistence(persistence);
35
+ }
36
+ }
37
+ // Fallback to default/global store
38
+ await ensureWorkerStoreConfigured();
39
+ return workerStore;
40
+ };
41
+ const validateAndGetStore = async (name, config, persistenceOverride) => {
42
+ const store = await getStoreForWorker(config, persistenceOverride);
43
+ const record = await store.get(name);
44
+ if (!record) {
45
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in the specified driver. Ensure you are addressing the correct storage backend.`);
46
+ }
47
+ return store;
48
+ };
49
+ // Worker creation status enum for proper lifecycle management
50
+ export const WorkerCreationStatus = {
51
+ CREATING: 'creating', // Initial state - worker is being created
52
+ CONNECTING: 'connecting', // Connecting to Redis/Queue
53
+ STARTING: 'starting', // Starting BullMQ worker
54
+ RUNNING: 'running', // Actually processing jobs
55
+ FAILED: 'failed', // Connection/startup failed
56
+ STOPPED: 'stopped', // Intentionally stopped
57
+ };
58
+ // Internal initialization state to prevent memory leaks and redundant calls
59
+ let clusteringInitialized = false;
60
+ let metricsInitialized = false;
61
+ let autoScalingInitialized = false;
62
+ let deadLetterQueueInitialized = false;
63
+ let resourceMonitoringInitialized = false;
64
+ let complianceInitialized = false;
65
+ let observabilityInitialized = false;
66
+ // Internal state
67
+ const workers = new Map();
68
+ let workerStore = InMemoryWorkerStore.create();
69
+ let workerStoreConfigured = false;
70
+ let workerStoreConfig = null;
71
+ const processorRegistry = new Map();
72
+ const processorPathRegistry = new Map();
73
+ const processorResolvers = [];
74
+ const buildPersistenceBootstrapConfig = () => {
75
+ const driver = Env.get('WORKER_PERSISTENCE_DRIVER', 'memory');
76
+ const config = {
77
+ name: '__zintrust_persistence_bootstrap__',
78
+ queueName: '__zintrust_bootstrap__',
79
+ processor: async () => undefined,
80
+ infrastructure: {
81
+ persistence: {
82
+ driver,
83
+ },
84
+ },
85
+ };
86
+ // Add Redis config if using Redis persistence
87
+ if (driver === 'redis') {
88
+ config.infrastructure = {
89
+ ...config.infrastructure,
90
+ redis: queueConfig.drivers.redis,
91
+ };
92
+ }
93
+ return config;
94
+ };
95
+ const registerProcessor = (name, processor) => {
96
+ processorRegistry.set(name, processor);
97
+ };
98
+ const registerProcessors = (processors) => {
99
+ Object.entries(processors).forEach(([name, processor]) => {
100
+ if (typeof processor === 'function') {
101
+ processorRegistry.set(name, processor);
102
+ }
103
+ });
104
+ };
105
+ const registerProcessorPaths = (paths) => {
106
+ Object.entries(paths).forEach(([name, modulePath]) => {
107
+ if (typeof modulePath === 'string' && modulePath.trim().length > 0) {
108
+ processorPathRegistry.set(name, modulePath);
109
+ }
110
+ });
111
+ };
112
+ const registerProcessorResolver = (resolver) => {
113
+ processorResolvers.push(resolver);
114
+ };
115
+ const decodeProcessorPathEntities = (value) => value
116
+ .replaceAll(///gi, '/')
117
+ .replaceAll('/', '/')
118
+ .replaceAll(///gi, '/');
119
+ const waitForWorkerConnection = async (worker, name, _queueName, timeoutMs) => {
120
+ const startTime = Date.now();
121
+ const checkInterval = 100; // 100ms between checks
122
+ let timeoutId = null;
123
+ return new Promise((resolve, reject) => {
124
+ const checkConnection = async () => {
125
+ try {
126
+ // Check if worker is actually running
127
+ const isRunning = await worker.isRunning();
128
+ if (!isRunning) {
129
+ throw ErrorFactory.createWorkerError('Worker not running');
130
+ }
131
+ // Check Redis connection
132
+ const client = await worker.client;
133
+ const pingResult = await client.ping();
134
+ if (pingResult !== 'PONG') {
135
+ throw ErrorFactory.createWorkerError('Redis ping failed');
136
+ }
137
+ // Removed heavy Queue instantiation loop - relying on Redis ping for connectivity check
138
+ // The queue instance creation was causing memory pressure and potential connection leaks in this retry loop
139
+ Logger.debug(`Worker health verification passed for ${name}`, {
140
+ isRunning,
141
+ pingResult,
142
+ });
143
+ if (timeoutId)
144
+ clearTimeout(timeoutId);
145
+ resolve();
146
+ return;
147
+ }
148
+ catch (error) {
149
+ Logger.debug(`Worker health verification failed for ${name}, retrying...`, error);
150
+ // Check timeout
151
+ if (Date.now() - startTime >= timeoutMs) {
152
+ if (timeoutId)
153
+ clearTimeout(timeoutId);
154
+ reject(ErrorFactory.createWorkerError('Worker failed health verification within timeout period'));
155
+ return;
156
+ }
157
+ // Schedule next check
158
+ timeoutId = globalThis.setTimeout(checkConnection, checkInterval);
159
+ }
160
+ };
161
+ // Start checking
162
+ checkConnection();
163
+ });
164
+ };
165
+ const startHealthMonitoring = (name, worker, queueName) => {
166
+ HealthMonitor.register(name, worker, queueName);
167
+ };
168
+ const sanitizeProcessorPath = (value) => {
169
+ const decoded = decodeProcessorPathEntities(value);
170
+ const base = decoded.split(/[?#&]/)[0]?.trim() ?? '';
171
+ if (!base)
172
+ return '';
173
+ const isAbsolutePath = base.startsWith('/') || /^[A-Za-z]:[\\/]/.test(base);
174
+ const relativePath = base.startsWith('.') ? base : `./${base}`;
175
+ return isAbsolutePath ? base : path.resolve(process.cwd(), relativePath);
176
+ };
177
+ const resolveProcessorFromPath = async (modulePath) => {
178
+ const trimmed = modulePath.trim();
179
+ if (!trimmed)
180
+ return undefined;
181
+ const resolved = sanitizeProcessorPath(trimmed);
182
+ if (!resolved)
183
+ return undefined;
184
+ try {
185
+ const mod = await import(resolved);
186
+ const candidate = mod?.default ?? mod?.processor ?? mod?.handler ?? mod?.handle;
187
+ if (typeof candidate !== 'function') {
188
+ Logger.warn(`Module imported from ${resolved} but no valid processor function found (exported: ${Object.keys(mod)})`);
189
+ }
190
+ return typeof candidate === 'function'
191
+ ? candidate
192
+ : undefined;
193
+ }
194
+ catch (err) {
195
+ Logger.error(`Failed to import processor from path: ${resolved}`, err);
196
+ return undefined;
197
+ }
198
+ };
199
+ const resolveProcessor = async (name) => {
200
+ const direct = processorRegistry.get(name);
201
+ if (direct)
202
+ return direct;
203
+ const pathHint = processorPathRegistry.get(name);
204
+ if (pathHint) {
205
+ try {
206
+ const resolved = await resolveProcessorFromPath(pathHint);
207
+ if (resolved)
208
+ return resolved;
209
+ }
210
+ catch (error) {
211
+ Logger.error(`Failed to resolve processor module for "${name}"`, error);
212
+ }
213
+ }
214
+ const resolverResults = await Promise.all(processorResolvers.map(async (resolver) => {
215
+ try {
216
+ return await resolver(name);
217
+ }
218
+ catch (error) {
219
+ Logger.error(`Processor resolver failed for "${name}"`, error);
220
+ return undefined;
221
+ }
222
+ }));
223
+ const resolvedFromResolvers = resolverResults.find((result) => result !== undefined);
224
+ if (resolvedFromResolvers)
225
+ return resolvedFromResolvers;
226
+ return undefined;
227
+ };
228
+ const resolveProcessorPath = async (modulePath) => resolveProcessorFromPath(modulePath);
229
+ const recordMetricSafely = (workerName, metricType, value, metadata) => {
230
+ WorkerMetrics.record(workerName, metricType, value, metadata).catch((error) => {
231
+ Logger.error(`Failed to record worker metric: ${workerName}/${metricType}`, error);
232
+ });
233
+ };
234
+ const ensureCircuitAllowsExecution = (workerName, version, jobId, features) => {
235
+ if (!(features?.circuitBreaker ?? false))
236
+ return;
237
+ const canExecute = CircuitBreaker.canExecute(workerName, version);
238
+ if (canExecute)
239
+ return;
240
+ const state = CircuitBreaker.getState(workerName, version);
241
+ Logger.warn('Circuit breaker is open, rejecting job', {
242
+ workerName,
243
+ version,
244
+ jobId,
245
+ circuitState: state?.state,
246
+ });
247
+ CircuitBreaker.recordRejection(workerName, version);
248
+ throw ErrorFactory.createGeneralError(`Circuit breaker is open for ${workerName}@${version}`);
249
+ };
250
+ const runBeforeProcessHooks = async (workerName, job, features) => {
251
+ if (!(features?.plugins ?? false)) {
252
+ return { skip: false, jobData: job.data };
253
+ }
254
+ const hookResult = await PluginManager.executeHook('beforeProcess', {
255
+ workerName,
256
+ jobId: job.id ?? '',
257
+ jobData: job.data,
258
+ timestamp: new Date(),
259
+ });
260
+ if (hookResult.stopped) {
261
+ const errorMessage = hookResult.errors[0]?.error?.message ?? 'Stopped by plugin';
262
+ Logger.info('Job processing stopped by plugin', {
263
+ workerName,
264
+ jobId: job.id,
265
+ reason: errorMessage,
266
+ });
267
+ return { skip: true, reason: errorMessage };
268
+ }
269
+ if (hookResult.modified) {
270
+ return { skip: false, jobData: hookResult.context.jobData };
271
+ }
272
+ return { skip: false, jobData: job.data };
273
+ };
274
+ const startProcessingSpan = (workerName, version, job, queueName, features) => {
275
+ if (!(features?.observability ?? false))
276
+ return null;
277
+ return Observability.startSpan(`worker.${workerName}.process`, {
278
+ attributes: {
279
+ worker_name: workerName,
280
+ worker_version: version,
281
+ job_id: job.id ?? '',
282
+ queue_name: queueName,
283
+ },
284
+ });
285
+ };
286
+ const usePluginManager = async (workerName, job, result) => {
287
+ await PluginManager.executeHook('afterProcess', {
288
+ workerName,
289
+ jobId: job.id ?? '',
290
+ jobData: job.data,
291
+ metadata: { result },
292
+ timestamp: new Date(),
293
+ });
294
+ await PluginManager.executeHook('onComplete', {
295
+ workerName,
296
+ jobId: job.id ?? '',
297
+ jobData: job.data,
298
+ metadata: { result },
299
+ timestamp: new Date(),
300
+ });
301
+ };
302
+ const handleSuccess = async (params) => {
303
+ const { workerName, jobVersion, job, result, duration, spanId, features } = params;
304
+ if (features?.metrics ?? false) {
305
+ recordMetricSafely(workerName, 'processed', 1);
306
+ recordMetricSafely(workerName, 'duration', duration);
307
+ }
308
+ if (features?.circuitBreaker ?? false) {
309
+ CircuitBreaker.recordSuccess(workerName, jobVersion);
310
+ }
311
+ if (features?.observability ?? false) {
312
+ Observability.recordJobMetrics(workerName, job.name, {
313
+ processed: 1,
314
+ failed: 0,
315
+ durationMs: duration,
316
+ });
317
+ if (spanId !== null) {
318
+ Observability.endSpan(spanId, { success: true });
319
+ }
320
+ }
321
+ if (features?.plugins ?? false) {
322
+ await usePluginManager(workerName, { id: job.id ?? '', data: job.data }, result);
323
+ }
324
+ };
325
+ const recordFailureMetrics = (workerName, _jobVersion, duration, features) => {
326
+ if (features?.metrics === true) {
327
+ recordMetricSafely(workerName, 'errors', 1);
328
+ recordMetricSafely(workerName, 'duration', duration);
329
+ }
330
+ };
331
+ const recordFailureObservability = (workerName, jobName, duration, spanId, features) => {
332
+ if (features?.observability === true) {
333
+ Observability.recordJobMetrics(workerName, jobName, {
334
+ processed: 0,
335
+ failed: 1,
336
+ durationMs: duration,
337
+ });
338
+ if (spanId !== null) {
339
+ Observability.recordSpanError(spanId, ErrorFactory.createGeneralError('Job processing failed'));
340
+ Observability.endSpan(spanId, { success: false });
341
+ }
342
+ }
343
+ };
344
+ const addFailedJobToDeadLetterQueue = async (workerName, job, error, duration, jobVersion, queueName, features) => {
345
+ if (features?.deadLetterQueue === true) {
346
+ await DeadLetterQueue.addFailedJob({
347
+ id: job.id ?? '',
348
+ queueName,
349
+ workerName,
350
+ jobName: job.name,
351
+ data: job.data,
352
+ error: {
353
+ name: error.name,
354
+ message: error.message,
355
+ stack: error.stack,
356
+ },
357
+ attemptsMade: job.attemptsMade ?? 0,
358
+ maxAttempts: job.opts.attempts ?? 0,
359
+ failedAt: new Date(),
360
+ firstAttemptAt: new Date(job.timestamp ?? Date.now()),
361
+ lastAttemptAt: new Date(),
362
+ processingTime: duration,
363
+ metadata: {
364
+ version: jobVersion,
365
+ },
366
+ complianceFlags: {
367
+ containsPII: false,
368
+ containsPHI: false,
369
+ dataClassification: 'public',
370
+ },
371
+ });
372
+ }
373
+ };
374
+ const executeFailurePlugins = async (workerName, job, error, features) => {
375
+ if (features?.plugins === true) {
376
+ await PluginManager.executeHook('onError', {
377
+ workerName,
378
+ jobId: job.id ?? '',
379
+ jobData: job.data,
380
+ error,
381
+ timestamp: new Date(),
382
+ });
383
+ }
384
+ };
385
+ const recordCircuitBreakerFailure = (workerName, jobVersion, error, features) => {
386
+ if (features?.circuitBreaker === true) {
387
+ CircuitBreaker.recordFailure(workerName, jobVersion, error);
388
+ }
389
+ };
390
+ const logAndRecordFailure = (workerName, jobVersion, job, error, features) => {
391
+ Logger.error(`Worker job failed: ${workerName}`, { error, jobId: job.id, version: jobVersion }, 'workers');
392
+ recordCircuitBreakerFailure(workerName, jobVersion, error, features);
393
+ };
394
+ const recordFailureObservabilityAndMetrics = (params) => {
395
+ const { workerName, jobVersion, jobName, duration, spanId, features } = params;
396
+ recordFailureMetrics(workerName, jobVersion, duration, features);
397
+ recordFailureObservability(workerName, jobName, duration, spanId, features);
398
+ };
399
+ const executeAllFailureHandlers = async (params) => {
400
+ const { workerName, jobVersion, job, error, duration, spanId, features, queueName } = params;
401
+ recordFailureObservabilityAndMetrics({
402
+ workerName,
403
+ jobVersion,
404
+ jobName: job.name,
405
+ duration,
406
+ spanId,
407
+ features,
408
+ });
409
+ if (features?.deadLetterQueue === true) {
410
+ await addFailedJobToDeadLetterQueue(workerName, job, error, duration, jobVersion, queueName, features);
411
+ }
412
+ };
413
+ const handleFailure = async (params) => {
414
+ const { workerName, jobVersion, job, error, features } = params;
415
+ logAndRecordFailure(workerName, jobVersion, job, error, features);
416
+ await executeAllFailureHandlers(params);
417
+ await executeFailurePlugins(workerName, job, error, features);
418
+ };
419
+ /**
420
+ * Helper: Create enhanced processor with all features
421
+ */
422
+ const createEnhancedProcessor = (config) => {
423
+ return async (job) => {
424
+ const { name, version, processor, features } = config;
425
+ const jobVersion = version ?? '1.0.0';
426
+ ensureCircuitAllowsExecution(name, jobVersion, job.id, features);
427
+ const beforeOutcome = await runBeforeProcessHooks(name, job, features);
428
+ if (beforeOutcome.skip) {
429
+ return { skipped: true, reason: beforeOutcome.reason };
430
+ }
431
+ if (beforeOutcome.jobData !== undefined) {
432
+ job.data = beforeOutcome.jobData;
433
+ }
434
+ const startTime = Date.now();
435
+ let result;
436
+ let spanId = null;
437
+ try {
438
+ spanId = startProcessingSpan(name, jobVersion, job, config.queueName, features);
439
+ // Process the job
440
+ result = await processor(job);
441
+ const duration = Date.now() - startTime;
442
+ await handleSuccess({
443
+ workerName: name,
444
+ jobVersion,
445
+ job,
446
+ result,
447
+ duration,
448
+ spanId,
449
+ features,
450
+ });
451
+ return result;
452
+ }
453
+ catch (err) {
454
+ const error = err;
455
+ const duration = Date.now() - startTime;
456
+ await handleFailure({
457
+ workerName: name,
458
+ jobVersion,
459
+ job,
460
+ error,
461
+ duration,
462
+ spanId,
463
+ features,
464
+ queueName: config.queueName,
465
+ });
466
+ throw error;
467
+ }
468
+ };
469
+ };
470
+ const requireInfrastructure = (value, message) => {
471
+ if (value === null || value === undefined) {
472
+ throw ErrorFactory.createConfigError(message);
473
+ }
474
+ return value;
475
+ };
476
+ const resolveEnvString = (envKey, fallback) => {
477
+ if (!envKey)
478
+ return fallback;
479
+ return Env.get(envKey, fallback);
480
+ };
481
+ const resolveEnvInt = (envKey, fallback) => {
482
+ if (!envKey)
483
+ return fallback;
484
+ return Env.getInt(envKey, fallback);
485
+ };
486
+ const isRedisEnvConfig = (config) => config.env === true;
487
+ const requireRedisHost = (host, context) => {
488
+ if (!host) {
489
+ throw ErrorFactory.createConfigError(`${context}.host is required`);
490
+ }
491
+ return host;
492
+ };
493
+ const resolveRedisFallbacks = () => {
494
+ const queueRedis = queueConfig.drivers.redis;
495
+ return {
496
+ host: queueRedis?.driver === 'redis' ? queueRedis.host : Env.get('REDIS_HOST', '127.0.0.1'),
497
+ port: queueRedis?.driver === 'redis' ? queueRedis.port : Env.getInt('REDIS_PORT', 6379),
498
+ db: queueRedis?.driver === 'redis' ? queueRedis.database : Env.getInt('REDIS_DB', 0),
499
+ password: queueRedis?.driver === 'redis' ? (queueRedis.password ?? '') : Env.get('REDIS_PASSWORD', ''),
500
+ };
501
+ };
502
+ const resolveRedisConfigFromEnv = (config, context) => {
503
+ const fallback = resolveRedisFallbacks();
504
+ const host = requireRedisHost(resolveEnvString(config.host ?? 'REDIS_HOST', fallback.host), context);
505
+ const port = resolveEnvInt(config.port ?? 'REDIS_PORT', fallback.port);
506
+ const db = resolveEnvInt(config.db ?? 'REDIS_DB', fallback.db);
507
+ const password = resolveEnvString(config.password ?? 'REDIS_PASSWORD', fallback.password);
508
+ return {
509
+ host,
510
+ port,
511
+ db,
512
+ password: password || undefined,
513
+ };
514
+ };
515
+ const resolveRedisConfigFromDirect = (config, context) => ({
516
+ host: requireRedisHost(config.host, context),
517
+ port: config.port,
518
+ db: config.db,
519
+ password: config.password ?? Env.get('REDIS_PASSWORD', undefined),
520
+ });
521
+ const resolveRedisConfig = (config, context) => isRedisEnvConfig(config)
522
+ ? resolveRedisConfigFromEnv(config, context)
523
+ : resolveRedisConfigFromDirect(config, context);
524
+ const resolveRedisConfigWithFallback = (primary, fallback, errorMessage, context) => {
525
+ const selected = primary ?? fallback;
526
+ if (!selected) {
527
+ throw ErrorFactory.createConfigError(errorMessage);
528
+ }
529
+ return resolveRedisConfig(selected, context);
530
+ };
531
+ const normalizeEnvValue = (value) => {
532
+ if (!value)
533
+ return undefined;
534
+ const trimmed = value.trim();
535
+ return trimmed.length > 0 ? trimmed : undefined;
536
+ };
537
+ const normalizeAppName = (value) => {
538
+ const normalized = (value ?? '').trim().replaceAll(/\s+/g, '_');
539
+ return normalized.length > 0 ? normalized : 'zintrust';
540
+ };
541
+ const resolveDefaultRedisKeyPrefix = () => 'worker_' + normalizeAppName(appConfig.prefix);
542
+ const resolveDefaultPersistenceTable = () => normalizeEnvValue(Env.get('WORKER_PERSISTENCE_TABLE', 'zintrust_workers')) ?? 'zintrust_workers';
543
+ const resolveDefaultPersistenceConnection = () => normalizeEnvValue(Env.get('WORKER_PERSISTENCE_DB_CONNECTION', 'default')) ?? 'default';
544
+ const resolveAutoStart = (config) => {
545
+ // If explicitly set in config (not null/undefined), use that
546
+ if (config.autoStart !== undefined && config.autoStart !== null) {
547
+ return config.autoStart;
548
+ }
549
+ // Otherwise, use environment variable
550
+ return Env.getBool('WORKER_AUTO_START', false);
551
+ };
552
+ const normalizeExplicitPersistence = (persistence) => {
553
+ if (persistence.driver === 'memory')
554
+ return { driver: 'memory' };
555
+ if (persistence.driver === 'redis') {
556
+ return {
557
+ driver: 'redis',
558
+ redis: persistence.redis,
559
+ keyPrefix: persistence.keyPrefix ??
560
+ normalizeEnvValue(Env.get('WORKER_PERSISTENCE_REDIS_KEY_PREFIX', '')) ??
561
+ resolveDefaultRedisKeyPrefix(),
562
+ };
563
+ }
564
+ const clientIsConnection = typeof persistence.client === 'string';
565
+ const clientConnection = clientIsConnection ? persistence.client : undefined;
566
+ const resolvedConnection = persistence.connection ??
567
+ clientConnection ??
568
+ normalizeEnvValue(Env.get('WORKER_PERSISTENCE_DB_CONNECTION', 'default')) ??
569
+ resolveDefaultPersistenceConnection();
570
+ return {
571
+ driver: 'database',
572
+ client: clientIsConnection ? undefined : persistence.client,
573
+ connection: resolvedConnection,
574
+ table: persistence.table ??
575
+ normalizeEnvValue(Env.get('WORKER_PERSISTENCE_TABLE', 'zintrust_workers')) ??
576
+ resolveDefaultPersistenceTable(),
577
+ };
578
+ };
579
+ const resolvePersistenceConfig = (config) => {
580
+ const explicit = config.infrastructure?.persistence;
581
+ if (explicit)
582
+ return normalizeExplicitPersistence(explicit);
583
+ const driver = normalizeEnvValue(Env.get('WORKER_PERSISTENCE_DRIVER', ''))?.toLowerCase();
584
+ if (!driver)
585
+ return undefined;
586
+ if (driver === 'memory')
587
+ return { driver: 'memory' };
588
+ if (driver === 'redis') {
589
+ const keyPrefix = normalizeEnvValue(Env.get('WORKER_PERSISTENCE_REDIS_KEY_PREFIX', ''));
590
+ return {
591
+ driver: 'redis',
592
+ redis: { env: true },
593
+ keyPrefix: `${keyPrefix}_worker_${appConfig.prefix}`,
594
+ };
595
+ }
596
+ if (driver === 'db' || driver === 'database') {
597
+ return {
598
+ driver: 'database',
599
+ connection: resolveDefaultPersistenceConnection(),
600
+ table: resolveDefaultPersistenceTable(),
601
+ };
602
+ }
603
+ throw ErrorFactory.createConfigError('WORKER_PERSISTENCE_DRIVER must be one of memory, redis, or database');
604
+ };
605
+ const resolveDbClientFromEnv = async (connectionName = 'default') => {
606
+ const connect = async () => await useEnsureDbConnected(undefined, connectionName);
607
+ try {
608
+ return await connect();
609
+ }
610
+ catch (error) {
611
+ Logger.error('Worker persistence failed to resolve database connection', error);
612
+ }
613
+ try {
614
+ registerDatabasesFromRuntimeConfig(databaseConfig);
615
+ return await connect();
616
+ }
617
+ catch (error) {
618
+ Logger.error('Worker persistence failed after registering runtime databases', error);
619
+ throw ErrorFactory.createConfigError(`Worker persistence requires a database client. Register connection '${connectionName}' or pass infrastructure.persistence.client.`);
620
+ }
621
+ };
622
+ const resolveWorkerStore = async (config) => {
623
+ const persistence = resolvePersistenceConfig(config);
624
+ if (!persistence)
625
+ return workerStore;
626
+ let next;
627
+ if (persistence.driver === 'memory') {
628
+ next = InMemoryWorkerStore.create();
629
+ }
630
+ else if (persistence.driver === 'redis') {
631
+ const redisConfig = resolveRedisConfigWithFallback(persistence.redis, config.infrastructure?.redis, 'Worker persistence requires redis config (persistence.redis or infrastructure.redis)', 'infrastructure.persistence.redis');
632
+ const client = createRedisConnection(redisConfig);
633
+ next = RedisWorkerStore.create(client, persistence.keyPrefix ?? resolveDefaultRedisKeyPrefix());
634
+ }
635
+ else if (persistence.driver === 'database') {
636
+ const explicitConnection = typeof persistence.client === 'string' ? persistence.client : persistence.connection;
637
+ const client = typeof persistence.client === 'string'
638
+ ? await resolveDbClientFromEnv(explicitConnection)
639
+ : (persistence.client ?? (await resolveDbClientFromEnv(explicitConnection)));
640
+ next = DbWorkerStore.create(client, persistence.table);
641
+ }
642
+ else {
643
+ next = InMemoryWorkerStore.create();
644
+ }
645
+ await next.init();
646
+ return next;
647
+ };
648
+ // Store instance cache to reuse connections
649
+ const storeInstanceCache = new Map();
650
+ /**
651
+ * Generate cache key for persistence configuration
652
+ */
653
+ const generateCacheKey = (persistence) => {
654
+ return JSON.stringify({
655
+ driver: persistence.driver,
656
+ redis: 'redis' in persistence ? persistence.redis : undefined,
657
+ keyPrefix: 'keyPrefix' in persistence ? persistence.keyPrefix : undefined,
658
+ connection: 'connection' in persistence ? persistence.connection : undefined,
659
+ table: 'table' in persistence ? persistence.table : undefined,
660
+ });
661
+ };
662
+ /**
663
+ * Create new store instance based on persistence configuration
664
+ */
665
+ const createWorkerStore = async (persistence) => {
666
+ if (persistence.driver === 'memory') {
667
+ if (workerStoreConfigured && workerStoreConfig?.driver === 'memory') {
668
+ return workerStore;
669
+ }
670
+ return InMemoryWorkerStore.create();
671
+ }
672
+ if (persistence.driver === 'redis') {
673
+ const redisConfig = resolveRedisConfigWithFallback(persistence.redis ?? { env: true }, undefined, 'Worker persistence requires redis config (persistence.redis or REDIS_* env values)', 'persistence.redis');
674
+ const client = createRedisConnection(redisConfig);
675
+ return RedisWorkerStore.create(client, persistence.keyPrefix ?? resolveDefaultRedisKeyPrefix());
676
+ }
677
+ // Database driver
678
+ const explicitConnection = typeof persistence.client === 'string' ? persistence.client : persistence.connection;
679
+ const client = typeof persistence.client === 'string'
680
+ ? await resolveDbClientFromEnv(explicitConnection)
681
+ : (persistence.client ?? (await resolveDbClientFromEnv(explicitConnection)));
682
+ return DbWorkerStore.create(client, persistence.table);
683
+ };
684
+ const resolveWorkerStoreForPersistence = async (persistence) => {
685
+ const cacheKey = generateCacheKey(persistence);
686
+ // Return cached instance if available
687
+ const cached = storeInstanceCache.get(cacheKey);
688
+ if (cached) {
689
+ return cached;
690
+ }
691
+ // Create new store instance
692
+ const store = await createWorkerStore(persistence);
693
+ await store.init();
694
+ // Cache the store instance for reuse
695
+ storeInstanceCache.set(cacheKey, store);
696
+ return store;
697
+ };
698
+ const getPersistedRecord = async (name, persistenceOverride) => {
699
+ if (!persistenceOverride) {
700
+ await ensureWorkerStoreConfigured();
701
+ return workerStore.get(name);
702
+ }
703
+ const store = await resolveWorkerStoreForPersistence(persistenceOverride);
704
+ return store.get(name);
705
+ };
706
+ const ensureWorkerStoreConfigured = async () => {
707
+ if (workerStoreConfigured)
708
+ return;
709
+ const bootstrapConfig = buildPersistenceBootstrapConfig();
710
+ const persistence = resolvePersistenceConfig(bootstrapConfig);
711
+ if (!persistence)
712
+ return;
713
+ workerStore = await resolveWorkerStore(bootstrapConfig);
714
+ workerStoreConfigured = true;
715
+ workerStoreConfig = persistence;
716
+ };
717
+ const buildWorkerRecord = (config, status) => {
718
+ const now = new Date();
719
+ const decodedProcessorPath = config.processorPath
720
+ ? decodeProcessorPathEntities(config.processorPath)
721
+ : null;
722
+ return {
723
+ name: config.name,
724
+ queueName: config.queueName,
725
+ version: config.version ?? '1.0.0',
726
+ status,
727
+ autoStart: resolveAutoStart(config),
728
+ concurrency: config.options?.concurrency ?? 1,
729
+ region: config.datacenter?.primaryRegion ?? null,
730
+ processorPath: decodedProcessorPath,
731
+ features: config.features ? { ...config.features } : null,
732
+ infrastructure: config.infrastructure ? { ...config.infrastructure } : null,
733
+ datacenter: config.datacenter ? { ...config.datacenter } : null,
734
+ createdAt: now,
735
+ updatedAt: now,
736
+ lastHealthCheck: undefined,
737
+ lastError: undefined,
738
+ connectionState: undefined,
739
+ };
740
+ };
741
+ const buildDefaultAutoScalerConfig = () => ({
742
+ enabled: workersConfig.autoScaling.enabled,
743
+ checkInterval: workersConfig.autoScaling.interval,
744
+ scalingPolicies: new Map(),
745
+ costOptimization: {
746
+ enabled: workersConfig.costOptimization.enabled,
747
+ maxCostPerHour: 0,
748
+ preferSpotInstances: workersConfig.costOptimization.spotInstances,
749
+ offPeakSchedule: {
750
+ start: workersConfig.autoScaling.offPeakSchedule.split('-')[0] ?? '22:00',
751
+ end: workersConfig.autoScaling.offPeakSchedule.split('-')[1] ?? '06:00',
752
+ timezone: 'UTC',
753
+ reductionPercentage: Math.round(workersConfig.autoScaling.offPeakReduction * 100),
754
+ },
755
+ budgetAlerts: {
756
+ dailyLimit: 0,
757
+ weeklyLimit: 0,
758
+ monthlyLimit: 0,
759
+ },
760
+ },
761
+ });
762
+ const resolveOffPeakSchedule = (input, defaults) => {
763
+ const fallback = defaults.costOptimization.offPeakSchedule ?? {
764
+ start: '22:00',
765
+ end: '06:00',
766
+ timezone: 'UTC',
767
+ reductionPercentage: 0,
768
+ };
769
+ const override = input?.costOptimization?.offPeakSchedule;
770
+ const schedule = { ...fallback };
771
+ if (override) {
772
+ Object.assign(schedule, override);
773
+ }
774
+ return schedule;
775
+ };
776
+ const resolveCostOptimization = (input, defaults) => ({
777
+ ...defaults.costOptimization,
778
+ ...input?.costOptimization,
779
+ offPeakSchedule: resolveOffPeakSchedule(input, defaults),
780
+ budgetAlerts: {
781
+ ...defaults.costOptimization.budgetAlerts,
782
+ ...input?.costOptimization?.budgetAlerts,
783
+ },
784
+ });
785
+ const resolveAutoScalerConfig = (input) => {
786
+ const defaults = buildDefaultAutoScalerConfig();
787
+ if (!input)
788
+ return defaults;
789
+ return {
790
+ ...defaults,
791
+ ...input,
792
+ costOptimization: resolveCostOptimization(input, defaults),
793
+ };
794
+ };
795
+ const resolveWorkerOptions = (config, autoStart) => {
796
+ const options = config.options ? { ...config.options } : {};
797
+ if (options.prefix === undefined) {
798
+ options.prefix = getBullMQSafeQueueName();
799
+ }
800
+ if (options.autorun === undefined) {
801
+ options.autorun = autoStart;
802
+ }
803
+ if (options.connection)
804
+ return options;
805
+ const redisConfig = resolveRedisConfigWithFallback(config.infrastructure?.redis, undefined, 'Worker requires a connection. Provide options.connection or infrastructure.redis config', 'infrastructure.redis');
806
+ return {
807
+ ...options,
808
+ connection: {
809
+ host: redisConfig.host,
810
+ port: redisConfig.port,
811
+ db: redisConfig.db,
812
+ password: redisConfig.password,
813
+ },
814
+ };
815
+ };
816
+ const buildDefaultObservabilityConfig = () => ({
817
+ prometheus: {
818
+ enabled: workersConfig.observability.prometheus.enabled,
819
+ port: workersConfig.observability.prometheus.port,
820
+ },
821
+ openTelemetry: {
822
+ enabled: workersConfig.observability.opentelemetry.enabled,
823
+ serviceName: 'zintrust-workers',
824
+ exporterUrl: workersConfig.observability.opentelemetry.endpoint,
825
+ },
826
+ datadog: {
827
+ enabled: workersConfig.observability.datadog.enabled,
828
+ tags: workersConfig.observability.datadog.apiKey
829
+ ? [`apiKey:${workersConfig.observability.datadog.apiKey}`]
830
+ : undefined,
831
+ },
832
+ });
833
+ const resolveObservabilityConfig = (input) => {
834
+ const defaults = buildDefaultObservabilityConfig();
835
+ if (!input)
836
+ return defaults;
837
+ const enabledOverride = 'enabled' in input ? input.enabled : undefined;
838
+ const prometheus = { ...defaults.prometheus };
839
+ if (input.prometheus) {
840
+ Object.assign(prometheus, input.prometheus);
841
+ }
842
+ const openTelemetry = { ...defaults.openTelemetry };
843
+ if (input.openTelemetry) {
844
+ Object.assign(openTelemetry, input.openTelemetry);
845
+ }
846
+ const datadog = { ...defaults.datadog };
847
+ if (input.datadog) {
848
+ Object.assign(datadog, input.datadog);
849
+ }
850
+ if (enabledOverride === false) {
851
+ prometheus.enabled = false;
852
+ openTelemetry.enabled = false;
853
+ datadog.enabled = false;
854
+ }
855
+ else if (enabledOverride === true) {
856
+ prometheus.enabled = true;
857
+ openTelemetry.enabled = true;
858
+ datadog.enabled = true;
859
+ }
860
+ if (!openTelemetry.serviceName) {
861
+ openTelemetry.serviceName = defaults.openTelemetry.serviceName;
862
+ }
863
+ return { prometheus, openTelemetry, datadog };
864
+ };
865
+ const initializeClustering = (config) => {
866
+ if (clusteringInitialized || !(config.features?.clustering ?? false))
867
+ return;
868
+ const redisConfig = resolveRedisConfigWithFallback(config.infrastructure?.redis, undefined, 'ClusterLock requires infrastructure.redis config', 'infrastructure.redis');
869
+ ClusterLock.initialize(redisConfig);
870
+ clusteringInitialized = true;
871
+ };
872
+ const initializeMetrics = (config) => {
873
+ if (metricsInitialized || !(config.features?.metrics ?? false))
874
+ return;
875
+ const redisConfig = resolveRedisConfigWithFallback(config.infrastructure?.redis, undefined, 'WorkerMetrics requires infrastructure.redis config', 'infrastructure.redis');
876
+ WorkerMetrics.initialize(redisConfig);
877
+ metricsInitialized = true;
878
+ };
879
+ const initializeAutoScaling = (config) => {
880
+ if (autoScalingInitialized || !(config.features?.autoScaling ?? false))
881
+ return;
882
+ const autoScalerConfig = resolveAutoScalerConfig(config.infrastructure?.autoScaler);
883
+ AutoScaler.initialize(autoScalerConfig);
884
+ autoScalingInitialized = true;
885
+ };
886
+ const initializeCircuitBreaker = (config, version) => {
887
+ if (!(config.features?.circuitBreaker ?? false))
888
+ return;
889
+ CircuitBreaker.initialize(config.name, version);
890
+ };
891
+ const initializeDeadLetterQueue = (config) => {
892
+ if (deadLetterQueueInitialized || !(config.features?.deadLetterQueue ?? false))
893
+ return;
894
+ const dlqConfig = requireInfrastructure(config.infrastructure?.deadLetterQueue, 'DeadLetterQueue requires infrastructure.deadLetterQueue config');
895
+ const dlqRedisConfig = resolveRedisConfigWithFallback(dlqConfig.redis, config.infrastructure?.redis, 'DeadLetterQueue requires infrastructure.deadLetterQueue.redis or infrastructure.redis config', 'infrastructure.deadLetterQueue.redis');
896
+ DeadLetterQueue.initialize(dlqRedisConfig, dlqConfig.policy);
897
+ deadLetterQueueInitialized = true;
898
+ };
899
+ const initializeResourceMonitoring = (config) => {
900
+ if (resourceMonitoringInitialized || !(config.features?.resourceMonitoring ?? false))
901
+ return;
902
+ ResourceMonitor.initialize();
903
+ ResourceMonitor.start();
904
+ resourceMonitoringInitialized = true;
905
+ };
906
+ const initializeCompliance = (config) => {
907
+ if (complianceInitialized || !(config.features?.compliance ?? false))
908
+ return;
909
+ const complianceConfig = requireInfrastructure(config.infrastructure?.compliance, 'ComplianceManager requires infrastructure.compliance config');
910
+ const complianceRedisConfig = resolveRedisConfigWithFallback(complianceConfig.redis, config.infrastructure?.redis, 'ComplianceManager requires infrastructure.compliance.redis or infrastructure.redis config', 'infrastructure.compliance.redis');
911
+ ComplianceManager.initialize(complianceRedisConfig, complianceConfig.config);
912
+ complianceInitialized = true;
913
+ };
914
+ const initializeObservability = async (config) => {
915
+ if (observabilityInitialized || !(config.features?.observability ?? false))
916
+ return;
917
+ const observabilityConfig = resolveObservabilityConfig(config.infrastructure?.observability);
918
+ await Observability.initialize(observabilityConfig);
919
+ observabilityInitialized = true;
920
+ };
921
+ const initializeVersioning = (config, version) => {
922
+ if (!(config.features?.versioning ?? false))
923
+ return;
924
+ WorkerVersioning.register({
925
+ workerName: config.name,
926
+ version: WorkerVersioning.parse(version),
927
+ changelog: 'Initial version',
928
+ });
929
+ };
930
+ const initializeDatacenter = (config) => {
931
+ if (!(config.features?.datacenterOrchestration ?? false) || !config.datacenter)
932
+ return;
933
+ DatacenterOrchestrator.placeWorker({
934
+ workerName: config.name,
935
+ primaryRegion: config.datacenter.primaryRegion,
936
+ secondaryRegions: config.datacenter.secondaryRegions ?? [],
937
+ replicationStrategy: 'active-passive',
938
+ affinityRules: {
939
+ preferLocal: config.datacenter.affinityRules?.preferLocal ?? true,
940
+ maxLatency: config.datacenter.affinityRules?.maxLatency,
941
+ avoidRegions: config.datacenter.affinityRules?.avoidRegions,
942
+ },
943
+ });
944
+ };
945
+ const setupWorkerEventListeners = (worker, workerName, workerVersion, features) => {
946
+ worker.on('completed', (job) => {
947
+ Logger.debug(`Job completed: ${workerName}`, { jobId: job.id });
948
+ if (features?.observability === true) {
949
+ Observability.incrementCounter('worker.jobs.completed', 1, {
950
+ worker: workerName,
951
+ version: workerVersion,
952
+ });
953
+ }
954
+ });
955
+ worker.on('failed', (job, error) => {
956
+ Logger.error(`Job failed: ${workerName}`, { error, jobId: job?.id }, 'workers');
957
+ if (features?.observability === true) {
958
+ Observability.incrementCounter('worker.jobs.failed', 1, {
959
+ worker: workerName,
960
+ version: workerVersion,
961
+ });
962
+ }
963
+ });
964
+ worker.on('error', (error) => {
965
+ Logger.error(`Worker error: ${workerName}`, error);
966
+ });
967
+ };
968
+ const registerWorkerInstance = (params) => {
969
+ const { worker, config, workerVersion, queueName, options, autoStart } = params;
970
+ WorkerRegistry.register({
971
+ name: config.name,
972
+ config: {},
973
+ version: workerVersion,
974
+ region: config.datacenter?.primaryRegion,
975
+ queues: [queueName],
976
+ factory: async () => {
977
+ await Promise.resolve();
978
+ return {
979
+ metadata: {
980
+ name: config.name,
981
+ status: autoStart ? 'running' : 'stopped',
982
+ version: workerVersion,
983
+ region: config.datacenter?.primaryRegion ?? 'unknown',
984
+ queueName,
985
+ concurrency: options?.concurrency ?? 1,
986
+ startedAt: new Date(),
987
+ stoppedAt: null,
988
+ lastProcessedAt: null,
989
+ restartCount: 0,
990
+ processedCount: 0,
991
+ errorCount: 0,
992
+ lockKey: null,
993
+ priority: 0,
994
+ memoryUsage: 0,
995
+ cpuUsage: 0,
996
+ circuitState: 'closed',
997
+ queues: [queueName],
998
+ plugins: [],
999
+ datacenter: config.datacenter?.primaryRegion ?? 'unknown',
1000
+ canaryPercentage: 0,
1001
+ config: {},
1002
+ },
1003
+ instance: worker,
1004
+ start: () => {
1005
+ if (!autoStart) {
1006
+ worker.run().catch((error) => {
1007
+ Logger.error(`Failed to start worker "${config.name}"`, error);
1008
+ });
1009
+ }
1010
+ },
1011
+ stop: async () => worker.close(),
1012
+ drain: async () => worker.close(),
1013
+ sleep: async () => worker.pause(),
1014
+ wakeup: () => {
1015
+ worker.resume();
1016
+ },
1017
+ getStatus: () => 'running',
1018
+ getHealth: () => 'green',
1019
+ };
1020
+ },
1021
+ });
1022
+ };
1023
+ const initializeWorkerFeatures = async (config, workerVersion) => {
1024
+ initializeClustering(config);
1025
+ initializeMetrics(config);
1026
+ initializeAutoScaling(config);
1027
+ initializeCircuitBreaker(config, workerVersion);
1028
+ initializeDeadLetterQueue(config);
1029
+ initializeResourceMonitoring(config);
1030
+ initializeCompliance(config);
1031
+ await initializeObservability(config);
1032
+ initializeVersioning(config, workerVersion);
1033
+ initializeDatacenter(config);
1034
+ };
1035
+ /**
1036
+ * Worker Factory - Sealed namespace
1037
+ */
1038
+ export const WorkerFactory = Object.freeze({
1039
+ registerProcessor,
1040
+ registerProcessors,
1041
+ registerProcessorPaths,
1042
+ registerProcessorResolver,
1043
+ resolveProcessorPath,
1044
+ /**
1045
+ * Create new worker with full setup
1046
+ */
1047
+ async create(config) {
1048
+ const { name, version, queueName, features } = config;
1049
+ const workerVersion = version ?? '1.0.0';
1050
+ const autoStart = resolveAutoStart(config);
1051
+ if (workers.has(name)) {
1052
+ throw ErrorFactory.createWorkerError(`Worker "${name}" already exists`);
1053
+ }
1054
+ // Resolve the correct store for this worker configuration
1055
+ const store = await getStoreForWorker(config);
1056
+ // Save initial status as "creating"
1057
+ await store.save(buildWorkerRecord(config, WorkerCreationStatus.CREATING));
1058
+ try {
1059
+ await initializeWorkerFeatures(config, workerVersion);
1060
+ // Update status to "connecting"
1061
+ await store.update(name, {
1062
+ status: WorkerCreationStatus.CONNECTING,
1063
+ updatedAt: new Date(),
1064
+ });
1065
+ // Create enhanced processor
1066
+ const enhancedProcessor = createEnhancedProcessor(config);
1067
+ // Create BullMQ worker
1068
+ const resolvedOptions = resolveWorkerOptions(config, autoStart);
1069
+ const worker = new Worker(queueName, enhancedProcessor, resolvedOptions);
1070
+ setupWorkerEventListeners(worker, name, workerVersion, features);
1071
+ // Update status to "starting"
1072
+ await store.update(name, {
1073
+ status: WorkerCreationStatus.STARTING,
1074
+ updatedAt: new Date(),
1075
+ });
1076
+ const timeoutMs = Env.getInt('WORKER_CONNECTION_TIMEOUT', 5000);
1077
+ // Wait for actual connection and health verification
1078
+ await waitForWorkerConnection(worker, name, queueName, timeoutMs);
1079
+ // Update status to "running" only after successful connection
1080
+ await store.update(name, {
1081
+ status: WorkerCreationStatus.RUNNING,
1082
+ updatedAt: new Date(),
1083
+ });
1084
+ // Store worker instance
1085
+ const instance = {
1086
+ worker,
1087
+ config,
1088
+ startedAt: new Date(),
1089
+ status: WorkerCreationStatus.RUNNING,
1090
+ connectionState: 'connected',
1091
+ };
1092
+ workers.set(name, instance);
1093
+ registerWorkerInstance({
1094
+ worker,
1095
+ config,
1096
+ workerVersion,
1097
+ queueName,
1098
+ options: resolvedOptions,
1099
+ autoStart,
1100
+ });
1101
+ if (autoStart) {
1102
+ await WorkerRegistry.start(name, workerVersion);
1103
+ }
1104
+ // Execute afterStart hooks
1105
+ if (features?.plugins === true) {
1106
+ await PluginManager.executeHook('afterStart', {
1107
+ workerName: name,
1108
+ timestamp: new Date(),
1109
+ });
1110
+ }
1111
+ // Start health monitoring for the worker
1112
+ startHealthMonitoring(name, worker, queueName);
1113
+ Logger.info(`Worker created: ${name}@${workerVersion}`, {
1114
+ queueName,
1115
+ features: Object.keys(features ?? {}).filter((k) => features?.[k] === true),
1116
+ });
1117
+ return worker;
1118
+ }
1119
+ catch (error) {
1120
+ // Handle failure - update status to "failed"
1121
+ // Re-resolve store in case of error to be safe
1122
+ const failStore = await getStoreForWorker(config);
1123
+ await failStore.update(name, {
1124
+ status: WorkerCreationStatus.FAILED,
1125
+ updatedAt: new Date(),
1126
+ lastError: error.message,
1127
+ });
1128
+ Logger.error(`Worker creation failed: ${name}`, error);
1129
+ throw error;
1130
+ }
1131
+ },
1132
+ /**
1133
+ * Get worker instance
1134
+ */
1135
+ get(name) {
1136
+ const instance = workers.get(name);
1137
+ return instance ? { ...instance } : null;
1138
+ },
1139
+ /**
1140
+ * Update worker status directly (used by HealthMonitor)
1141
+ */
1142
+ async updateStatus(name, status, error) {
1143
+ const instance = workers.get(name);
1144
+ if (instance) {
1145
+ instance.status = status;
1146
+ }
1147
+ try {
1148
+ const store = await getStoreForWorker(instance?.config ?? {
1149
+ name,
1150
+ queueName: 'unknown',
1151
+ processor: async () => {
1152
+ return Promise.resolve(); //NOSONAR
1153
+ },
1154
+ });
1155
+ const errorMessage = typeof error === 'string' ? error : error?.message;
1156
+ await store.update(name, {
1157
+ status: status,
1158
+ updatedAt: new Date(),
1159
+ lastError: errorMessage,
1160
+ });
1161
+ }
1162
+ catch (err) {
1163
+ Logger.warn(`Failed to update status for ${name} to ${status}`, err);
1164
+ }
1165
+ },
1166
+ /**
1167
+ * Stop worker
1168
+ */
1169
+ async stop(name, persistenceOverride) {
1170
+ const instance = workers.get(name);
1171
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1172
+ if (!instance) {
1173
+ await store.update(name, { status: 'stopped', updatedAt: new Date() });
1174
+ Logger.info(`Worker marked stopped (not running): ${name}`);
1175
+ return;
1176
+ }
1177
+ // Execute beforeStop hooks
1178
+ if (instance.config.features?.plugins === true) {
1179
+ await PluginManager.executeHook('beforeStop', {
1180
+ workerName: name,
1181
+ timestamp: new Date(),
1182
+ });
1183
+ }
1184
+ // Close worker with timeout to prevent hanging
1185
+ const workerClosePromise = instance.worker.close();
1186
+ let timeoutId;
1187
+ const timeoutPromise = new Promise((_, reject) => {
1188
+ // eslint-disable-next-line no-restricted-syntax
1189
+ timeoutId = setTimeout(() => {
1190
+ reject(new Error('Worker close timeout'));
1191
+ }, 5000);
1192
+ });
1193
+ try {
1194
+ await Promise.race([workerClosePromise, timeoutPromise]);
1195
+ }
1196
+ catch (error) {
1197
+ Logger.warn(`Worker "${name}" close failed or timed out, continuing...`, error);
1198
+ }
1199
+ finally {
1200
+ // Always clean up timeout to prevent memory leak
1201
+ if (timeoutId) {
1202
+ clearTimeout(timeoutId);
1203
+ timeoutId = undefined;
1204
+ }
1205
+ }
1206
+ instance.status = WorkerCreationStatus.STOPPED;
1207
+ // Stop health monitoring for this worker
1208
+ HealthMonitor.unregister(name);
1209
+ try {
1210
+ await store.update(name, {
1211
+ status: WorkerCreationStatus.STOPPED,
1212
+ updatedAt: new Date(),
1213
+ });
1214
+ Logger.info(`Worker "${name}" status updated to stopped`);
1215
+ }
1216
+ catch (error) {
1217
+ Logger.error(`Failed to update worker "${name}" status`, error);
1218
+ }
1219
+ await WorkerRegistry.stop(name);
1220
+ // Execute afterStop hooks
1221
+ if (instance.config.features?.plugins === true) {
1222
+ await PluginManager.executeHook('afterStop', {
1223
+ workerName: name,
1224
+ timestamp: new Date(),
1225
+ });
1226
+ }
1227
+ Logger.info(`Worker stopped: ${name}`);
1228
+ },
1229
+ /**
1230
+ * Restart worker
1231
+ */
1232
+ async restart(name, persistenceOverride) {
1233
+ const instance = workers.get(name);
1234
+ if (!instance) {
1235
+ await WorkerFactory.startFromPersisted(name, persistenceOverride);
1236
+ Logger.info(`Worker started from persistence: ${name}`);
1237
+ return;
1238
+ }
1239
+ await WorkerFactory.stop(name, persistenceOverride);
1240
+ const refreshed = workers.get(name);
1241
+ if (!refreshed) {
1242
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found`);
1243
+ }
1244
+ workers.delete(name);
1245
+ const newWorker = await WorkerFactory.create(refreshed.config);
1246
+ refreshed.worker = newWorker;
1247
+ refreshed.status = WorkerCreationStatus.RUNNING;
1248
+ refreshed.startedAt = new Date();
1249
+ Logger.info(`Worker restarted: ${name}`);
1250
+ },
1251
+ /**
1252
+ * Pause worker
1253
+ */
1254
+ async pause(name, persistenceOverride) {
1255
+ const instance = workers.get(name);
1256
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1257
+ if (instance) {
1258
+ await instance.worker.pause();
1259
+ instance.status = WorkerCreationStatus.STARTING; // Using STARTING as equivalent to sleeping/paused
1260
+ }
1261
+ await store.update(name, {
1262
+ status: WorkerCreationStatus.STARTING,
1263
+ updatedAt: new Date(),
1264
+ });
1265
+ Logger.info(`Worker paused: ${name}`);
1266
+ },
1267
+ /**
1268
+ * Resume worker
1269
+ */
1270
+ async resume(name, persistenceOverride) {
1271
+ const instance = workers.get(name);
1272
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1273
+ if (instance) {
1274
+ instance.worker.resume();
1275
+ instance.status = WorkerCreationStatus.RUNNING;
1276
+ }
1277
+ try {
1278
+ await store.update(name, { status: WorkerCreationStatus.RUNNING, updatedAt: new Date() });
1279
+ }
1280
+ catch (error) {
1281
+ Logger.error('Failed to persist worker resume', error);
1282
+ }
1283
+ Logger.info(`Worker resumed: ${name}`);
1284
+ },
1285
+ /**
1286
+ * Update auto-start for persisted worker
1287
+ */
1288
+ async setAutoStart(name, autoStart, persistenceOverride) {
1289
+ const instance = workers.get(name);
1290
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1291
+ if (instance) {
1292
+ instance.config.autoStart = autoStart;
1293
+ }
1294
+ await store.update(name, { autoStart, updatedAt: new Date() });
1295
+ if (!autoStart)
1296
+ return;
1297
+ const refreshed = workers.get(name);
1298
+ if (refreshed) {
1299
+ if (refreshed.status !== 'running') {
1300
+ await WorkerFactory.start(name, persistenceOverride);
1301
+ }
1302
+ return;
1303
+ }
1304
+ await WorkerFactory.startFromPersisted(name, persistenceOverride);
1305
+ },
1306
+ /**
1307
+ * Update persisted worker record and in-memory config if running.
1308
+ */
1309
+ async update(name, patch, persistenceOverride) {
1310
+ const instance = workers.get(name);
1311
+ const store = await getStoreForWorker(instance?.config, persistenceOverride);
1312
+ const current = await store.get(name);
1313
+ if (!current) {
1314
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
1315
+ }
1316
+ const merged = {
1317
+ ...current,
1318
+ ...patch,
1319
+ updatedAt: patch.updatedAt ?? new Date(),
1320
+ };
1321
+ // Use save() which will insert or update appropriately for each store
1322
+ await store.save(merged);
1323
+ // If the worker is running in memory, update its runtime config so restarts use the new config
1324
+ if (instance) {
1325
+ const cfg = instance.config;
1326
+ instance.config = {
1327
+ ...cfg,
1328
+ version: merged.version ?? cfg.version,
1329
+ queueName: merged.queueName ?? cfg.queueName,
1330
+ options: {
1331
+ ...cfg.options,
1332
+ concurrency: merged.concurrency ?? cfg.options?.concurrency,
1333
+ },
1334
+ infrastructure: merged.infrastructure ?? cfg.infrastructure,
1335
+ features: merged.features ?? cfg.features,
1336
+ datacenter: merged.datacenter ?? cfg.datacenter,
1337
+ };
1338
+ }
1339
+ },
1340
+ /**
1341
+ * Start worker
1342
+ */
1343
+ async start(name, persistenceOverride) {
1344
+ const instance = workers.get(name);
1345
+ // Even if instance exists, we must validate against the requested driver
1346
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1347
+ if (!instance) {
1348
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found`);
1349
+ }
1350
+ const version = instance.config.version ?? '1.0.0';
1351
+ await WorkerRegistry.start(name, version);
1352
+ instance.status = WorkerCreationStatus.RUNNING;
1353
+ instance.startedAt = new Date();
1354
+ await store.update(name, { status: WorkerCreationStatus.RUNNING, updatedAt: new Date() });
1355
+ Logger.info(`Worker started: ${name}`);
1356
+ },
1357
+ /**
1358
+ * List all workers
1359
+ */
1360
+ list() {
1361
+ return Array.from(workers.keys());
1362
+ },
1363
+ /**
1364
+ * List all persisted workers
1365
+ */
1366
+ async listPersisted(persistenceOverride, options) {
1367
+ const records = await WorkerFactory.listPersistedRecords(persistenceOverride, options);
1368
+ return records.map((record) => record.name);
1369
+ },
1370
+ async listPersistedRecords(persistenceOverride, options) {
1371
+ if (!persistenceOverride) {
1372
+ await ensureWorkerStoreConfigured();
1373
+ return workerStore.list(options);
1374
+ }
1375
+ const store = await resolveWorkerStoreForPersistence(persistenceOverride);
1376
+ return store.list(options);
1377
+ },
1378
+ /**
1379
+ * Start a worker from persisted storage when it is not registered.
1380
+ */
1381
+ async startFromPersisted(name, persistenceOverride) {
1382
+ const record = await getPersistedRecord(name, persistenceOverride);
1383
+ if (!record) {
1384
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found in persistence store`);
1385
+ }
1386
+ let processor = await resolveProcessor(name);
1387
+ if (!processor && record.processorPath) {
1388
+ try {
1389
+ processor = await resolveProcessorFromPath(record.processorPath);
1390
+ }
1391
+ catch (error) {
1392
+ Logger.error(`Failed to resolve processor module for "${name}"`, error);
1393
+ }
1394
+ }
1395
+ if (!processor) {
1396
+ throw ErrorFactory.createConfigError(`Worker "${name}" processor is not registered or resolvable. Register the processor at startup or persist a processorPath.`);
1397
+ }
1398
+ await WorkerFactory.create({
1399
+ name: record.name,
1400
+ queueName: record.queueName,
1401
+ version: record.version ?? undefined,
1402
+ processor,
1403
+ processorPath: record.processorPath ?? undefined,
1404
+ autoStart: true, // Override to true when manually starting
1405
+ options: { concurrency: record.concurrency },
1406
+ infrastructure: record.infrastructure,
1407
+ features: record.features,
1408
+ datacenter: record.datacenter,
1409
+ });
1410
+ },
1411
+ /**
1412
+ * Get persisted worker record
1413
+ */
1414
+ async getPersisted(name, persistenceOverride) {
1415
+ const instance = workers.get(name);
1416
+ const store = await getStoreForWorker(instance?.config, persistenceOverride);
1417
+ return store.get(name);
1418
+ },
1419
+ /**
1420
+ * Remove worker
1421
+ */
1422
+ async remove(name, persistenceOverride) {
1423
+ const instance = workers.get(name);
1424
+ // Validate that worker exists in the store we are trying to remove from
1425
+ const store = await validateAndGetStore(name, instance?.config, persistenceOverride);
1426
+ if (instance) {
1427
+ await WorkerFactory.stop(name, persistenceOverride);
1428
+ const registry = WorkerRegistry;
1429
+ registry.unregister?.(name);
1430
+ AutoScaler.clearHistory(name);
1431
+ ResourceMonitor.clearHistory(name);
1432
+ CircuitBreaker.deleteWorker(name);
1433
+ CanaryController.purge(name);
1434
+ WorkerVersioning.clear(name);
1435
+ DatacenterOrchestrator.removeWorker(name);
1436
+ await Observability.clearWorkerMetrics(name);
1437
+ // Stop health monitoring for this worker
1438
+ HealthMonitor.unregister(name);
1439
+ workers.delete(name);
1440
+ }
1441
+ await store.remove(name);
1442
+ Logger.info(`Worker removed: ${name}`);
1443
+ },
1444
+ /**
1445
+ * Get worker metrics
1446
+ */
1447
+ async getMetrics(name) {
1448
+ const instance = workers.get(name);
1449
+ if (!instance) {
1450
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found`);
1451
+ }
1452
+ if (instance.config.features?.metrics === undefined || !instance.config.features?.metrics) {
1453
+ return null;
1454
+ }
1455
+ const now = Date.now();
1456
+ const oneHourAgo = now - 3600 * 1000;
1457
+ const metrics = await WorkerMetrics.aggregate({
1458
+ workerName: name,
1459
+ metricType: 'processed',
1460
+ granularity: 'hourly',
1461
+ startDate: new Date(oneHourAgo),
1462
+ endDate: new Date(now),
1463
+ });
1464
+ return metrics;
1465
+ },
1466
+ /**
1467
+ * Get worker health
1468
+ */
1469
+ async getHealth(name) {
1470
+ const instance = workers.get(name);
1471
+ if (!instance) {
1472
+ throw ErrorFactory.createNotFoundError(`Worker "${name}" not found`);
1473
+ }
1474
+ if (!(instance.config.features?.metrics ?? false)) {
1475
+ return { status: 'unknown' };
1476
+ }
1477
+ const health = await WorkerMetrics.getLatestHealth(name);
1478
+ return health;
1479
+ },
1480
+ /**
1481
+ * Shutdown all workers
1482
+ */
1483
+ async shutdown() {
1484
+ Logger.info('WorkerFactory shutting down...');
1485
+ const workerNames = Array.from(workers.keys());
1486
+ await Promise.all(workerNames.map(async (name) => WorkerFactory.stop(name)));
1487
+ // Shutdown all modules
1488
+ ResourceMonitor.stop();
1489
+ await WorkerMetrics.shutdown();
1490
+ await MultiQueueWorker.shutdown();
1491
+ await ComplianceManager.shutdown();
1492
+ await PriorityQueue.shutdown();
1493
+ HealthMonitor.shutdown();
1494
+ AutoScaler.stop();
1495
+ ClusterLock.shutdown();
1496
+ WorkerVersioning.shutdown();
1497
+ CanaryController.shutdown();
1498
+ DatacenterOrchestrator.shutdown();
1499
+ PluginManager.shutdown();
1500
+ Observability.shutdown();
1501
+ await DeadLetterQueue.shutdown();
1502
+ CircuitBreaker.shutdown();
1503
+ workers.clear();
1504
+ Logger.info('WorkerFactory shutdown complete');
1505
+ },
1506
+ });
1507
+ // Graceful shutdown handled by WorkerShutdown