@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,654 @@
1
+ /**
2
+ * Worker Auto-Scaler
3
+ * Automatic worker scaling based on queue depth, resource usage, and cost optimization
4
+ * Sealed namespace for immutability
5
+ */
6
+
7
+ import type { WorkerConfig } from '@zintrust/core';
8
+ import { ErrorFactory, Logger, workersConfig } from '@zintrust/core';
9
+
10
+ export type ScalingDecision = {
11
+ workerName: string;
12
+ action: 'scale-up' | 'scale-down' | 'no-change';
13
+ currentConcurrency: number;
14
+ targetConcurrency: number;
15
+ reason: string;
16
+ metrics: {
17
+ queueDepth: number;
18
+ avgProcessingTime: number;
19
+ cpuUsage: number;
20
+ memoryUsage: number;
21
+ errorRate: number;
22
+ costPerHour: number;
23
+ };
24
+ timestamp: Date;
25
+ };
26
+
27
+ export type ScalingPolicy = {
28
+ minConcurrency: number;
29
+ maxConcurrency: number;
30
+ scaleUpThreshold: {
31
+ queueDepth: number;
32
+ cpuUsage: number;
33
+ memoryUsage: number;
34
+ };
35
+ scaleDownThreshold: {
36
+ queueDepth: number;
37
+ cpuUsage: number;
38
+ memoryUsage: number;
39
+ };
40
+ cooldownPeriod: number; // seconds
41
+ aggressiveness: 'conservative' | 'moderate' | 'aggressive';
42
+ };
43
+
44
+ export type CostOptimizationStrategy = {
45
+ enabled: boolean;
46
+ maxCostPerHour: number;
47
+ preferSpotInstances: boolean;
48
+ offPeakSchedule?: {
49
+ start: string; // HH:MM format
50
+ end: string; // HH:MM format
51
+ timezone: string;
52
+ reductionPercentage: number; // 0-100
53
+ };
54
+ budgetAlerts: {
55
+ dailyLimit: number;
56
+ weeklyLimit: number;
57
+ monthlyLimit: number;
58
+ };
59
+ };
60
+
61
+ export type AutoScalerConfig = {
62
+ enabled: boolean;
63
+ checkInterval: number; // seconds
64
+ scalingPolicies: Map<string, ScalingPolicy>;
65
+ costOptimization: CostOptimizationStrategy;
66
+ };
67
+
68
+ // Internal state
69
+ let config: AutoScalerConfig | null = null;
70
+ let scalingInterval: NodeJS.Timeout | null = null;
71
+ const lastScalingDecisions = new Map<string, ScalingDecision>();
72
+ const scalingHistory = new Map<string, ScalingDecision[]>();
73
+
74
+ // Cost tracking
75
+ let currentHourlyCost = 0;
76
+ let dailyCost = 0;
77
+ let weeklyCost = 0;
78
+ let monthlyCost = 0;
79
+ const lastCostReset = {
80
+ daily: new Date(),
81
+ weekly: new Date(),
82
+ monthly: new Date(),
83
+ };
84
+
85
+ /**
86
+ * Helper: Reset cost counters if period has passed
87
+ */
88
+ const resetCostCountersIfNeeded = (): void => {
89
+ const now = new Date();
90
+
91
+ // Daily reset (midnight UTC)
92
+ const lastDailyReset = new Date(lastCostReset.daily);
93
+ lastDailyReset.setUTCHours(0, 0, 0, 0);
94
+ const todayMidnight = new Date(now);
95
+ todayMidnight.setUTCHours(0, 0, 0, 0);
96
+
97
+ if (todayMidnight > lastDailyReset) {
98
+ dailyCost = 0;
99
+ lastCostReset.daily = now;
100
+ Logger.info('Daily cost counter reset');
101
+ }
102
+
103
+ // Weekly reset (Sunday midnight UTC)
104
+ const dayOfWeek = now.getUTCDay();
105
+ const lastWeeklyReset = new Date(lastCostReset.weekly);
106
+ const daysSinceLastReset = Math.floor(
107
+ (now.getTime() - lastWeeklyReset.getTime()) / (24 * 60 * 60 * 1000)
108
+ );
109
+
110
+ if (daysSinceLastReset >= 7 || (dayOfWeek === 0 && now.getUTCHours() === 0)) {
111
+ weeklyCost = 0;
112
+ lastCostReset.weekly = now;
113
+ Logger.info('Weekly cost counter reset');
114
+ }
115
+
116
+ // Monthly reset (1st of month midnight UTC)
117
+ if (now.getUTCDate() === 1 && now.getUTCDate() !== lastCostReset.monthly.getUTCDate()) {
118
+ monthlyCost = 0;
119
+ lastCostReset.monthly = now;
120
+ Logger.info('Monthly cost counter reset');
121
+ }
122
+ };
123
+
124
+ /**
125
+ * Helper: Check if in off-peak period
126
+ */
127
+ const isOffPeakPeriod = (schedule?: CostOptimizationStrategy['offPeakSchedule']): boolean => {
128
+ if (!schedule) return false;
129
+
130
+ const now = new Date();
131
+ const timeStr = now.toLocaleTimeString('en-US', {
132
+ timeZone: schedule.timezone,
133
+ hour12: false,
134
+ hour: '2-digit',
135
+ minute: '2-digit',
136
+ });
137
+
138
+ const [currentHour, currentMinute] = timeStr.split(':').map(Number);
139
+ const currentMinutes = currentHour * 60 + currentMinute;
140
+
141
+ const [startHour, startMinute] = schedule.start.split(':').map(Number);
142
+ const startMinutes = startHour * 60 + startMinute;
143
+
144
+ const [endHour, endMinute] = schedule.end.split(':').map(Number);
145
+ const endMinutes = endHour * 60 + endMinute;
146
+
147
+ // Handle cases where period crosses midnight
148
+ if (startMinutes > endMinutes) {
149
+ return currentMinutes >= startMinutes || currentMinutes < endMinutes;
150
+ }
151
+
152
+ return currentMinutes >= startMinutes && currentMinutes < endMinutes;
153
+ };
154
+
155
+ /**
156
+ * Helper: Calculate scaling step based on aggressiveness
157
+ */
158
+ const calculateScalingStep = (
159
+ currentConcurrency: number,
160
+ aggressiveness: ScalingPolicy['aggressiveness']
161
+ ): number => {
162
+ const baseStep = Math.max(1, Math.ceil(currentConcurrency * 0.1)); // 10% of current
163
+
164
+ switch (aggressiveness) {
165
+ case 'conservative':
166
+ return Math.max(1, Math.ceil(baseStep * 0.5)); // 5% increase
167
+ case 'moderate':
168
+ return baseStep; // 10% increase
169
+ case 'aggressive':
170
+ return Math.ceil(baseStep * 2); // 20% increase
171
+ }
172
+ };
173
+
174
+ /**
175
+ * Helper: Check if cooldown period has passed
176
+ */
177
+ const canScale = (workerName: string, cooldownPeriod: number): boolean => {
178
+ const lastDecision = lastScalingDecisions.get(workerName);
179
+
180
+ if (!lastDecision || lastDecision.action === 'no-change') {
181
+ return true;
182
+ }
183
+
184
+ const elapsedSeconds = (Date.now() - lastDecision.timestamp.getTime()) / 1000;
185
+ return elapsedSeconds >= cooldownPeriod;
186
+ };
187
+
188
+ /**
189
+ * Helper: Check budget constraints
190
+ */
191
+ const checkBudgetConstraints = (additionalCost: number): { allowed: boolean; reason?: string } => {
192
+ if (config?.costOptimization?.enabled === undefined) {
193
+ return { allowed: true };
194
+ }
195
+
196
+ resetCostCountersIfNeeded();
197
+
198
+ const { budgetAlerts } = config.costOptimization;
199
+
200
+ // Check daily limit
201
+ if (dailyCost + additionalCost > budgetAlerts.dailyLimit) {
202
+ return {
203
+ allowed: false,
204
+ reason: `Would exceed daily budget: $${(dailyCost + additionalCost).toFixed(2)} > $${budgetAlerts.dailyLimit}`,
205
+ };
206
+ }
207
+
208
+ // Check weekly limit
209
+ if (weeklyCost + additionalCost > budgetAlerts.weeklyLimit) {
210
+ return {
211
+ allowed: false,
212
+ reason: `Would exceed weekly budget: $${(weeklyCost + additionalCost).toFixed(2)} > $${budgetAlerts.weeklyLimit}`,
213
+ };
214
+ }
215
+
216
+ // Check monthly limit
217
+ if (monthlyCost + additionalCost > budgetAlerts.monthlyLimit) {
218
+ return {
219
+ allowed: false,
220
+ reason: `Would exceed monthly budget: $${(monthlyCost + additionalCost).toFixed(2)} > $${budgetAlerts.monthlyLimit}`,
221
+ };
222
+ }
223
+
224
+ return { allowed: true };
225
+ };
226
+
227
+ /**
228
+ * Helper: Make scaling decision for a worker
229
+ */
230
+ const buildDecision = (
231
+ workerName: string,
232
+ action: ScalingDecision['action'],
233
+ currentConcurrency: number,
234
+ targetConcurrency: number,
235
+ reason: string,
236
+ metrics: ScalingDecision['metrics']
237
+ ): ScalingDecision => ({
238
+ workerName,
239
+ action,
240
+ currentConcurrency,
241
+ targetConcurrency,
242
+ reason,
243
+ metrics,
244
+ timestamp: new Date(),
245
+ });
246
+
247
+ const getOffPeakDecision = (
248
+ workerName: string,
249
+ policy: ScalingPolicy,
250
+ currentConcurrency: number,
251
+ metrics: ScalingDecision['metrics']
252
+ ): ScalingDecision | null => {
253
+ if (config?.costOptimization.enabled === undefined) return null;
254
+
255
+ const schedule = config.costOptimization.offPeakSchedule;
256
+ if (!schedule || !isOffPeakPeriod(schedule)) return null;
257
+
258
+ const reductionPercentage = schedule.reductionPercentage;
259
+ const targetConcurrency = Math.max(
260
+ policy.minConcurrency,
261
+ Math.ceil(currentConcurrency * (1 - reductionPercentage / 100))
262
+ );
263
+
264
+ if (targetConcurrency >= currentConcurrency) return null;
265
+
266
+ return buildDecision(
267
+ workerName,
268
+ 'scale-down',
269
+ currentConcurrency,
270
+ targetConcurrency,
271
+ `Off-peak reduction: ${reductionPercentage}%`,
272
+ metrics
273
+ );
274
+ };
275
+
276
+ const getScaleUpDecision = (
277
+ workerName: string,
278
+ policy: ScalingPolicy,
279
+ currentConcurrency: number,
280
+ metrics: ScalingDecision['metrics']
281
+ ): ScalingDecision | null => {
282
+ const shouldScaleUp =
283
+ metrics.queueDepth > policy.scaleUpThreshold.queueDepth ||
284
+ metrics.cpuUsage > policy.scaleUpThreshold.cpuUsage ||
285
+ metrics.memoryUsage > policy.scaleUpThreshold.memoryUsage;
286
+
287
+ if (!shouldScaleUp || currentConcurrency >= policy.maxConcurrency) return null;
288
+
289
+ const step = calculateScalingStep(currentConcurrency, policy.aggressiveness);
290
+ const targetConcurrency = Math.min(policy.maxConcurrency, currentConcurrency + step);
291
+ const additionalCost = metrics.costPerHour * (targetConcurrency - currentConcurrency);
292
+ const budgetCheck = checkBudgetConstraints(additionalCost);
293
+
294
+ if (!budgetCheck.allowed) {
295
+ return buildDecision(
296
+ workerName,
297
+ 'no-change',
298
+ currentConcurrency,
299
+ currentConcurrency,
300
+ budgetCheck.reason ?? 'Budget constraints prevent scale-up',
301
+ metrics
302
+ );
303
+ }
304
+
305
+ const reasons: string[] = [];
306
+ if (metrics.queueDepth > policy.scaleUpThreshold.queueDepth) {
307
+ reasons.push(`Queue depth: ${metrics.queueDepth} > ${policy.scaleUpThreshold.queueDepth}`);
308
+ }
309
+ if (metrics.cpuUsage > policy.scaleUpThreshold.cpuUsage) {
310
+ reasons.push(`CPU usage: ${metrics.cpuUsage}% > ${policy.scaleUpThreshold.cpuUsage}%`);
311
+ }
312
+ if (metrics.memoryUsage > policy.scaleUpThreshold.memoryUsage) {
313
+ reasons.push(`Memory usage: ${metrics.memoryUsage}% > ${policy.scaleUpThreshold.memoryUsage}%`);
314
+ }
315
+
316
+ return buildDecision(
317
+ workerName,
318
+ 'scale-up',
319
+ currentConcurrency,
320
+ targetConcurrency,
321
+ reasons.join('; '),
322
+ metrics
323
+ );
324
+ };
325
+
326
+ const getScaleDownDecision = (
327
+ workerName: string,
328
+ policy: ScalingPolicy,
329
+ currentConcurrency: number,
330
+ metrics: ScalingDecision['metrics']
331
+ ): ScalingDecision | null => {
332
+ const shouldScaleDown =
333
+ metrics.queueDepth < policy.scaleDownThreshold.queueDepth &&
334
+ metrics.cpuUsage < policy.scaleDownThreshold.cpuUsage &&
335
+ metrics.memoryUsage < policy.scaleDownThreshold.memoryUsage;
336
+
337
+ if (!shouldScaleDown || currentConcurrency <= policy.minConcurrency) return null;
338
+
339
+ const step = calculateScalingStep(currentConcurrency, policy.aggressiveness);
340
+ const targetConcurrency = Math.max(policy.minConcurrency, currentConcurrency - step);
341
+
342
+ return buildDecision(
343
+ workerName,
344
+ 'scale-down',
345
+ currentConcurrency,
346
+ targetConcurrency,
347
+ `Low utilization: Queue=${metrics.queueDepth}, CPU=${metrics.cpuUsage}%, Mem=${metrics.memoryUsage}%`,
348
+ metrics
349
+ );
350
+ };
351
+
352
+ const makeScalingDecision = (
353
+ workerName: string,
354
+ workerConfig: Partial<WorkerConfig>,
355
+ metrics: ScalingDecision['metrics']
356
+ ): ScalingDecision => {
357
+ if (!config) {
358
+ throw ErrorFactory.createGeneralError('AutoScaler not configured');
359
+ }
360
+
361
+ const policy = config.scalingPolicies.get(workerName) ?? getDefaultScalingPolicy(workerConfig);
362
+ const currentConcurrency = workerConfig.concurrency ?? 1;
363
+
364
+ if (!canScale(workerName, policy.cooldownPeriod)) {
365
+ return buildDecision(
366
+ workerName,
367
+ 'no-change',
368
+ currentConcurrency,
369
+ currentConcurrency,
370
+ 'Cooldown period not elapsed',
371
+ metrics
372
+ );
373
+ }
374
+
375
+ const offPeakDecision = getOffPeakDecision(workerName, policy, currentConcurrency, metrics);
376
+ if (offPeakDecision) return offPeakDecision;
377
+
378
+ const scaleUpDecision = getScaleUpDecision(workerName, policy, currentConcurrency, metrics);
379
+ if (scaleUpDecision) return scaleUpDecision;
380
+
381
+ const scaleDownDecision = getScaleDownDecision(workerName, policy, currentConcurrency, metrics);
382
+ if (scaleDownDecision) return scaleDownDecision;
383
+
384
+ return buildDecision(
385
+ workerName,
386
+ 'no-change',
387
+ currentConcurrency,
388
+ currentConcurrency,
389
+ 'Metrics within acceptable range',
390
+ metrics
391
+ );
392
+ };
393
+
394
+ /**
395
+ * Helper: Get default scaling policy from worker config
396
+ */
397
+ const getDefaultScalingPolicy = (workerConfig: Partial<WorkerConfig>): ScalingPolicy => {
398
+ const autoScaling = workerConfig.autoScaling;
399
+
400
+ return {
401
+ minConcurrency: autoScaling?.minConcurrency ?? 1,
402
+ maxConcurrency: autoScaling?.maxConcurrency ?? 10,
403
+ scaleUpThreshold: {
404
+ queueDepth: autoScaling?.scaleUpThreshold ?? 100,
405
+ cpuUsage: 70,
406
+ memoryUsage: 80,
407
+ },
408
+ scaleDownThreshold: {
409
+ queueDepth: autoScaling?.scaleDownThreshold ?? 10,
410
+ cpuUsage: 30,
411
+ memoryUsage: 40,
412
+ },
413
+ cooldownPeriod: autoScaling?.cooldownPeriod ?? 300, // 5 minutes
414
+ aggressiveness: 'moderate',
415
+ };
416
+ };
417
+
418
+ /**
419
+ * Helper: Record scaling decision
420
+ */
421
+ const recordScalingDecision = (decision: ScalingDecision): void => {
422
+ lastScalingDecisions.set(decision.workerName, decision);
423
+
424
+ // Add to history
425
+ let history = scalingHistory.get(decision.workerName);
426
+ if (!history) {
427
+ history = [];
428
+ scalingHistory.set(decision.workerName, history);
429
+ }
430
+
431
+ history.push(decision);
432
+
433
+ // Keep only last 1000 decisions
434
+ if (history.length > 1000) {
435
+ history.shift();
436
+ }
437
+
438
+ // Update cost tracking
439
+ if (decision.action === 'scale-up') {
440
+ const additionalCost =
441
+ decision.metrics.costPerHour * (decision.targetConcurrency - decision.currentConcurrency);
442
+ currentHourlyCost += additionalCost;
443
+ dailyCost += additionalCost;
444
+ weeklyCost += additionalCost;
445
+ monthlyCost += additionalCost;
446
+ }
447
+ };
448
+
449
+ /**
450
+ * Worker Auto-Scaler - Sealed namespace
451
+ */
452
+ export const AutoScaler = Object.freeze({
453
+ /**
454
+ * Initialize auto-scaler with configuration
455
+ */
456
+ initialize(autoScalerConfig: AutoScalerConfig): void {
457
+ if (config) {
458
+ Logger.warn('AutoScaler already initialized');
459
+ return;
460
+ }
461
+
462
+ config = autoScalerConfig;
463
+
464
+ if (config.enabled) {
465
+ AutoScaler.start();
466
+ }
467
+
468
+ Logger.info('AutoScaler initialized', { enabled: config.enabled });
469
+ },
470
+
471
+ /**
472
+ * Start auto-scaling checks
473
+ */
474
+ start(): void {
475
+ if (!config) {
476
+ throw ErrorFactory.createConfigError('AutoScaler not initialized');
477
+ }
478
+
479
+ if (scalingInterval) {
480
+ Logger.warn('AutoScaler already running');
481
+ return;
482
+ }
483
+
484
+ if (!config.enabled) {
485
+ Logger.warn('AutoScaler is disabled in config');
486
+ return;
487
+ }
488
+
489
+ scalingInterval = setInterval(() => {
490
+ // Scaling checks will be triggered externally via evaluate()
491
+ // This interval is just a keepalive
492
+ }, config.checkInterval * 1000);
493
+
494
+ Logger.info('AutoScaler started', { checkInterval: config.checkInterval });
495
+ },
496
+
497
+ /**
498
+ * Stop auto-scaling checks
499
+ */
500
+ stop(): void {
501
+ if (scalingInterval) {
502
+ clearInterval(scalingInterval);
503
+ scalingInterval = null;
504
+ Logger.info('AutoScaler stopped');
505
+ }
506
+ },
507
+
508
+ /**
509
+ * Evaluate scaling decision for a worker
510
+ */
511
+ evaluate(workerName: string, metrics: ScalingDecision['metrics']): ScalingDecision {
512
+ if (!config) {
513
+ throw ErrorFactory.createConfigError('AutoScaler not initialized');
514
+ }
515
+
516
+ const workerConfig: Partial<WorkerConfig> = workersConfig.defaultWorker;
517
+
518
+ const decision = makeScalingDecision(workerName, workerConfig, metrics);
519
+ recordScalingDecision(decision);
520
+
521
+ if (decision.action !== 'no-change') {
522
+ Logger.info(`Scaling decision for ${workerName}`, {
523
+ action: decision.action,
524
+ from: decision.currentConcurrency,
525
+ to: decision.targetConcurrency,
526
+ reason: decision.reason,
527
+ });
528
+ }
529
+
530
+ return decision;
531
+ },
532
+
533
+ /**
534
+ * Get last scaling decision
535
+ */
536
+ getLastDecision(workerName: string): ScalingDecision | null {
537
+ return lastScalingDecisions.get(workerName) ?? null;
538
+ },
539
+
540
+ /**
541
+ * Get scaling history
542
+ */
543
+ getHistory(workerName: string, limit = 100): ReadonlyArray<ScalingDecision> {
544
+ const history = scalingHistory.get(workerName) ?? [];
545
+ return history.slice(-limit);
546
+ },
547
+
548
+ /**
549
+ * Clear scaling history for a worker
550
+ */
551
+ clearHistory(workerName: string): void {
552
+ lastScalingDecisions.delete(workerName);
553
+ scalingHistory.delete(workerName);
554
+ Logger.info(`Cleared auto-scaling history for ${workerName}`);
555
+ },
556
+
557
+ /**
558
+ * Get cost summary
559
+ */
560
+ getCostSummary(): {
561
+ currentHourlyCost: number;
562
+ dailyCost: number;
563
+ weeklyCost: number;
564
+ monthlyCost: number;
565
+ budgetLimits: CostOptimizationStrategy['budgetAlerts'];
566
+ utilizationPercentage: {
567
+ daily: number;
568
+ weekly: number;
569
+ monthly: number;
570
+ };
571
+ } {
572
+ if (config?.costOptimization.enabled === undefined) {
573
+ return {
574
+ currentHourlyCost: 0,
575
+ dailyCost: 0,
576
+ weeklyCost: 0,
577
+ monthlyCost: 0,
578
+ budgetLimits: { dailyLimit: 0, weeklyLimit: 0, monthlyLimit: 0 },
579
+ utilizationPercentage: { daily: 0, weekly: 0, monthly: 0 },
580
+ };
581
+ }
582
+
583
+ resetCostCountersIfNeeded();
584
+
585
+ const { budgetAlerts } = config.costOptimization;
586
+
587
+ return {
588
+ currentHourlyCost,
589
+ dailyCost,
590
+ weeklyCost,
591
+ monthlyCost,
592
+ budgetLimits: budgetAlerts,
593
+ utilizationPercentage: {
594
+ daily: (dailyCost / budgetAlerts.dailyLimit) * 100,
595
+ weekly: (weeklyCost / budgetAlerts.weeklyLimit) * 100,
596
+ monthly: (monthlyCost / budgetAlerts.monthlyLimit) * 100,
597
+ },
598
+ };
599
+ },
600
+
601
+ /**
602
+ * Set scaling policy for a worker
603
+ */
604
+ setScalingPolicy(workerName: string, policy: ScalingPolicy): void {
605
+ if (!config) {
606
+ throw ErrorFactory.createWorkerError('AutoScaler not initialized');
607
+ }
608
+
609
+ config.scalingPolicies.set(workerName, policy);
610
+ Logger.info(`Updated scaling policy for ${workerName}`);
611
+ },
612
+
613
+ /**
614
+ * Get scaling policy for a worker
615
+ */
616
+ getScalingPolicy(workerName: string): ScalingPolicy | null {
617
+ if (!config) {
618
+ return null;
619
+ }
620
+
621
+ return config.scalingPolicies.get(workerName) ?? null;
622
+ },
623
+
624
+ /**
625
+ * Check if currently in off-peak period
626
+ */
627
+ isOffPeak(): boolean {
628
+ if (config?.costOptimization.enabled === undefined) {
629
+ return false;
630
+ }
631
+
632
+ return isOffPeakPeriod(config.costOptimization.offPeakSchedule);
633
+ },
634
+
635
+ /**
636
+ * Get configuration
637
+ */
638
+ getConfig(): AutoScalerConfig | null {
639
+ return config ? { ...config } : null;
640
+ },
641
+
642
+ /**
643
+ * Shutdown
644
+ */
645
+ shutdown(): void {
646
+ AutoScaler.stop();
647
+ config = null;
648
+ lastScalingDecisions.clear();
649
+ scalingHistory.clear();
650
+ Logger.info('AutoScaler shutdown complete');
651
+ },
652
+ });
653
+
654
+ // Graceful shutdown handled by WorkerShutdown
@@ -0,0 +1,34 @@
1
+ /**
2
+ * BroadcastWorker - Processes queued broadcasts
3
+ *
4
+ * This worker dequeues broadcast messages and sends them using the Broadcast service.
5
+ * Use with Queue.dequeue() in a background process or cron job.
6
+ */
7
+
8
+ import { Broadcast } from '@zintrust/core';
9
+ import { createQueueWorker } from './createQueueWorker';
10
+
11
+ type BroadcastJob = {
12
+ channel: string;
13
+ event: string;
14
+ data: unknown;
15
+ timestamp: number;
16
+ };
17
+
18
+ export const BroadcastWorker = Object.freeze({
19
+ ...createQueueWorker<BroadcastJob>({
20
+ kindLabel: 'broadcast',
21
+ defaultQueueName: 'broadcasts',
22
+ maxAttempts: 3,
23
+ getLogFields: (payload) => ({
24
+ channel: payload.channel,
25
+ event: payload.event,
26
+ queuedAt: payload.timestamp,
27
+ }),
28
+ handle: async (payload) => {
29
+ await Broadcast.send(payload.channel, payload.event, payload.data);
30
+ },
31
+ }),
32
+ });
33
+
34
+ export default BroadcastWorker;