@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,509 @@
1
+ /**
2
+ * Worker Metrics Manager
3
+ * Time-series metrics persistence with Redis Sorted Sets
4
+ * Sealed namespace for immutability
5
+ */
6
+ import { ErrorFactory, Logger, appConfig, createRedisConnection, } from '@zintrust/core';
7
+ const PREFIX = appConfig.prefix;
8
+ // Redis key prefixes
9
+ const METRICS_PREFIX = `${PREFIX}:worker:metrics:`;
10
+ const HEALTH_PREFIX = `${PREFIX}:worker:health:`;
11
+ // Retention periods (in seconds)
12
+ const RETENTION = {
13
+ hourly: 7 * 24 * 60 * 60, // 7 days
14
+ daily: 30 * 24 * 60 * 60, // 30 days
15
+ monthly: 365 * 24 * 60 * 60, // 1 year
16
+ };
17
+ // Internal state
18
+ let redisClient = null;
19
+ /**
20
+ * Helper: Get Redis key for metrics
21
+ */
22
+ const getMetricsKey = (workerName, metricType, granularity) => {
23
+ return `${METRICS_PREFIX}${workerName}:${metricType}:${granularity}`;
24
+ };
25
+ /**
26
+ * Helper: Get Redis key for health scores
27
+ */
28
+ const getHealthKey = (workerName) => {
29
+ return `${HEALTH_PREFIX}${workerName}`;
30
+ };
31
+ /**
32
+ * Helper: Round timestamp to granularity
33
+ */
34
+ const roundTimestamp = (date, granularity) => {
35
+ const timestamp = date.getTime();
36
+ switch (granularity) {
37
+ case 'hourly':
38
+ // Round to nearest hour
39
+ return new Date(Math.floor(timestamp / (60 * 60 * 1000)) * 60 * 60 * 1000);
40
+ case 'daily': {
41
+ // Round to start of day (UTC)
42
+ const d = new Date(timestamp);
43
+ d.setUTCHours(0, 0, 0, 0);
44
+ return d;
45
+ }
46
+ case 'monthly': {
47
+ // Round to start of month (UTC)
48
+ const m = new Date(timestamp);
49
+ m.setUTCDate(1);
50
+ m.setUTCHours(0, 0, 0, 0);
51
+ return m;
52
+ }
53
+ }
54
+ };
55
+ /**
56
+ * Helper: Clean up old metrics based on retention policy
57
+ */
58
+ const cleanupOldMetrics = async (client, key, granularity) => {
59
+ try {
60
+ const retentionSeconds = RETENTION[granularity];
61
+ const cutoffTimestamp = Date.now() - retentionSeconds * 1000;
62
+ // Remove entries older than retention period
63
+ await client.zremrangebyscore(key, '-inf', cutoffTimestamp);
64
+ // Set expiry on the key (2x retention period for safety)
65
+ await client.expire(key, retentionSeconds * 2);
66
+ }
67
+ catch (error) {
68
+ Logger.error(`Failed to cleanup old metrics for key "${key}"`, error);
69
+ }
70
+ };
71
+ /**
72
+ * Helper: Calculate health score based on metrics
73
+ */
74
+ const calculateHealthScore = (metrics) => {
75
+ // Error rate factor (0-100, lower is better)
76
+ // 0% errors = 100, 10%+ errors = 0
77
+ const errorRateFactor = Math.max(0, 100 - metrics.errorRate * 1000);
78
+ // Throughput factor (0-100, higher is better)
79
+ // Normalized: >100 jobs/min = 100, 0 jobs/min = 0
80
+ const throughputFactor = Math.min(100, metrics.throughput);
81
+ // Latency factor (0-100, lower is better)
82
+ // <1s = 100, >10s = 0
83
+ const latencyFactor = Math.max(0, 100 - (metrics.avgDuration / 10000) * 100);
84
+ // Resource usage factor (0-100, lower is better)
85
+ // <50% = 100, >90% = 0
86
+ const avgResourceUsage = (metrics.memoryUsage + metrics.cpuUsage) / 2;
87
+ const resourceFactor = Math.max(0, 100 - Math.max(0, avgResourceUsage - 50) * 2.5);
88
+ // Weighted average: errors are most important
89
+ const score = errorRateFactor * 0.4 + throughputFactor * 0.2 + latencyFactor * 0.2 + resourceFactor * 0.2;
90
+ let status;
91
+ if (score >= 80) {
92
+ status = 'healthy';
93
+ }
94
+ else if (score >= 50) {
95
+ status = 'degraded';
96
+ }
97
+ else {
98
+ status = 'unhealthy';
99
+ }
100
+ return {
101
+ score: Math.round(score),
102
+ status,
103
+ factors: {
104
+ errorRate: Math.round(errorRateFactor),
105
+ throughput: Math.round(throughputFactor),
106
+ latency: Math.round(latencyFactor),
107
+ resourceUsage: Math.round(resourceFactor),
108
+ },
109
+ };
110
+ };
111
+ /**
112
+ * Worker Metrics Manager - Sealed namespace
113
+ */
114
+ export const WorkerMetrics = Object.freeze({
115
+ /**
116
+ * Initialize the metrics manager with Redis connection
117
+ */
118
+ initialize(config) {
119
+ if (redisClient) {
120
+ Logger.warn('WorkerMetrics already initialized');
121
+ return;
122
+ }
123
+ redisClient = createRedisConnection(config);
124
+ Logger.info('WorkerMetrics initialized');
125
+ },
126
+ /**
127
+ * Record a metric point
128
+ */
129
+ async record(workerName, metricType, value, metadata) {
130
+ if (!redisClient) {
131
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized. Call initialize() first.');
132
+ }
133
+ const now = new Date();
134
+ // Record at all granularities
135
+ const granularities = ['hourly', 'daily', 'monthly'];
136
+ await Promise.all(granularities.map(async (granularity) => {
137
+ const roundedTimestamp = roundTimestamp(now, granularity);
138
+ const key = getMetricsKey(workerName, metricType, granularity);
139
+ const point = {
140
+ timestamp: roundedTimestamp,
141
+ value,
142
+ metadata,
143
+ };
144
+ // Store in sorted set with timestamp as score
145
+ const score = roundedTimestamp.getTime();
146
+ const data = JSON.stringify(point);
147
+ await redisClient?.zadd(key, score, data);
148
+ // Cleanup old metrics (lightweight: ~1% based on time slice)
149
+ const client = redisClient;
150
+ if (client && Date.now() % 100 === 0) {
151
+ cleanupOldMetrics(client, key, granularity).catch((err) => {
152
+ Logger.error('Failed to cleanup old metrics', err);
153
+ });
154
+ }
155
+ }));
156
+ Logger.debug(`Recorded metric: ${workerName}/${metricType} = ${value}`);
157
+ },
158
+ /**
159
+ * Record multiple metrics at once (batch operation)
160
+ */
161
+ async recordBatch(workerName, metrics) {
162
+ await Promise.all(metrics.map(async (m) => WorkerMetrics.record(workerName, m.metricType, m.value, m.metadata)));
163
+ },
164
+ /**
165
+ * Query metrics for a time range
166
+ */
167
+ async query(options) {
168
+ if (!redisClient) {
169
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
170
+ }
171
+ const { workerName, metricType, granularity, startDate, endDate, limit = 1000 } = options;
172
+ const key = getMetricsKey(workerName, metricType, granularity);
173
+ const minScore = startDate ? startDate.getTime() : '-inf';
174
+ const maxScore = endDate ? endDate.getTime() : '+inf';
175
+ try {
176
+ // Get data from sorted set
177
+ const results = await redisClient.zrangebyscore(key, minScore, maxScore, 'LIMIT', 0, limit);
178
+ const points = results.map((data) => JSON.parse(data));
179
+ return {
180
+ workerName,
181
+ metricType,
182
+ granularity,
183
+ points,
184
+ };
185
+ }
186
+ catch (error) {
187
+ Logger.error(`Error querying metrics for ${workerName}/${metricType}`, error);
188
+ throw error;
189
+ }
190
+ },
191
+ /**
192
+ * Get aggregated metrics for a time range
193
+ */
194
+ async aggregate(options) {
195
+ const entry = await WorkerMetrics.query(options);
196
+ if (entry.points.length === 0) {
197
+ return {
198
+ workerName: entry.workerName,
199
+ metricType: entry.metricType,
200
+ period: {
201
+ start: options.startDate ?? new Date(0),
202
+ end: options.endDate ?? new Date(),
203
+ },
204
+ total: 0,
205
+ average: 0,
206
+ min: 0,
207
+ max: 0,
208
+ count: 0,
209
+ };
210
+ }
211
+ const values = entry.points.map((p) => p.value);
212
+ const total = values.reduce((sum, val) => sum + val, 0);
213
+ const average = total / values.length;
214
+ const min = Math.min(...values);
215
+ const max = Math.max(...values);
216
+ return {
217
+ workerName: entry.workerName,
218
+ metricType: entry.metricType,
219
+ period: {
220
+ start: entry.points[0].timestamp,
221
+ end: entry.points.at(-1)?.timestamp ?? new Date(),
222
+ },
223
+ total,
224
+ average,
225
+ min,
226
+ max,
227
+ count: values.length,
228
+ };
229
+ },
230
+ async aggregateBatch(optionsList) {
231
+ if (!redisClient) {
232
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
233
+ }
234
+ if (optionsList.length === 0)
235
+ return [];
236
+ const pipeline = redisClient.pipeline();
237
+ for (const options of optionsList) {
238
+ const { workerName, metricType, granularity, startDate, endDate, limit = 1000 } = options;
239
+ const key = getMetricsKey(workerName, metricType, granularity);
240
+ const minScore = startDate ? startDate.getTime() : '-inf';
241
+ const maxScore = endDate ? endDate.getTime() : '+inf';
242
+ pipeline.zrangebyscore(key, minScore, maxScore, 'LIMIT', 0, limit);
243
+ }
244
+ const results = await pipeline.exec();
245
+ if (!results) {
246
+ throw ErrorFactory.createWorkerError('Failed to execute metrics pipeline');
247
+ }
248
+ return optionsList.map((options, index) => {
249
+ const [err, data] = results[index];
250
+ if (err) {
251
+ Logger.error(`Error querying metrics for ${options.workerName}/${options.metricType}`, err);
252
+ return {
253
+ workerName: options.workerName,
254
+ metricType: options.metricType,
255
+ period: { start: options.startDate ?? new Date(), end: options.endDate ?? new Date() },
256
+ total: 0,
257
+ average: 0,
258
+ min: 0,
259
+ max: 0,
260
+ count: 0,
261
+ };
262
+ }
263
+ const points = data.map((d) => JSON.parse(d));
264
+ if (points.length === 0) {
265
+ return {
266
+ workerName: options.workerName,
267
+ metricType: options.metricType,
268
+ period: { start: options.startDate ?? new Date(0), end: options.endDate ?? new Date() },
269
+ total: 0,
270
+ average: 0,
271
+ min: 0,
272
+ max: 0,
273
+ count: 0,
274
+ };
275
+ }
276
+ const values = points.map((p) => p.value);
277
+ const total = values.reduce((sum, val) => sum + val, 0);
278
+ const average = total / values.length;
279
+ const min = Math.min(...values);
280
+ const max = Math.max(...values);
281
+ return {
282
+ workerName: options.workerName,
283
+ metricType: options.metricType,
284
+ period: {
285
+ start: points[0].timestamp,
286
+ end: points.at(-1)?.timestamp ?? new Date(),
287
+ },
288
+ total,
289
+ average,
290
+ min,
291
+ max,
292
+ count: values.length,
293
+ };
294
+ });
295
+ },
296
+ /**
297
+ * Calculate and store health score
298
+ */
299
+ async calculateHealth(workerName) {
300
+ if (!redisClient) {
301
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
302
+ }
303
+ const now = new Date();
304
+ const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
305
+ try {
306
+ // Get recent metrics (last hour)
307
+ const [processed, errors, duration, memory, cpu] = await Promise.all([
308
+ WorkerMetrics.aggregate({
309
+ workerName,
310
+ metricType: 'processed',
311
+ granularity: 'hourly',
312
+ startDate: oneHourAgo,
313
+ endDate: now,
314
+ }),
315
+ WorkerMetrics.aggregate({
316
+ workerName,
317
+ metricType: 'errors',
318
+ granularity: 'hourly',
319
+ startDate: oneHourAgo,
320
+ endDate: now,
321
+ }),
322
+ WorkerMetrics.aggregate({
323
+ workerName,
324
+ metricType: 'duration',
325
+ granularity: 'hourly',
326
+ startDate: oneHourAgo,
327
+ endDate: now,
328
+ }),
329
+ WorkerMetrics.aggregate({
330
+ workerName,
331
+ metricType: 'memory',
332
+ granularity: 'hourly',
333
+ startDate: oneHourAgo,
334
+ endDate: now,
335
+ }),
336
+ WorkerMetrics.aggregate({
337
+ workerName,
338
+ metricType: 'cpu',
339
+ granularity: 'hourly',
340
+ startDate: oneHourAgo,
341
+ endDate: now,
342
+ }),
343
+ ]);
344
+ const totalJobs = processed.total + errors.total;
345
+ const errorRate = totalJobs > 0 ? errors.total / totalJobs : 0;
346
+ const throughput = processed.total; // Jobs in last hour
347
+ const avgDuration = duration.average || 0;
348
+ const memoryUsage = memory.average || 0;
349
+ const cpuUsage = cpu.average || 0;
350
+ const healthData = calculateHealthScore({
351
+ errorRate,
352
+ throughput,
353
+ avgDuration,
354
+ memoryUsage,
355
+ cpuUsage,
356
+ });
357
+ const healthScore = {
358
+ workerName,
359
+ timestamp: now,
360
+ score: healthData.score,
361
+ factors: healthData.factors,
362
+ status: healthData.status,
363
+ };
364
+ // Store health score in sorted set (keep last 24 hours)
365
+ const key = getHealthKey(workerName);
366
+ const score = now.getTime();
367
+ const data = JSON.stringify(healthScore);
368
+ await redisClient.zadd(key, score, data);
369
+ // Keep only last 24 hours
370
+ const cutoff = now.getTime() - 24 * 60 * 60 * 1000;
371
+ await redisClient.zremrangebyscore(key, '-inf', cutoff);
372
+ // Set expiry (48 hours)
373
+ await redisClient.expire(key, 48 * 60 * 60);
374
+ Logger.debug(`Health score for ${workerName}: ${healthScore.score} (${healthScore.status})`);
375
+ return healthScore;
376
+ }
377
+ catch (error) {
378
+ Logger.error(`Error calculating health score for ${workerName}`, error);
379
+ throw error;
380
+ }
381
+ },
382
+ /**
383
+ * Get recent health scores
384
+ */
385
+ async getHealthHistory(workerName, hours = 24) {
386
+ if (!redisClient) {
387
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
388
+ }
389
+ try {
390
+ const key = getHealthKey(workerName);
391
+ const now = Date.now();
392
+ const startTime = now - hours * 60 * 60 * 1000;
393
+ const results = await redisClient.zrangebyscore(key, startTime, now);
394
+ return results.map((data) => JSON.parse(data));
395
+ }
396
+ catch (error) {
397
+ Logger.error(`Error retrieving health history for ${workerName}`, error);
398
+ return [];
399
+ }
400
+ },
401
+ /**
402
+ * Get latest health score
403
+ */
404
+ async getLatestHealth(workerName) {
405
+ if (!redisClient) {
406
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
407
+ }
408
+ try {
409
+ const key = getHealthKey(workerName);
410
+ // Get the most recent entry
411
+ const results = await redisClient.zrevrange(key, 0, 0);
412
+ if (results.length === 0) {
413
+ return null;
414
+ }
415
+ return JSON.parse(results[0]);
416
+ }
417
+ catch (error) {
418
+ Logger.error(`Error retrieving latest health for ${workerName}`, error);
419
+ return null;
420
+ }
421
+ },
422
+ /**
423
+ * Get metrics summary for all workers
424
+ */
425
+ async getAllWorkersSummary() {
426
+ if (!redisClient) {
427
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
428
+ }
429
+ try {
430
+ // Find all unique worker names from health keys
431
+ const pattern = `${HEALTH_PREFIX}*`;
432
+ const keys = await redisClient.keys(pattern);
433
+ const workerNames = keys.map((key) => key.replace(HEALTH_PREFIX, ''));
434
+ const summaries = await Promise.all(workerNames.map(async (workerName) => {
435
+ const now = new Date();
436
+ const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
437
+ const [health, processed, errors] = await Promise.all([
438
+ WorkerMetrics.getLatestHealth(workerName),
439
+ WorkerMetrics.aggregate({
440
+ workerName,
441
+ metricType: 'processed',
442
+ granularity: 'hourly',
443
+ startDate: oneHourAgo,
444
+ endDate: now,
445
+ }),
446
+ WorkerMetrics.aggregate({
447
+ workerName,
448
+ metricType: 'errors',
449
+ granularity: 'hourly',
450
+ startDate: oneHourAgo,
451
+ endDate: now,
452
+ }),
453
+ ]);
454
+ const totalJobs = processed.total + errors.total;
455
+ const errorRate = totalJobs > 0 ? errors.total / totalJobs : 0;
456
+ return {
457
+ workerName,
458
+ health,
459
+ metrics: {
460
+ processed: processed.total,
461
+ errors: errors.total,
462
+ errorRate,
463
+ },
464
+ };
465
+ }));
466
+ return summaries;
467
+ }
468
+ catch (error) {
469
+ Logger.error('Error retrieving all workers summary', error);
470
+ return [];
471
+ }
472
+ },
473
+ /**
474
+ * Delete all metrics for a worker
475
+ */
476
+ async deleteWorkerMetrics(workerName) {
477
+ if (!redisClient) {
478
+ throw ErrorFactory.createWorkerError('WorkerMetrics not initialized');
479
+ }
480
+ try {
481
+ const pattern = `${METRICS_PREFIX}${workerName}:*`;
482
+ const keys = await redisClient.keys(pattern);
483
+ if (keys.length > 0) {
484
+ await redisClient.del(...keys);
485
+ }
486
+ // Also delete health scores
487
+ const healthKey = getHealthKey(workerName);
488
+ await redisClient.del(healthKey);
489
+ Logger.info(`Deleted all metrics for worker "${workerName}"`);
490
+ }
491
+ catch (error) {
492
+ Logger.error(`Error deleting metrics for worker "${workerName}"`, error);
493
+ throw error;
494
+ }
495
+ },
496
+ /**
497
+ * Shutdown and disconnect
498
+ */
499
+ async shutdown() {
500
+ if (!redisClient) {
501
+ return;
502
+ }
503
+ Logger.info('WorkerMetrics shutting down...');
504
+ await redisClient.quit();
505
+ redisClient = null;
506
+ Logger.info('WorkerMetrics shutdown complete');
507
+ },
508
+ });
509
+ // Graceful shutdown handled by WorkerShutdown
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Worker Registry
3
+ * Central registry for all background workers with lifecycle management
4
+ * Sealed namespace for immutability
5
+ */
6
+ import { type WorkerConfig, type WorkerStatus } from '@zintrust/core';
7
+ export type WorkerMetadata = {
8
+ name: string;
9
+ status: WorkerStatus;
10
+ version: string;
11
+ region: string;
12
+ queueName: string;
13
+ concurrency: number;
14
+ startedAt: Date | null;
15
+ stoppedAt: Date | null;
16
+ lastProcessedAt: Date | null;
17
+ restartCount: number;
18
+ processedCount: number;
19
+ errorCount: number;
20
+ lockKey: string | null;
21
+ priority: number;
22
+ memoryUsage: number;
23
+ cpuUsage: number;
24
+ circuitState: 'closed' | 'open' | 'half-open';
25
+ queues: ReadonlyArray<string>;
26
+ plugins: ReadonlyArray<string>;
27
+ datacenter: string;
28
+ canaryPercentage: number;
29
+ config: Partial<WorkerConfig>;
30
+ };
31
+ export type WorkerInstance = {
32
+ metadata: WorkerMetadata;
33
+ instance: unknown;
34
+ start: () => void;
35
+ stop: () => Promise<void>;
36
+ drain: () => Promise<void>;
37
+ sleep: () => Promise<void>;
38
+ wakeup: () => void;
39
+ getStatus: () => WorkerStatus;
40
+ getHealth: () => 'green' | 'yellow' | 'red';
41
+ };
42
+ export type RegisterWorkerOptions = {
43
+ name: string;
44
+ config: Partial<WorkerConfig>;
45
+ version?: string;
46
+ region?: string;
47
+ queues?: ReadonlyArray<string>;
48
+ factory: () => Promise<WorkerInstance>;
49
+ };
50
+ export type WorkerRegistrySnapshot = {
51
+ timestamp: Date;
52
+ totalWorkers: number;
53
+ runningWorkers: number;
54
+ stoppedWorkers: number;
55
+ sleepingWorkers: number;
56
+ unhealthyWorkers: number;
57
+ workers: ReadonlyArray<{
58
+ name: string;
59
+ status: WorkerStatus;
60
+ health: 'green' | 'yellow' | 'red';
61
+ uptime: number | null;
62
+ processedCount: number;
63
+ errorCount: number;
64
+ }>;
65
+ };
66
+ /**
67
+ * Worker Registry - Sealed namespace
68
+ */
69
+ export declare const WorkerRegistry: Readonly<{
70
+ /**
71
+ * Register a worker with the registry
72
+ */
73
+ register(options: RegisterWorkerOptions): void;
74
+ /**
75
+ * Start a worker
76
+ */
77
+ start(name: string, version?: string): Promise<void>;
78
+ /**
79
+ * Stop a worker
80
+ */
81
+ stop(name: string): Promise<void>;
82
+ /**
83
+ * Restart a worker (stop + start)
84
+ */
85
+ restart(name: string): Promise<void>;
86
+ /**
87
+ * Sleep a worker (pause processing but keep lock)
88
+ */
89
+ sleep(name: string): Promise<void>;
90
+ /**
91
+ * Wakeup a worker (resume from sleep)
92
+ */
93
+ wakeup(name: string): Promise<void>;
94
+ /**
95
+ * Get worker status
96
+ */
97
+ status(name: string): WorkerMetadata | null;
98
+ /**
99
+ * List all registered workers
100
+ */
101
+ list(): ReadonlyArray<string>;
102
+ /**
103
+ * List all running workers
104
+ */
105
+ listRunning(): ReadonlyArray<string>;
106
+ /**
107
+ * Stop all running workers
108
+ */
109
+ stopAll(): Promise<void>;
110
+ /**
111
+ * Get worker metrics
112
+ */
113
+ getMetrics(name: string): Pick<WorkerMetadata, "processedCount" | "errorCount" | "memoryUsage" | "cpuUsage"> | null;
114
+ /**
115
+ * Get worker health status
116
+ */
117
+ getHealth(name: string): "green" | "yellow" | "red" | null;
118
+ /**
119
+ * Get registry snapshot
120
+ */
121
+ getSnapshot(): WorkerRegistrySnapshot;
122
+ /**
123
+ * Get worker topology (cluster view)
124
+ */
125
+ getTopology(): Record<string, {
126
+ workers: string[];
127
+ count: number;
128
+ }>;
129
+ /**
130
+ * Unregister a worker and clear its instance
131
+ */
132
+ unregister(name: string): void;
133
+ /**
134
+ * Check if worker is registered
135
+ */
136
+ isRegistered(name: string): boolean;
137
+ /**
138
+ * Check if worker is running
139
+ */
140
+ isRunning(name: string): boolean;
141
+ /**
142
+ * Get worker instance (internal use)
143
+ */
144
+ getInstance(name: string): WorkerInstance | null;
145
+ }>;