@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.
- package/README.md +861 -0
- package/dist/AnomalyDetection.d.ts +102 -0
- package/dist/AnomalyDetection.js +321 -0
- package/dist/AutoScaler.d.ts +127 -0
- package/dist/AutoScaler.js +425 -0
- package/dist/BroadcastWorker.d.ts +21 -0
- package/dist/BroadcastWorker.js +24 -0
- package/dist/CanaryController.d.ts +103 -0
- package/dist/CanaryController.js +380 -0
- package/dist/ChaosEngineering.d.ts +79 -0
- package/dist/ChaosEngineering.js +216 -0
- package/dist/CircuitBreaker.d.ts +106 -0
- package/dist/CircuitBreaker.js +374 -0
- package/dist/ClusterLock.d.ts +90 -0
- package/dist/ClusterLock.js +385 -0
- package/dist/ComplianceManager.d.ts +177 -0
- package/dist/ComplianceManager.js +556 -0
- package/dist/DatacenterOrchestrator.d.ts +133 -0
- package/dist/DatacenterOrchestrator.js +404 -0
- package/dist/DeadLetterQueue.d.ts +122 -0
- package/dist/DeadLetterQueue.js +539 -0
- package/dist/HealthMonitor.d.ts +42 -0
- package/dist/HealthMonitor.js +301 -0
- package/dist/MultiQueueWorker.d.ts +89 -0
- package/dist/MultiQueueWorker.js +277 -0
- package/dist/NotificationWorker.d.ts +21 -0
- package/dist/NotificationWorker.js +23 -0
- package/dist/Observability.d.ts +153 -0
- package/dist/Observability.js +530 -0
- package/dist/PluginManager.d.ts +123 -0
- package/dist/PluginManager.js +392 -0
- package/dist/PriorityQueue.d.ts +117 -0
- package/dist/PriorityQueue.js +244 -0
- package/dist/ResourceMonitor.d.ts +164 -0
- package/dist/ResourceMonitor.js +605 -0
- package/dist/SLAMonitor.d.ts +110 -0
- package/dist/SLAMonitor.js +274 -0
- package/dist/WorkerFactory.d.ts +193 -0
- package/dist/WorkerFactory.js +1507 -0
- package/dist/WorkerInit.d.ts +85 -0
- package/dist/WorkerInit.js +223 -0
- package/dist/WorkerMetrics.d.ts +114 -0
- package/dist/WorkerMetrics.js +509 -0
- package/dist/WorkerRegistry.d.ts +145 -0
- package/dist/WorkerRegistry.js +319 -0
- package/dist/WorkerShutdown.d.ts +61 -0
- package/dist/WorkerShutdown.js +159 -0
- package/dist/WorkerVersioning.d.ts +107 -0
- package/dist/WorkerVersioning.js +300 -0
- package/dist/build-manifest.json +462 -0
- package/dist/config/workerConfig.d.ts +3 -0
- package/dist/config/workerConfig.js +19 -0
- package/dist/createQueueWorker.d.ts +23 -0
- package/dist/createQueueWorker.js +113 -0
- package/dist/dashboard/index.d.ts +1 -0
- package/dist/dashboard/index.js +1 -0
- package/dist/dashboard/types.d.ts +117 -0
- package/dist/dashboard/types.js +1 -0
- package/dist/dashboard/workers-api.d.ts +4 -0
- package/dist/dashboard/workers-api.js +638 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +3 -0
- package/dist/dashboard/workers-dashboard-ui.js +1026 -0
- package/dist/dashboard/workers-dashboard.d.ts +4 -0
- package/dist/dashboard/workers-dashboard.js +904 -0
- package/dist/helper/index.d.ts +5 -0
- package/dist/helper/index.js +10 -0
- package/dist/http/WorkerApiController.d.ts +38 -0
- package/dist/http/WorkerApiController.js +312 -0
- package/dist/http/WorkerController.d.ts +374 -0
- package/dist/http/WorkerController.js +1351 -0
- package/dist/http/middleware/CustomValidation.d.ts +92 -0
- package/dist/http/middleware/CustomValidation.js +270 -0
- package/dist/http/middleware/DatacenterValidator.d.ts +3 -0
- package/dist/http/middleware/DatacenterValidator.js +94 -0
- package/dist/http/middleware/EditWorkerValidation.d.ts +7 -0
- package/dist/http/middleware/EditWorkerValidation.js +55 -0
- package/dist/http/middleware/FeaturesValidator.d.ts +3 -0
- package/dist/http/middleware/FeaturesValidator.js +60 -0
- package/dist/http/middleware/InfrastructureValidator.d.ts +31 -0
- package/dist/http/middleware/InfrastructureValidator.js +226 -0
- package/dist/http/middleware/OptionsValidator.d.ts +3 -0
- package/dist/http/middleware/OptionsValidator.js +112 -0
- package/dist/http/middleware/PayloadSanitizer.d.ts +7 -0
- package/dist/http/middleware/PayloadSanitizer.js +42 -0
- package/dist/http/middleware/ProcessorPathSanitizer.d.ts +3 -0
- package/dist/http/middleware/ProcessorPathSanitizer.js +74 -0
- package/dist/http/middleware/QueueNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/QueueNameSanitizer.js +45 -0
- package/dist/http/middleware/ValidateDriver.d.ts +7 -0
- package/dist/http/middleware/ValidateDriver.js +20 -0
- package/dist/http/middleware/VersionSanitizer.d.ts +3 -0
- package/dist/http/middleware/VersionSanitizer.js +25 -0
- package/dist/http/middleware/WorkerNameSanitizer.d.ts +3 -0
- package/dist/http/middleware/WorkerNameSanitizer.js +46 -0
- package/dist/http/middleware/WorkerValidationChain.d.ts +27 -0
- package/dist/http/middleware/WorkerValidationChain.js +185 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +48 -0
- package/dist/routes/workers.d.ts +12 -0
- package/dist/routes/workers.js +81 -0
- package/dist/storage/WorkerStore.d.ts +45 -0
- package/dist/storage/WorkerStore.js +195 -0
- package/dist/type.d.ts +76 -0
- package/dist/type.js +1 -0
- package/dist/ui/router/ui.d.ts +3 -0
- package/dist/ui/router/ui.js +83 -0
- package/dist/ui/types/worker-ui.d.ts +229 -0
- package/dist/ui/types/worker-ui.js +5 -0
- package/package.json +53 -0
- package/src/AnomalyDetection.ts +434 -0
- package/src/AutoScaler.ts +654 -0
- package/src/BroadcastWorker.ts +34 -0
- package/src/CanaryController.ts +531 -0
- package/src/ChaosEngineering.ts +301 -0
- package/src/CircuitBreaker.ts +495 -0
- package/src/ClusterLock.ts +499 -0
- package/src/ComplianceManager.ts +815 -0
- package/src/DatacenterOrchestrator.ts +561 -0
- package/src/DeadLetterQueue.ts +733 -0
- package/src/HealthMonitor.ts +390 -0
- package/src/MultiQueueWorker.ts +431 -0
- package/src/NotificationWorker.ts +33 -0
- package/src/Observability.ts +696 -0
- package/src/PluginManager.ts +551 -0
- package/src/PriorityQueue.ts +351 -0
- package/src/ResourceMonitor.ts +769 -0
- package/src/SLAMonitor.ts +408 -0
- package/src/WorkerFactory.ts +2108 -0
- package/src/WorkerInit.ts +313 -0
- package/src/WorkerMetrics.ts +709 -0
- package/src/WorkerRegistry.ts +443 -0
- package/src/WorkerShutdown.ts +210 -0
- package/src/WorkerVersioning.ts +422 -0
- package/src/config/workerConfig.ts +25 -0
- package/src/createQueueWorker.ts +174 -0
- package/src/dashboard/index.ts +6 -0
- package/src/dashboard/types.ts +141 -0
- package/src/dashboard/workers-api.ts +785 -0
- package/src/dashboard/zintrust.svg +30 -0
- package/src/helper/index.ts +11 -0
- package/src/http/WorkerApiController.ts +369 -0
- package/src/http/WorkerController.ts +1512 -0
- package/src/http/middleware/CustomValidation.ts +360 -0
- package/src/http/middleware/DatacenterValidator.ts +124 -0
- package/src/http/middleware/EditWorkerValidation.ts +74 -0
- package/src/http/middleware/FeaturesValidator.ts +82 -0
- package/src/http/middleware/InfrastructureValidator.ts +295 -0
- package/src/http/middleware/OptionsValidator.ts +144 -0
- package/src/http/middleware/PayloadSanitizer.ts +52 -0
- package/src/http/middleware/ProcessorPathSanitizer.ts +86 -0
- package/src/http/middleware/QueueNameSanitizer.ts +55 -0
- package/src/http/middleware/ValidateDriver.ts +29 -0
- package/src/http/middleware/VersionSanitizer.ts +30 -0
- package/src/http/middleware/WorkerNameSanitizer.ts +56 -0
- package/src/http/middleware/WorkerValidationChain.ts +230 -0
- package/src/index.ts +98 -0
- package/src/routes/workers.ts +154 -0
- package/src/storage/WorkerStore.ts +240 -0
- package/src/type.ts +89 -0
- package/src/types/queue-monitor.d.ts +38 -0
- package/src/types/queue-redis.d.ts +38 -0
- package/src/ui/README.md +13 -0
- package/src/ui/components/JsonEditor.js +670 -0
- package/src/ui/components/JsonViewer.js +387 -0
- package/src/ui/components/WorkerCard.js +178 -0
- package/src/ui/components/WorkerExpandPanel.js +257 -0
- package/src/ui/components/fetcher.js +42 -0
- package/src/ui/components/sla-scorecard.js +32 -0
- package/src/ui/components/styles.css +30 -0
- package/src/ui/components/table-expander.js +34 -0
- package/src/ui/integration/worker-ui-integration.js +565 -0
- package/src/ui/router/ui.ts +99 -0
- package/src/ui/services/workerApi.js +240 -0
- package/src/ui/types/worker-ui.ts +283 -0
- package/src/ui/utils/jsonValidator.js +444 -0
- package/src/ui/workers/index.html +202 -0
- package/src/ui/workers/main.js +1781 -0
- 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;
|