@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,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canary Deployment Controller
|
|
3
|
+
* Gradual rollout with traffic percentage control and automatic rollback
|
|
4
|
+
* Sealed namespace for immutability
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ErrorFactory, Logger } from '@zintrust/core';
|
|
8
|
+
import { CircuitBreaker } from './CircuitBreaker';
|
|
9
|
+
|
|
10
|
+
export type CanaryStage =
|
|
11
|
+
| 'initial'
|
|
12
|
+
| 'ramping'
|
|
13
|
+
| 'monitoring'
|
|
14
|
+
| 'completed'
|
|
15
|
+
| 'rolling-back'
|
|
16
|
+
| 'failed';
|
|
17
|
+
|
|
18
|
+
export type CanaryDeploymentConfig = {
|
|
19
|
+
workerName: string;
|
|
20
|
+
currentVersion: string;
|
|
21
|
+
canaryVersion: string;
|
|
22
|
+
initialTrafficPercent: number; // Start with this percentage
|
|
23
|
+
targetTrafficPercent: number; // End with this percentage
|
|
24
|
+
incrementPercent: number; // Increase traffic by this amount each step
|
|
25
|
+
incrementInterval: number; // Wait this many seconds between increments
|
|
26
|
+
monitoringDuration: number; // Monitor for this many seconds at each step
|
|
27
|
+
errorThreshold: number; // Rollback if error rate exceeds this (0-1)
|
|
28
|
+
latencyThreshold: number; // Rollback if p95 latency exceeds this (ms)
|
|
29
|
+
minSuccessRate: number; // Rollback if success rate below this (0-1)
|
|
30
|
+
autoRollback: boolean; // Automatically rollback on failure
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type CanaryDeployment = {
|
|
34
|
+
config: CanaryDeploymentConfig;
|
|
35
|
+
currentTrafficPercent: number;
|
|
36
|
+
stage: CanaryStage;
|
|
37
|
+
startedAt: Date;
|
|
38
|
+
completedAt?: Date;
|
|
39
|
+
metrics: {
|
|
40
|
+
currentVersion: {
|
|
41
|
+
processed: number;
|
|
42
|
+
errors: number;
|
|
43
|
+
avgLatency: number;
|
|
44
|
+
};
|
|
45
|
+
canaryVersion: {
|
|
46
|
+
processed: number;
|
|
47
|
+
errors: number;
|
|
48
|
+
avgLatency: number;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
history: Array<{
|
|
52
|
+
timestamp: Date;
|
|
53
|
+
trafficPercent: number;
|
|
54
|
+
stage: CanaryStage;
|
|
55
|
+
metrics: CanaryDeployment['metrics'];
|
|
56
|
+
decision: string;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Internal state
|
|
61
|
+
const canaryDeployments = new Map<string, CanaryDeployment>();
|
|
62
|
+
const canaryTimers = new Map<string, NodeJS.Timeout>();
|
|
63
|
+
const MAX_HISTORY = 1000;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Helper: Calculate error rate
|
|
67
|
+
*/
|
|
68
|
+
const calculateErrorRate = (processed: number, errors: number): number => {
|
|
69
|
+
if (processed === 0) return 0;
|
|
70
|
+
return errors / processed;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Helper: Calculate success rate
|
|
75
|
+
*/
|
|
76
|
+
const calculateSuccessRate = (processed: number, errors: number): number => {
|
|
77
|
+
if (processed === 0) return 1;
|
|
78
|
+
return (processed - errors) / processed;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Helper: Should rollback based on metrics
|
|
83
|
+
*/
|
|
84
|
+
const shouldRollback = (deployment: CanaryDeployment): { should: boolean; reason?: string } => {
|
|
85
|
+
const { config, metrics } = deployment;
|
|
86
|
+
const { canaryVersion } = metrics;
|
|
87
|
+
|
|
88
|
+
// Check error threshold
|
|
89
|
+
const errorRate = calculateErrorRate(canaryVersion.processed, canaryVersion.errors);
|
|
90
|
+
if (errorRate > config.errorThreshold) {
|
|
91
|
+
return {
|
|
92
|
+
should: true,
|
|
93
|
+
reason: `Error rate ${(errorRate * 100).toFixed(2)}% exceeds threshold ${(config.errorThreshold * 100).toFixed(2)}%`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check success rate
|
|
98
|
+
const successRate = calculateSuccessRate(canaryVersion.processed, canaryVersion.errors);
|
|
99
|
+
if (successRate < config.minSuccessRate) {
|
|
100
|
+
return {
|
|
101
|
+
should: true,
|
|
102
|
+
reason: `Success rate ${(successRate * 100).toFixed(2)}% below minimum ${(config.minSuccessRate * 100).toFixed(2)}%`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check latency threshold
|
|
107
|
+
if (canaryVersion.avgLatency > config.latencyThreshold) {
|
|
108
|
+
return {
|
|
109
|
+
should: true,
|
|
110
|
+
reason: `P95 latency ${canaryVersion.avgLatency}ms exceeds threshold ${config.latencyThreshold}ms`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { should: false };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Helper: Increment traffic
|
|
119
|
+
*/
|
|
120
|
+
const incrementTraffic = (workerName: string): void => {
|
|
121
|
+
const deployment = canaryDeployments.get(workerName);
|
|
122
|
+
|
|
123
|
+
if (!deployment) {
|
|
124
|
+
Logger.error('Canary deployment not found', { workerName });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { config } = deployment;
|
|
129
|
+
|
|
130
|
+
// Check if we should rollback
|
|
131
|
+
const rollbackCheck = shouldRollback(deployment);
|
|
132
|
+
if (rollbackCheck.should && config.autoRollback) {
|
|
133
|
+
Logger.warn('Auto-rollback triggered', {
|
|
134
|
+
workerName,
|
|
135
|
+
reason: rollbackCheck.reason,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
CanaryController.rollback(workerName, rollbackCheck.reason ?? '');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Increment traffic
|
|
143
|
+
const newTrafficPercent = Math.min(
|
|
144
|
+
deployment.currentTrafficPercent + config.incrementPercent,
|
|
145
|
+
config.targetTrafficPercent
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
deployment.currentTrafficPercent = newTrafficPercent;
|
|
149
|
+
|
|
150
|
+
// Record history
|
|
151
|
+
appendHistory(deployment, {
|
|
152
|
+
timestamp: new Date(),
|
|
153
|
+
trafficPercent: newTrafficPercent,
|
|
154
|
+
stage: deployment.stage,
|
|
155
|
+
metrics: { ...deployment.metrics },
|
|
156
|
+
decision: `Traffic increased to ${newTrafficPercent}%`,
|
|
157
|
+
});
|
|
158
|
+
Logger.info('Canary traffic incremented', {
|
|
159
|
+
workerName,
|
|
160
|
+
trafficPercent: newTrafficPercent,
|
|
161
|
+
targetPercent: config.targetTrafficPercent,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Check if we've reached the target
|
|
165
|
+
if (newTrafficPercent >= config.targetTrafficPercent) {
|
|
166
|
+
deployment.stage = 'monitoring';
|
|
167
|
+
|
|
168
|
+
// Wait for final monitoring period
|
|
169
|
+
const existingCompleteTimer = canaryTimers.get(`${workerName}:complete`);
|
|
170
|
+
if (existingCompleteTimer) {
|
|
171
|
+
clearTimeout(existingCompleteTimer);
|
|
172
|
+
canaryTimers.delete(`${workerName}:complete`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
CanaryController.complete(workerName);
|
|
178
|
+
}, config.monitoringDuration * 1000);
|
|
179
|
+
|
|
180
|
+
canaryTimers.set(`${workerName}:complete`, timer);
|
|
181
|
+
} else {
|
|
182
|
+
// Schedule next increment
|
|
183
|
+
const existingTimer = canaryTimers.get(workerName);
|
|
184
|
+
if (existingTimer) {
|
|
185
|
+
clearTimeout(existingTimer);
|
|
186
|
+
canaryTimers.delete(workerName);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
incrementTraffic(workerName);
|
|
192
|
+
}, config.incrementInterval * 1000);
|
|
193
|
+
|
|
194
|
+
canaryTimers.set(workerName, timer);
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const appendHistory = (
|
|
199
|
+
deployment: CanaryDeployment,
|
|
200
|
+
entry: CanaryDeployment['history'][number]
|
|
201
|
+
): void => {
|
|
202
|
+
deployment.history.push(entry);
|
|
203
|
+
if (deployment.history.length > MAX_HISTORY) {
|
|
204
|
+
deployment.history.shift();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Canary Deployment Controller - Sealed namespace
|
|
210
|
+
*/
|
|
211
|
+
export const CanaryController = Object.freeze({
|
|
212
|
+
/**
|
|
213
|
+
* Start canary deployment
|
|
214
|
+
*/
|
|
215
|
+
start(config: CanaryDeploymentConfig): void {
|
|
216
|
+
const { workerName } = config;
|
|
217
|
+
|
|
218
|
+
if (canaryDeployments.has(workerName)) {
|
|
219
|
+
throw ErrorFactory.createGeneralError(
|
|
220
|
+
`Canary deployment already in progress for "${workerName}"`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Validate config
|
|
225
|
+
if (config.initialTrafficPercent < 0 || config.initialTrafficPercent > 100) {
|
|
226
|
+
throw ErrorFactory.createValidationError('Initial traffic percent must be between 0 and 100');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
config.targetTrafficPercent < config.initialTrafficPercent ||
|
|
231
|
+
config.targetTrafficPercent > 100
|
|
232
|
+
) {
|
|
233
|
+
throw ErrorFactory.createValidationError(
|
|
234
|
+
'Target traffic percent must be >= initial and <= 100'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Create deployment
|
|
239
|
+
const deployment: CanaryDeployment = {
|
|
240
|
+
config,
|
|
241
|
+
currentTrafficPercent: config.initialTrafficPercent,
|
|
242
|
+
stage: 'initial',
|
|
243
|
+
startedAt: new Date(),
|
|
244
|
+
metrics: {
|
|
245
|
+
currentVersion: { processed: 0, errors: 0, avgLatency: 0 },
|
|
246
|
+
canaryVersion: { processed: 0, errors: 0, avgLatency: 0 },
|
|
247
|
+
},
|
|
248
|
+
history: [],
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
canaryDeployments.set(workerName, deployment);
|
|
252
|
+
|
|
253
|
+
Logger.info('Canary deployment started', {
|
|
254
|
+
workerName,
|
|
255
|
+
currentVersion: config.currentVersion,
|
|
256
|
+
canaryVersion: config.canaryVersion,
|
|
257
|
+
initialTraffic: config.initialTrafficPercent,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Start ramping up
|
|
261
|
+
deployment.stage = 'ramping';
|
|
262
|
+
|
|
263
|
+
// Schedule first increment
|
|
264
|
+
const existingTimer = canaryTimers.get(workerName);
|
|
265
|
+
if (existingTimer) {
|
|
266
|
+
clearTimeout(existingTimer);
|
|
267
|
+
canaryTimers.delete(workerName);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
271
|
+
const timer = setTimeout(() => {
|
|
272
|
+
incrementTraffic(workerName);
|
|
273
|
+
}, config.monitoringDuration * 1000);
|
|
274
|
+
|
|
275
|
+
canaryTimers.set(workerName, timer);
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Pause canary deployment
|
|
280
|
+
*/
|
|
281
|
+
pause(workerName: string): void {
|
|
282
|
+
const deployment = canaryDeployments.get(workerName);
|
|
283
|
+
|
|
284
|
+
if (!deployment) {
|
|
285
|
+
throw ErrorFactory.createNotFoundError(`Canary deployment not found for "${workerName}"`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Clear timer
|
|
289
|
+
const timer = canaryTimers.get(workerName);
|
|
290
|
+
if (timer) {
|
|
291
|
+
clearTimeout(timer);
|
|
292
|
+
canaryTimers.delete(workerName);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
Logger.info('Canary deployment paused', { workerName });
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resume canary deployment
|
|
300
|
+
*/
|
|
301
|
+
resume(workerName: string): void {
|
|
302
|
+
const deployment = canaryDeployments.get(workerName);
|
|
303
|
+
|
|
304
|
+
if (!deployment) {
|
|
305
|
+
throw ErrorFactory.createNotFoundError(`Canary deployment not found for "${workerName}"`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Resume incrementing
|
|
309
|
+
const existingTimer = canaryTimers.get(workerName);
|
|
310
|
+
if (existingTimer) {
|
|
311
|
+
clearTimeout(existingTimer);
|
|
312
|
+
canaryTimers.delete(workerName);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
316
|
+
const timer = setTimeout(() => {
|
|
317
|
+
incrementTraffic(workerName);
|
|
318
|
+
}, deployment.config.incrementInterval * 1000);
|
|
319
|
+
|
|
320
|
+
canaryTimers.set(workerName, timer);
|
|
321
|
+
|
|
322
|
+
Logger.info('Canary deployment resumed', { workerName });
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Complete canary deployment
|
|
327
|
+
*/
|
|
328
|
+
complete(workerName: string): void {
|
|
329
|
+
const deployment = canaryDeployments.get(workerName);
|
|
330
|
+
|
|
331
|
+
if (!deployment) {
|
|
332
|
+
throw ErrorFactory.createNotFoundError(`Canary deployment not found for "${workerName}"`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
deployment.stage = 'completed';
|
|
336
|
+
deployment.completedAt = new Date();
|
|
337
|
+
|
|
338
|
+
// Clear timers
|
|
339
|
+
const timer = canaryTimers.get(workerName);
|
|
340
|
+
if (timer) {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
canaryTimers.delete(workerName);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const completeTimer = canaryTimers.get(`${workerName}:complete`);
|
|
346
|
+
if (completeTimer) {
|
|
347
|
+
clearTimeout(completeTimer);
|
|
348
|
+
canaryTimers.delete(`${workerName}:complete`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Record completion in history
|
|
352
|
+
appendHistory(deployment, {
|
|
353
|
+
timestamp: new Date(),
|
|
354
|
+
trafficPercent: deployment.currentTrafficPercent,
|
|
355
|
+
stage: 'completed',
|
|
356
|
+
metrics: { ...deployment.metrics },
|
|
357
|
+
decision: 'Deployment completed successfully',
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
Logger.info('Canary deployment completed', {
|
|
361
|
+
workerName,
|
|
362
|
+
duration: deployment.completedAt.getTime() - deployment.startedAt.getTime(),
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Rollback canary deployment
|
|
368
|
+
*/
|
|
369
|
+
rollback(workerName: string, reason: string): void {
|
|
370
|
+
const deployment = canaryDeployments.get(workerName);
|
|
371
|
+
|
|
372
|
+
if (!deployment) {
|
|
373
|
+
throw ErrorFactory.createNotFoundError(`Canary deployment not found for "${workerName}"`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
deployment.stage = 'rolling-back';
|
|
377
|
+
|
|
378
|
+
// Clear timers
|
|
379
|
+
const timer = canaryTimers.get(workerName);
|
|
380
|
+
if (timer) {
|
|
381
|
+
clearTimeout(timer);
|
|
382
|
+
canaryTimers.delete(workerName);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Roll back traffic to 0%
|
|
386
|
+
deployment.currentTrafficPercent = 0;
|
|
387
|
+
|
|
388
|
+
// Record rollback in history
|
|
389
|
+
appendHistory(deployment, {
|
|
390
|
+
timestamp: new Date(),
|
|
391
|
+
trafficPercent: 0,
|
|
392
|
+
stage: 'rolling-back',
|
|
393
|
+
metrics: { ...deployment.metrics },
|
|
394
|
+
decision: `Rollback initiated: ${reason}`,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
deployment.stage = 'failed';
|
|
398
|
+
deployment.completedAt = new Date();
|
|
399
|
+
|
|
400
|
+
Logger.error('Canary deployment rolled back', { workerName, reason });
|
|
401
|
+
|
|
402
|
+
// Optional: Open circuit breaker for canary version
|
|
403
|
+
CircuitBreaker.forceOpen(workerName, deployment.config.canaryVersion, reason);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get canary deployment status
|
|
408
|
+
*/
|
|
409
|
+
getStatus(workerName: string): CanaryDeployment | null {
|
|
410
|
+
const deployment = canaryDeployments.get(workerName);
|
|
411
|
+
return deployment ? { ...deployment } : null;
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Update metrics for canary deployment
|
|
416
|
+
*/
|
|
417
|
+
updateMetrics(
|
|
418
|
+
workerName: string,
|
|
419
|
+
version: string,
|
|
420
|
+
processed: number,
|
|
421
|
+
errors: number,
|
|
422
|
+
avgLatency: number
|
|
423
|
+
): void {
|
|
424
|
+
const deployment = canaryDeployments.get(workerName);
|
|
425
|
+
|
|
426
|
+
if (!deployment) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (version === deployment.config.currentVersion) {
|
|
431
|
+
deployment.metrics.currentVersion = { processed, errors, avgLatency };
|
|
432
|
+
} else if (version === deployment.config.canaryVersion) {
|
|
433
|
+
deployment.metrics.canaryVersion = { processed, errors, avgLatency };
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Route job to version based on traffic percentage
|
|
439
|
+
*/
|
|
440
|
+
routeJob(workerName: string): string | null {
|
|
441
|
+
const deployment = canaryDeployments.get(workerName);
|
|
442
|
+
|
|
443
|
+
if (!deployment || deployment.stage === 'completed' || deployment.stage === 'failed') {
|
|
444
|
+
return null; // No active canary
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Random routing based on traffic percentage
|
|
448
|
+
const random = Math.random() * 100; // NOSONAR
|
|
449
|
+
|
|
450
|
+
if (random < deployment.currentTrafficPercent) {
|
|
451
|
+
return deployment.config.canaryVersion;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return deployment.config.currentVersion;
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* List all canary deployments
|
|
459
|
+
*/
|
|
460
|
+
listDeployments(): string[] {
|
|
461
|
+
return Array.from(canaryDeployments.keys());
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get deployment history
|
|
466
|
+
*/
|
|
467
|
+
getHistory(workerName: string): CanaryDeployment['history'] | null {
|
|
468
|
+
const deployment = canaryDeployments.get(workerName);
|
|
469
|
+
return deployment ? [...deployment.history] : null;
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Remove completed/failed deployment
|
|
474
|
+
*/
|
|
475
|
+
remove(workerName: string): void {
|
|
476
|
+
const deployment = canaryDeployments.get(workerName);
|
|
477
|
+
|
|
478
|
+
if (!deployment) {
|
|
479
|
+
throw ErrorFactory.createNotFoundError(`Canary deployment not found for "${workerName}"`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (deployment.stage !== 'completed' && deployment.stage !== 'failed') {
|
|
483
|
+
throw ErrorFactory.createValidationError(
|
|
484
|
+
'Cannot remove active deployment. Pause or complete it first.'
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
canaryDeployments.delete(workerName);
|
|
489
|
+
|
|
490
|
+
Logger.info('Canary deployment removed', { workerName });
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Purge deployment data (force cleanup)
|
|
495
|
+
*/
|
|
496
|
+
purge(workerName: string): void {
|
|
497
|
+
const timer = canaryTimers.get(workerName);
|
|
498
|
+
if (timer) {
|
|
499
|
+
clearTimeout(timer);
|
|
500
|
+
canaryTimers.delete(workerName);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const completionTimer = canaryTimers.get(`${workerName}:complete`);
|
|
504
|
+
if (completionTimer) {
|
|
505
|
+
clearTimeout(completionTimer);
|
|
506
|
+
canaryTimers.delete(`${workerName}:complete`);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
canaryDeployments.delete(workerName);
|
|
510
|
+
Logger.info('Canary deployment purged', { workerName });
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Shutdown all canary deployments
|
|
515
|
+
*/
|
|
516
|
+
shutdown(): void {
|
|
517
|
+
Logger.info('CanaryController shutting down...');
|
|
518
|
+
|
|
519
|
+
// Clear all timers
|
|
520
|
+
for (const timer of canaryTimers.values()) {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
}
|
|
523
|
+
canaryTimers.clear();
|
|
524
|
+
|
|
525
|
+
canaryDeployments.clear();
|
|
526
|
+
|
|
527
|
+
Logger.info('CanaryController shutdown complete');
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Graceful shutdown handled by WorkerShutdown
|