@cocreate/server-autoscaler 1.0.0
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/.github/FUNDING.yml +3 -0
- package/.github/workflows/automated.yml +56 -0
- package/CHANGELOG.md +8 -0
- package/CONTRIBUTING.md +96 -0
- package/CoCreate.config.js +24 -0
- package/LICENSE +683 -0
- package/README.md +91 -0
- package/demo/index.html +19 -0
- package/docs/index.html +228 -0
- package/package.json +39 -0
- package/prettier.config.js +16 -0
- package/release.config.js +30 -0
- package/src/index.js +503 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
********************************************************************************/
|
|
18
|
+
|
|
19
|
+
import cluster from 'node:cluster';
|
|
20
|
+
import https from 'node:https';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import Config from '@cocreate/config';
|
|
23
|
+
import { getValueFromObject } from '@cocreate/utils';
|
|
24
|
+
|
|
25
|
+
// Guard clause verification to ensure cluster API abstraction integrity
|
|
26
|
+
const isPrimary = cluster.isPrimary !== undefined ? cluster.isPrimary : cluster.isMaster;
|
|
27
|
+
|
|
28
|
+
// ==============================================================================
|
|
29
|
+
// Module-Level Sandbox State (ESM Singleton Container)
|
|
30
|
+
// ==============================================================================
|
|
31
|
+
let serverCtx = null; // Central reference to CoCreateServer Primary instance
|
|
32
|
+
let autoscaleConfig = null; // Configured thresholds and duration constants
|
|
33
|
+
let isRunning = false; // Execution loop flag
|
|
34
|
+
|
|
35
|
+
// Consecutive tracking counters to filter transient spikes
|
|
36
|
+
let consecutiveCpuSpikes = 0;
|
|
37
|
+
let consecutiveLowLoadTicks = 0;
|
|
38
|
+
|
|
39
|
+
// Timestamp tracking to prevent cascading scaling operations (Cooldown Management)
|
|
40
|
+
let lastScaleOperationTimestamp = 0;
|
|
41
|
+
|
|
42
|
+
// Default operational values for metrics-driven autoscale thresholds
|
|
43
|
+
const defaults = {
|
|
44
|
+
scaleUpCpuThreshold: 80, // CPU usage percentage trigger for Scaling Up (Sustained)
|
|
45
|
+
scaleUpCpuExtremeThreshold: 90, // Extreme CPU usage trigger for INSTANT Scaling Up (Single Flush)
|
|
46
|
+
scaleDownCpuThreshold: 25, // CPU usage percentage trigger for Scaling Down
|
|
47
|
+
scaleUpMemoryThreshold: 85, // Heap memory utilization trigger for Scaling Up
|
|
48
|
+
consecutiveCpuSpikesRequired: 3, // Require 3 consecutive flushes of high CPU (e.g. 3 minutes)
|
|
49
|
+
consecutiveLowTicksRequired: 10, // Require 10 consecutive flushes of low load (e.g. 10 minutes)
|
|
50
|
+
minClusterSize: 2, // Never scale down below this active node count
|
|
51
|
+
cooldownDurationMs: 300000, // Cooldown lockout period to stabilize nodes (default: 5 minutes)
|
|
52
|
+
provisioningApiEndpoint: 'https://api.cocreate.app/v1/infrastructure/scale'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes dataset arrays returned from standard CRUD database operations.
|
|
57
|
+
*/
|
|
58
|
+
function normalizePeersList(result) {
|
|
59
|
+
if (!result) return [];
|
|
60
|
+
if (Array.isArray(result)) return result;
|
|
61
|
+
if (Array.isArray(result.object)) return result.object;
|
|
62
|
+
if (Array.isArray(result.data)) return result.data;
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursively parses and evaluates an AST rule against a provided context.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} node - AST node definition
|
|
70
|
+
* @param {Object} context - Combined telemetry metrics and state counters
|
|
71
|
+
* @returns {boolean} Outcome of the evaluation statement
|
|
72
|
+
*/
|
|
73
|
+
function evaluateAST(node, context) {
|
|
74
|
+
if (!node) return false;
|
|
75
|
+
|
|
76
|
+
switch (node.type) {
|
|
77
|
+
case 'LogicalExpression': {
|
|
78
|
+
const leftVal = evaluateAST(node.left, context);
|
|
79
|
+
|
|
80
|
+
// Short-circuit evaluations to optimize V8 execution loops
|
|
81
|
+
if (node.operator === 'OR' && leftVal) return true;
|
|
82
|
+
if (node.operator === 'AND' && !leftVal) return false;
|
|
83
|
+
|
|
84
|
+
const rightVal = evaluateAST(node.right, context);
|
|
85
|
+
if (node.operator === 'OR') return leftVal || rightVal;
|
|
86
|
+
if (node.operator === 'AND') return leftVal && rightVal;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'Condition': {
|
|
91
|
+
const metricValue = getValueFromObject(context, node.metric);
|
|
92
|
+
const targetValue = node.value;
|
|
93
|
+
|
|
94
|
+
if (metricValue === undefined) {
|
|
95
|
+
console.warn(`[@cocreate/server-autoscaler] AST metric resolution failed for token: "${node.metric}"`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (node.operator) {
|
|
100
|
+
case 'GT': return metricValue > targetValue;
|
|
101
|
+
case 'GTE': return metricValue >= targetValue;
|
|
102
|
+
case 'LT': return metricValue < targetValue;
|
|
103
|
+
case 'LTE': return metricValue <= targetValue;
|
|
104
|
+
case 'EQ': return metricValue === targetValue;
|
|
105
|
+
case 'NEQ': return metricValue !== targetValue;
|
|
106
|
+
default: return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
console.warn(`[@cocreate/server-autoscaler] Unsupported AST node type detected: "${node.type}"`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Invokes an external HTTPS API call modeled around standard CoCreate AST payload shapes.
|
|
119
|
+
* If the Cloud Endpoint is offline or unreachable, falls back to a highly descriptive simulated log.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} direction - Target scaling direction ('up' | 'down')
|
|
122
|
+
* @param {Object} metrics - Performance statistics triggering the scale event
|
|
123
|
+
* @param {Object} actionConfig - Specific configuration parameters extracted from AST action node
|
|
124
|
+
*/
|
|
125
|
+
async function executeScaleAction(direction, metrics, actionConfig = {}) {
|
|
126
|
+
const targetUrl = getValueFromObject(autoscaleConfig, 'autoscale.apiEndpoint') || defaults.provisioningApiEndpoint;
|
|
127
|
+
const clusterSecret = process.env.CLUSTER_SECRET || 'LOCAL_SIMULATED_SECRET';
|
|
128
|
+
|
|
129
|
+
// Build standard AST-modeled API Payload Object
|
|
130
|
+
const payload = {
|
|
131
|
+
method: actionConfig.method || 'infrastructure.scale',
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
serverId: serverCtx?.id || 'simulated-server-id',
|
|
134
|
+
direction: direction,
|
|
135
|
+
provider: actionConfig.provider || 'round-robin',
|
|
136
|
+
spec: actionConfig.spec || 'standard',
|
|
137
|
+
region: actionConfig.region || 'local',
|
|
138
|
+
reason: {
|
|
139
|
+
avgCpu: metrics.cpu?.avg || metrics.cpu?.totalUsage || 0,
|
|
140
|
+
avgMemory: metrics.memory?.avg_used_pct || metrics.memory?.percentage || 0,
|
|
141
|
+
message: `Autoscale execution triggered dynamically by AST rule evaluation.`
|
|
142
|
+
},
|
|
143
|
+
token: clusterSecret
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// 1. Broadcast scaling details locally and over the P2P Server Mesh
|
|
147
|
+
if (serverCtx && typeof serverCtx.send === 'function') {
|
|
148
|
+
serverCtx.send({
|
|
149
|
+
method: 'infrastructure.scale',
|
|
150
|
+
broadcast: true,
|
|
151
|
+
payload: payload
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
serverCtx.send({
|
|
155
|
+
method: 'metric.scaleAlert',
|
|
156
|
+
broadcast: true,
|
|
157
|
+
direction,
|
|
158
|
+
metrics: payload.reason
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. Render highly visible console warning banners for local simulations
|
|
163
|
+
console.log(`\n=============================================================================`);
|
|
164
|
+
console.log(`[AUTOSCALER ALERT] DYNAMIC RESOURCE SCALE TRIGGERED`);
|
|
165
|
+
console.log(`=============================================================================`);
|
|
166
|
+
console.log(`* ACTION DIRECTION : ${direction.toUpperCase()}`);
|
|
167
|
+
console.log(`* TARGET REGION : ${payload.region.toUpperCase()}`);
|
|
168
|
+
console.log(`* SYSTEM PROFILE : ${payload.spec.toUpperCase()}`);
|
|
169
|
+
console.log(`* INSTANCE METRIC : CPU [${payload.reason.avgCpu}%], RAM [${payload.reason.avgMemory}%]`);
|
|
170
|
+
console.log(`* TRIGGER MESSAGE : ${payload.reason.message}`);
|
|
171
|
+
console.log(`=============================================================================`);
|
|
172
|
+
|
|
173
|
+
console.log(`[@cocreate/server-autoscaler] [ACTION] Dispatching AST scaling payload to endpoint: ${targetUrl}`);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const urlObj = new URL(targetUrl);
|
|
177
|
+
const options = {
|
|
178
|
+
hostname: urlObj.hostname,
|
|
179
|
+
port: urlObj.port || 443,
|
|
180
|
+
path: urlObj.pathname + urlObj.search,
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: {
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
'Content-Length': Buffer.byteLength(JSON.stringify(payload))
|
|
185
|
+
},
|
|
186
|
+
timeout: 5000 // Limit network stall times
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return await new Promise((resolve, reject) => {
|
|
190
|
+
const req = https.request(options, (res) => {
|
|
191
|
+
let body = '';
|
|
192
|
+
res.on('data', (chunk) => body += chunk);
|
|
193
|
+
res.on('end', () => {
|
|
194
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
195
|
+
console.log(`[@cocreate/server-autoscaler] Scaling action response accepted: HTTP ${res.statusCode}`);
|
|
196
|
+
resolve(JSON.parse(body));
|
|
197
|
+
} else {
|
|
198
|
+
reject(new Error(`Scaling HTTP request failed: Status ${res.statusCode}`));
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
req.on('error', reject);
|
|
204
|
+
req.write(JSON.stringify(payload));
|
|
205
|
+
req.end();
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.warn(`\n[@cocreate/server-autoscaler] [OFFLINE SIMULATOR] Cloud API Endpoint is unconfigured or unreachable (${err.message}).`);
|
|
209
|
+
console.warn(`[@cocreate/server-autoscaler] [OFFLINE SIMULATOR] Gracefully running in simulation mode. scaling action succeeded locally.`);
|
|
210
|
+
console.log(`=============================================================================\n`);
|
|
211
|
+
return { simulated: true, status: 'acknowledged', direction };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Calculates a dynamic safety memory threshold percentage based on the physical core-to-memory ratio.
|
|
217
|
+
* Ensures the system has enough physical MB of headroom to survive the boot provisioning delay.
|
|
218
|
+
*
|
|
219
|
+
* @returns {number} Calculated maximum safe memory percentage threshold (capped between 50% and 90%)
|
|
220
|
+
*/
|
|
221
|
+
export function calculateDynamicSafetyThreshold() {
|
|
222
|
+
const coresCount = os.cpus().length;
|
|
223
|
+
const totalMemoryMb = os.totalmem() / 1024 / 1024;
|
|
224
|
+
|
|
225
|
+
const allocationRatePerCoreMs = 15; // Estimated peak allocation speed in MB/sec under sustained load
|
|
226
|
+
const bootWindowSeconds = 60; // Boot delay window for new instances
|
|
227
|
+
|
|
228
|
+
const requiredHeadroomMb = coresCount * allocationRatePerCoreMs * bootWindowSeconds;
|
|
229
|
+
const headroomPercentage = (requiredHeadroomMb / totalMemoryMb) * 100;
|
|
230
|
+
|
|
231
|
+
const maxSafePercentage = Math.round(100 - headroomPercentage);
|
|
232
|
+
|
|
233
|
+
return Math.max(50, Math.min(90, maxSafePercentage));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Queries the shared servers directory database to ensure we can safely remove this node
|
|
238
|
+
* without bringing the remaining active nodes below our configured minimum bounds.
|
|
239
|
+
*
|
|
240
|
+
* @returns {boolean} Verified consensus state allowing scale-down
|
|
241
|
+
*/
|
|
242
|
+
async function isClusterSafeToScaleDown() {
|
|
243
|
+
if (!serverCtx?.crud) {
|
|
244
|
+
console.warn("[@cocreate/server-autoscaler] Database access layer is unavailable. Scaling down rejected.");
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const minSize = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.minClusterSize'), 10) || defaults.minClusterSize;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const readResponse = await serverCtx.crud.send({
|
|
252
|
+
method: 'object.read',
|
|
253
|
+
array: 'servers',
|
|
254
|
+
$filter: {
|
|
255
|
+
query: { status: 'active' },
|
|
256
|
+
limit: 0
|
|
257
|
+
},
|
|
258
|
+
organization_id: serverCtx.organization_id,
|
|
259
|
+
host: serverCtx.host
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const activeServers = normalizePeersList(readResponse);
|
|
263
|
+
|
|
264
|
+
if (activeServers.length <= minSize) {
|
|
265
|
+
console.log(`[@cocreate/server-autoscaler] [CONSENSUS] Scale-down rejected. Active nodes (${activeServers.length}) is already at or below the minimum cluster limit (${minSize}).`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(`[@cocreate/server-autoscaler] [CONSENSUS] Scale-down authorized. Cluster contains ${activeServers.length} active nodes (Safe margin: > ${minSize}).`);
|
|
270
|
+
return true;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error("[@cocreate/server-autoscaler] Failed to verify consensus safety from DB, canceling scale down:", err.message);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Intercepts high-priority 1-second instantaneous memory alerts and executes fast-track scaling.
|
|
279
|
+
*
|
|
280
|
+
* @param {Object} metricPayload - Real-time metric snapshot collected on the 1-second tick
|
|
281
|
+
*/
|
|
282
|
+
async function processEmergencyAlert(metricPayload) {
|
|
283
|
+
if (!isRunning) return;
|
|
284
|
+
|
|
285
|
+
const cooldownDuration = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.cooldownDurationMs'), 10) || defaults.cooldownDurationMs;
|
|
286
|
+
|
|
287
|
+
if (Date.now() - lastScaleOperationTimestamp < cooldownDuration) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const memoryPct = metricPayload.memory?.percentage || 0;
|
|
292
|
+
|
|
293
|
+
lastScaleOperationTimestamp = Date.now();
|
|
294
|
+
consecutiveCpuSpikes = 0;
|
|
295
|
+
consecutiveLowLoadTicks = 0;
|
|
296
|
+
|
|
297
|
+
console.warn(`[@cocreate/server-autoscaler] [EMERGENCY ALERT] Instantaneous OOM threat detected! Memory: ${memoryPct}%. Initiating immediate fast-track scale-up...`);
|
|
298
|
+
|
|
299
|
+
// Default action configuration for high-priority fast-track memory scale requests
|
|
300
|
+
const emergencyActionConfig = {
|
|
301
|
+
method: 'infrastructure.scale',
|
|
302
|
+
provider: 'round-robin',
|
|
303
|
+
spec: 'memory-optimized',
|
|
304
|
+
region: 'local'
|
|
305
|
+
};
|
|
306
|
+
await executeScaleAction('up', metricPayload, emergencyActionConfig);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Evaluates the pre-averaged flush packet directly against the threshold limits.
|
|
311
|
+
* Handles both classic properties and advanced declarative AST-configured orchestration rules.
|
|
312
|
+
*
|
|
313
|
+
* @param {Object} metricPayload - Normalized metrics data frame received on flush
|
|
314
|
+
*/
|
|
315
|
+
async function processMetricFlush(metricPayload) {
|
|
316
|
+
if (!isRunning) return;
|
|
317
|
+
|
|
318
|
+
const scaleUpCpu = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.scaleUpCpuThreshold'), 10) || defaults.scaleUpCpuThreshold;
|
|
319
|
+
const scaleUpCpuExtreme = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.scaleUpCpuExtremeThreshold'), 10) || defaults.scaleUpCpuExtremeThreshold;
|
|
320
|
+
const scaleDownCpu = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.scaleDownCpuThreshold'), 10) || defaults.scaleDownCpuThreshold;
|
|
321
|
+
const cooldownDuration = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.cooldownDurationMs'), 10) || defaults.cooldownDurationMs;
|
|
322
|
+
const spikesRequired = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.consecutiveCpuSpikesRequired'), 10) || defaults.consecutiveCpuSpikesRequired;
|
|
323
|
+
const lowTicksRequired = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.consecutiveLowTicksRequired'), 10) || defaults.consecutiveLowTicksRequired;
|
|
324
|
+
|
|
325
|
+
let targetMemoryThreshold = parseInt(getValueFromObject(autoscaleConfig, 'autoscale.scaleUpMemoryThreshold'), 10) || defaults.scaleUpMemoryThreshold;
|
|
326
|
+
|
|
327
|
+
const isSafetyLimitsEnabled = getValueFromObject(autoscaleConfig, 'autoscale.safetyLimits') !== false && getValueFromObject(autoscaleConfig, 'autoscale.safetyLimits') !== 'false';
|
|
328
|
+
|
|
329
|
+
if (isSafetyLimitsEnabled) {
|
|
330
|
+
const dynamicSafetyThreshold = calculateDynamicSafetyThreshold();
|
|
331
|
+
if (targetMemoryThreshold > dynamicSafetyThreshold) {
|
|
332
|
+
console.warn(`[@cocreate/server-autoscaler] [SAFETY] Configured memory threshold (${targetMemoryThreshold}%) exceeds calculated safety limit for this system context (${dynamicSafetyThreshold}%). Clamping to ${dynamicSafetyThreshold}% to guarantee headroom during provisioning.`);
|
|
333
|
+
targetMemoryThreshold = dynamicSafetyThreshold;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (Date.now() - lastScaleOperationTimestamp < cooldownDuration) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const cpuAvg = metricPayload.cpu?.avg || 0;
|
|
342
|
+
const memoryAvg = metricPayload.memory?.avg_used_pct || 0;
|
|
343
|
+
|
|
344
|
+
// Maintain and increment state accumulators
|
|
345
|
+
if (cpuAvg >= scaleUpCpu) {
|
|
346
|
+
consecutiveCpuSpikes++;
|
|
347
|
+
consecutiveLowLoadTicks = 0;
|
|
348
|
+
} else {
|
|
349
|
+
consecutiveCpuSpikes = 0;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const isUnderloaded = cpuAvg <= scaleDownCpu && memoryAvg < (targetMemoryThreshold * 0.6);
|
|
353
|
+
|
|
354
|
+
if (isUnderloaded) {
|
|
355
|
+
consecutiveLowLoadTicks++;
|
|
356
|
+
} else {
|
|
357
|
+
consecutiveLowLoadTicks = 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Build the consolidated evaluation context for the AST interpreter
|
|
361
|
+
const evaluationContext = {
|
|
362
|
+
...metricPayload,
|
|
363
|
+
consecutiveCpuSpikes,
|
|
364
|
+
consecutiveLowLoadTicks
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Retrieve potential AST orchestration rules from configuration
|
|
368
|
+
const customOrchestrationRule = getValueFromObject(autoscaleConfig, 'autoscale.orchestrationRule');
|
|
369
|
+
|
|
370
|
+
if (customOrchestrationRule) {
|
|
371
|
+
// --- DECLARATIVE AST EXECUTION TRACK ---
|
|
372
|
+
console.log(`[@cocreate/server-autoscaler] Evaluating custom declarative AST scaling rules...`);
|
|
373
|
+
|
|
374
|
+
const isTriggered = evaluateAST(customOrchestrationRule, evaluationContext);
|
|
375
|
+
|
|
376
|
+
if (isTriggered) {
|
|
377
|
+
const actionNode = customOrchestrationRule.action || {};
|
|
378
|
+
lastScaleOperationTimestamp = Date.now();
|
|
379
|
+
consecutiveCpuSpikes = 0;
|
|
380
|
+
consecutiveLowLoadTicks = 0;
|
|
381
|
+
|
|
382
|
+
console.warn(`[@cocreate/server-autoscaler] Custom AST evaluation resolved to TRUE. Triggering custom scaled action...`);
|
|
383
|
+
await executeScaleAction('up', metricPayload, actionNode);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
// --- TRADITIONAL DUAL-TRACK HARDCODED FALLBACK ---
|
|
388
|
+
const isMemoryPeaked = memoryAvg >= targetMemoryThreshold;
|
|
389
|
+
const isCpuExtremePeak = cpuAvg >= scaleUpCpuExtreme;
|
|
390
|
+
const isCpuSustainedPeak = consecutiveCpuSpikes >= spikesRequired;
|
|
391
|
+
|
|
392
|
+
if (isCpuExtremePeak || isCpuSustainedPeak || isMemoryPeaked) {
|
|
393
|
+
let triggerReason = '';
|
|
394
|
+
let targetSpec = 'standard';
|
|
395
|
+
|
|
396
|
+
if (isMemoryPeaked) {
|
|
397
|
+
triggerReason = `Incompressible memory threshold exceeded (${memoryAvg}% >= limit: ${targetMemoryThreshold}%)`;
|
|
398
|
+
targetSpec = 'memory-optimized';
|
|
399
|
+
} else if (isCpuExtremePeak) {
|
|
400
|
+
triggerReason = `Extreme CPU ceiling exceeded on a single flush (${cpuAvg}% >= limit: ${scaleUpCpuExtreme}%)`;
|
|
401
|
+
targetSpec = 'compute-optimized';
|
|
402
|
+
} else {
|
|
403
|
+
triggerReason = `Sustained CPU threshold exceeded for ${consecutiveCpuSpikes} consecutive cycles (${cpuAvg}% >= limit: ${scaleUpCpu}%)`;
|
|
404
|
+
targetSpec = 'compute-optimized';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
lastScaleOperationTimestamp = Date.now();
|
|
408
|
+
consecutiveCpuSpikes = 0;
|
|
409
|
+
|
|
410
|
+
console.warn(`[@cocreate/server-autoscaler] Peak limit hit! ${triggerReason}. Scaling up...`);
|
|
411
|
+
await executeScaleAction('up', metricPayload, { spec: targetSpec });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Scale Down evaluation track: Consolidated across both AST and traditional modes
|
|
417
|
+
if (consecutiveLowLoadTicks >= lowTicksRequired) {
|
|
418
|
+
console.log(`[@cocreate/server-autoscaler] Low load sustained for ${consecutiveLowLoadTicks} consecutive cycles. Checking cluster state to verify consensus...`);
|
|
419
|
+
|
|
420
|
+
const safeToScaleDown = await isClusterSafeToScaleDown();
|
|
421
|
+
if (safeToScaleDown) {
|
|
422
|
+
lastScaleOperationTimestamp = Date.now();
|
|
423
|
+
consecutiveLowLoadTicks = 0;
|
|
424
|
+
|
|
425
|
+
console.log(`[@cocreate/server-autoscaler] Safe-down consensus verified. Scaling down...`);
|
|
426
|
+
await executeScaleAction('down', metricPayload);
|
|
427
|
+
} else {
|
|
428
|
+
consecutiveLowLoadTicks = Math.round(lowTicksRequired / 2);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Boots the autoscaler system exclusively on the Master/Primary process context.
|
|
435
|
+
*
|
|
436
|
+
* @param {Object} server - Central CoCreateServer instance references
|
|
437
|
+
* @returns {Object} Active autoscale control API
|
|
438
|
+
*/
|
|
439
|
+
export async function init(server) {
|
|
440
|
+
if (!isPrimary) {
|
|
441
|
+
throw new Error("[@cocreate/server-autoscaler] [FATAL] Violation of scope: ServerAutoscaler can only run on the Primary (Master) process.");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
serverCtx = server;
|
|
445
|
+
|
|
446
|
+
autoscaleConfig = await Config({
|
|
447
|
+
'autoscale': ''
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const isEnabled = getValueFromObject(autoscaleConfig, 'autoscale.enabled') !== false && getValueFromObject(autoscaleConfig, 'autoscale.enabled') !== 'false';
|
|
451
|
+
|
|
452
|
+
if (!isEnabled) {
|
|
453
|
+
console.log('[@cocreate/server-autoscaler] ServerAutoscaler is disabled by configuration.');
|
|
454
|
+
return { init, stop };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
isRunning = true;
|
|
458
|
+
|
|
459
|
+
consecutiveCpuSpikes = 0;
|
|
460
|
+
consecutiveLowLoadTicks = 0;
|
|
461
|
+
|
|
462
|
+
serverCtx.on('metric', (payload) => {
|
|
463
|
+
if (payload && payload.data) {
|
|
464
|
+
processMetricFlush(payload.data).catch(err => {
|
|
465
|
+
console.error("[@cocreate/server-autoscaler] Error processing autoscale execution tick:", err);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
serverCtx.on('metric.emergency', (payload) => {
|
|
471
|
+
if (payload && payload.data) {
|
|
472
|
+
processEmergencyAlert(payload.data).catch(err => {
|
|
473
|
+
console.error("[@cocreate/server-autoscaler] Error processing emergency autoscale trigger:", err);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
console.log('[@cocreate/server-autoscaler] Autoscaler initialized and subscribed to telemetry feed.');
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
init,
|
|
482
|
+
stop,
|
|
483
|
+
processMetricFlush,
|
|
484
|
+
processEmergencyAlert,
|
|
485
|
+
executeScaleAction,
|
|
486
|
+
calculateDynamicSafetyThreshold
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Terminates monitoring sweeps and unsubscribes from the process emitter.
|
|
492
|
+
*/
|
|
493
|
+
export function stop() {
|
|
494
|
+
isRunning = false;
|
|
495
|
+
consecutiveCpuSpikes = 0;
|
|
496
|
+
consecutiveLowLoadTicks = 0;
|
|
497
|
+
console.log('[@cocreate/server-autoscaler] ServerAutoscaler monitoring stopped.');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export default {
|
|
501
|
+
init,
|
|
502
|
+
stop
|
|
503
|
+
};
|