@forgezero/agent 0.1.113 → 0.1.115
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/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +39 -1
- package/dist/capacity-calibration.d.ts +4 -0
- package/dist/capacity-calibration.js +7 -4
- package/dist/definition.js +9 -4
- package/dist/deploy-compiler.js +75 -1
- package/dist/deploy-file.js +9 -4
- package/dist/deploy-plan-runner.js +75 -1
- package/dist/deploy-plan.js +75 -1
- package/dist/deploy.d.ts +14 -0
- package/dist/fz-agent.js +83 -10
- package/dist/fz.js +53 -5
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +49 -3
- package/dist/platform-fleet-verification.js +10 -5
- package/dist/provision.js +10 -5
- package/dist/software-helper.js +9 -4
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.js
CHANGED
|
@@ -1460,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1460
1460
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1461
1461
|
|
|
1462
1462
|
// src/version.ts
|
|
1463
|
-
var VERSION = "0.1.
|
|
1463
|
+
var VERSION = "0.1.115";
|
|
1464
1464
|
|
|
1465
1465
|
// src/software.ts
|
|
1466
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -1504,6 +1504,44 @@ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
|
|
|
1504
1504
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1505
1505
|
import { dirname as dirname3, join as join2, resolve as resolve3, sep } from "path";
|
|
1506
1506
|
|
|
1507
|
+
// src/capacity-calibration.ts
|
|
1508
|
+
function localCalibrationEndpoint(value) {
|
|
1509
|
+
const endpoint2 = new URL(value);
|
|
1510
|
+
if (endpoint2.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint2.hostname) || !endpoint2.port || endpoint2.username || endpoint2.password || endpoint2.hash) {
|
|
1511
|
+
throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
|
|
1512
|
+
}
|
|
1513
|
+
return endpoint2;
|
|
1514
|
+
}
|
|
1515
|
+
function validateCapacityCalibrationOptions(options) {
|
|
1516
|
+
const endpoint2 = localCalibrationEndpoint(options.endpoint);
|
|
1517
|
+
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
1518
|
+
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
1519
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
1520
|
+
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
1521
|
+
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
1522
|
+
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
1523
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
1524
|
+
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
1525
|
+
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
1526
|
+
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
1527
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
1528
|
+
throw new Error("Capacity calibration bounds are invalid.");
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
endpoint: endpoint2,
|
|
1532
|
+
maxConcurrency,
|
|
1533
|
+
requestsPerWorker,
|
|
1534
|
+
maximumRequestsPerWorker,
|
|
1535
|
+
maxP95Ms,
|
|
1536
|
+
maxErrorRate,
|
|
1537
|
+
safetyRatio,
|
|
1538
|
+
requestTimeoutMs,
|
|
1539
|
+
minimumStageDurationMs,
|
|
1540
|
+
maxCpuUtilizationPercent,
|
|
1541
|
+
maxMemoryUtilizationPercent
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1507
1545
|
// src/definition.ts
|
|
1508
1546
|
var RESERVED_STEP_ENV = new Set([
|
|
1509
1547
|
"PATH",
|
|
@@ -39,7 +39,10 @@ export interface CapacityCalibration {
|
|
|
39
39
|
export interface CapacityCalibrationOptions {
|
|
40
40
|
endpoint: string;
|
|
41
41
|
maxConcurrency?: number;
|
|
42
|
+
/** Minimum samples per worker before a stage may finish. */
|
|
42
43
|
requestsPerWorker?: number;
|
|
44
|
+
/** Hard request budget per worker. Defaults to 100 and can never exceed 100. */
|
|
45
|
+
maximumRequestsPerWorker?: number;
|
|
43
46
|
maxP95Ms?: number;
|
|
44
47
|
maxErrorRate?: number;
|
|
45
48
|
/** Defaults to 0.8: use at most 80% of measured sustainable RPS. */
|
|
@@ -53,6 +56,7 @@ export interface ValidatedCapacityCalibrationOptions {
|
|
|
53
56
|
endpoint: URL;
|
|
54
57
|
maxConcurrency: number;
|
|
55
58
|
requestsPerWorker: number;
|
|
59
|
+
maximumRequestsPerWorker: number;
|
|
56
60
|
maxP95Ms: number;
|
|
57
61
|
maxErrorRate: number;
|
|
58
62
|
safetyRatio: number;
|
|
@@ -27,6 +27,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
27
27
|
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
28
28
|
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
29
29
|
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
30
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
30
31
|
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
31
32
|
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
32
33
|
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
@@ -34,13 +35,14 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
34
35
|
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
35
36
|
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
36
37
|
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
37
|
-
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
38
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
38
39
|
throw new Error("Capacity calibration bounds are invalid.");
|
|
39
40
|
}
|
|
40
41
|
return {
|
|
41
42
|
endpoint,
|
|
42
43
|
maxConcurrency,
|
|
43
44
|
requestsPerWorker,
|
|
45
|
+
maximumRequestsPerWorker,
|
|
44
46
|
maxP95Ms,
|
|
45
47
|
maxErrorRate,
|
|
46
48
|
safetyRatio,
|
|
@@ -50,7 +52,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
50
52
|
maxMemoryUtilizationPercent
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
55
|
+
async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
54
56
|
const latencies = [];
|
|
55
57
|
let succeeded = 0;
|
|
56
58
|
let failed = 0;
|
|
@@ -59,7 +61,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
|
|
|
59
61
|
const started = performance.now();
|
|
60
62
|
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
61
63
|
let request = 0;
|
|
62
|
-
while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
|
|
64
|
+
while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
|
|
63
65
|
request += 1;
|
|
64
66
|
const requestStarted = performance.now();
|
|
65
67
|
try {
|
|
@@ -118,6 +120,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
118
120
|
safetyRatio,
|
|
119
121
|
requestTimeoutMs: timeoutMs,
|
|
120
122
|
minimumStageDurationMs,
|
|
123
|
+
maximumRequestsPerWorker,
|
|
121
124
|
maxCpuUtilizationPercent,
|
|
122
125
|
maxMemoryUtilizationPercent
|
|
123
126
|
} = validateCapacityCalibrationOptions(options);
|
|
@@ -125,7 +128,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
125
128
|
let lastSafe;
|
|
126
129
|
let stopReason = "maximum-tested";
|
|
127
130
|
for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
|
|
128
|
-
const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
131
|
+
const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
129
132
|
stages.push(measured);
|
|
130
133
|
const previous = stages.at(-2);
|
|
131
134
|
const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
|
package/dist/definition.js
CHANGED
|
@@ -572,6 +572,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
572
572
|
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
573
573
|
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
574
574
|
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
575
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
575
576
|
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
576
577
|
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
577
578
|
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
@@ -579,13 +580,14 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
579
580
|
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
580
581
|
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
581
582
|
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
582
|
-
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
583
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
583
584
|
throw new Error("Capacity calibration bounds are invalid.");
|
|
584
585
|
}
|
|
585
586
|
return {
|
|
586
587
|
endpoint,
|
|
587
588
|
maxConcurrency,
|
|
588
589
|
requestsPerWorker,
|
|
590
|
+
maximumRequestsPerWorker,
|
|
589
591
|
maxP95Ms,
|
|
590
592
|
maxErrorRate,
|
|
591
593
|
safetyRatio,
|
|
@@ -595,7 +597,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
595
597
|
maxMemoryUtilizationPercent
|
|
596
598
|
};
|
|
597
599
|
}
|
|
598
|
-
async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
600
|
+
async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
599
601
|
const latencies = [];
|
|
600
602
|
let succeeded = 0;
|
|
601
603
|
let failed = 0;
|
|
@@ -604,7 +606,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
|
|
|
604
606
|
const started = performance.now();
|
|
605
607
|
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
606
608
|
let request = 0;
|
|
607
|
-
while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
|
|
609
|
+
while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
|
|
608
610
|
request += 1;
|
|
609
611
|
const requestStarted = performance.now();
|
|
610
612
|
try {
|
|
@@ -663,6 +665,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
663
665
|
safetyRatio,
|
|
664
666
|
requestTimeoutMs: timeoutMs,
|
|
665
667
|
minimumStageDurationMs,
|
|
668
|
+
maximumRequestsPerWorker,
|
|
666
669
|
maxCpuUtilizationPercent,
|
|
667
670
|
maxMemoryUtilizationPercent
|
|
668
671
|
} = validateCapacityCalibrationOptions(options);
|
|
@@ -670,7 +673,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
670
673
|
let lastSafe;
|
|
671
674
|
let stopReason = "maximum-tested";
|
|
672
675
|
for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
|
|
673
|
-
const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
676
|
+
const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
674
677
|
stages.push(measured);
|
|
675
678
|
const previous = stages.at(-2);
|
|
676
679
|
const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
|
|
@@ -771,6 +774,7 @@ function capacityCalibration(value, where) {
|
|
|
771
774
|
"endpoint",
|
|
772
775
|
"maxConcurrency",
|
|
773
776
|
"requestsPerWorker",
|
|
777
|
+
"maximumRequestsPerWorker",
|
|
774
778
|
"maxP95Ms",
|
|
775
779
|
"maxErrorRate",
|
|
776
780
|
"safetyRatio",
|
|
@@ -799,6 +803,7 @@ function capacityCalibration(value, where) {
|
|
|
799
803
|
...Object.fromEntries([
|
|
800
804
|
"maxConcurrency",
|
|
801
805
|
"requestsPerWorker",
|
|
806
|
+
"maximumRequestsPerWorker",
|
|
802
807
|
"maxP95Ms",
|
|
803
808
|
"maxErrorRate",
|
|
804
809
|
"safetyRatio",
|
package/dist/deploy-compiler.js
CHANGED
|
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
132
132
|
throw new Error(`${where} ArangoDB config is invalid`);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// src/capacity-calibration.ts
|
|
136
|
+
function localCalibrationEndpoint(value) {
|
|
137
|
+
const endpoint = new URL(value);
|
|
138
|
+
if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
|
|
139
|
+
throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
|
|
140
|
+
}
|
|
141
|
+
return endpoint;
|
|
142
|
+
}
|
|
143
|
+
function validateCapacityCalibrationOptions(options) {
|
|
144
|
+
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
145
|
+
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
146
|
+
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
147
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
148
|
+
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
149
|
+
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
150
|
+
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
151
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
152
|
+
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
153
|
+
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
154
|
+
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
155
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
156
|
+
throw new Error("Capacity calibration bounds are invalid.");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
endpoint,
|
|
160
|
+
maxConcurrency,
|
|
161
|
+
requestsPerWorker,
|
|
162
|
+
maximumRequestsPerWorker,
|
|
163
|
+
maxP95Ms,
|
|
164
|
+
maxErrorRate,
|
|
165
|
+
safetyRatio,
|
|
166
|
+
requestTimeoutMs,
|
|
167
|
+
minimumStageDurationMs,
|
|
168
|
+
maxCpuUtilizationPercent,
|
|
169
|
+
maxMemoryUtilizationPercent
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
// src/deploy-plan.ts
|
|
136
174
|
var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
|
|
137
175
|
var DEPLOY_PLAN_VERSION = 1;
|
|
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
447
485
|
const row2 = object(value, where);
|
|
448
486
|
const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
|
|
449
487
|
if (row2.kind === "application") {
|
|
450
|
-
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
488
|
+
exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
|
|
451
489
|
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
452
490
|
const runtimeRequirement = requirements.get(String(runtime.requirement));
|
|
453
491
|
if (!runtimeRequirement)
|
|
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
529
567
|
fail(`${where}.service.websocket`, "must be a boolean.");
|
|
530
568
|
if (service.maximumConnections !== undefined)
|
|
531
569
|
integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
|
|
570
|
+
if (row2.capacityCalibration !== undefined) {
|
|
571
|
+
const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
|
|
572
|
+
exact3(calibration, [
|
|
573
|
+
"path",
|
|
574
|
+
"maxConcurrency",
|
|
575
|
+
"requestsPerWorker",
|
|
576
|
+
"maximumRequestsPerWorker",
|
|
577
|
+
"maxP95Ms",
|
|
578
|
+
"maxErrorRate",
|
|
579
|
+
"safetyRatio",
|
|
580
|
+
"requestTimeoutMs",
|
|
581
|
+
"minimumStageDurationMs",
|
|
582
|
+
"maxCpuUtilizationPercent",
|
|
583
|
+
"maxMemoryUtilizationPercent"
|
|
584
|
+
], `${where}.capacityCalibration`);
|
|
585
|
+
if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
|
|
586
|
+
fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
validateCapacityCalibrationOptions({
|
|
590
|
+
...calibration,
|
|
591
|
+
endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
|
|
592
|
+
});
|
|
593
|
+
} catch {
|
|
594
|
+
fail(`${where}.capacityCalibration`, "contains invalid bounds.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
532
597
|
validateResources(row2.resources, `${where}.resources`);
|
|
533
598
|
if (runtime.kind === "container") {
|
|
534
599
|
const resources = object(row2.resources, `${where}.resources`);
|
|
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
|
|
|
991
1056
|
named(componentName, "deployment.spec.components key");
|
|
992
1057
|
validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
|
|
993
1058
|
}
|
|
1059
|
+
for (const targetName of targetNames) {
|
|
1060
|
+
const calibrated = componentEntries.filter(([, component2]) => {
|
|
1061
|
+
const candidate = component2;
|
|
1062
|
+
return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
|
|
1063
|
+
});
|
|
1064
|
+
if (calibrated.length > 1) {
|
|
1065
|
+
fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
994
1068
|
for (const [targetName, target] of targetDefinitions) {
|
|
995
1069
|
if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
|
|
996
1070
|
continue;
|
package/dist/deploy-file.js
CHANGED
|
@@ -572,6 +572,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
572
572
|
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
573
573
|
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
574
574
|
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
575
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
575
576
|
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
576
577
|
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
577
578
|
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
@@ -579,13 +580,14 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
579
580
|
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
580
581
|
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
581
582
|
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
582
|
-
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
583
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
583
584
|
throw new Error("Capacity calibration bounds are invalid.");
|
|
584
585
|
}
|
|
585
586
|
return {
|
|
586
587
|
endpoint,
|
|
587
588
|
maxConcurrency,
|
|
588
589
|
requestsPerWorker,
|
|
590
|
+
maximumRequestsPerWorker,
|
|
589
591
|
maxP95Ms,
|
|
590
592
|
maxErrorRate,
|
|
591
593
|
safetyRatio,
|
|
@@ -595,7 +597,7 @@ function validateCapacityCalibrationOptions(options) {
|
|
|
595
597
|
maxMemoryUtilizationPercent
|
|
596
598
|
};
|
|
597
599
|
}
|
|
598
|
-
async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
600
|
+
async function stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumDurationMs, fetcher, sampler) {
|
|
599
601
|
const latencies = [];
|
|
600
602
|
let succeeded = 0;
|
|
601
603
|
let failed = 0;
|
|
@@ -604,7 +606,7 @@ async function stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimu
|
|
|
604
606
|
const started = performance.now();
|
|
605
607
|
await Promise.all(Array.from({ length: concurrency }, async () => {
|
|
606
608
|
let request = 0;
|
|
607
|
-
while (request < requestsPerWorker || performance.now() - started < minimumDurationMs) {
|
|
609
|
+
while (request < maximumRequestsPerWorker && (request < requestsPerWorker || performance.now() - started < minimumDurationMs)) {
|
|
608
610
|
request += 1;
|
|
609
611
|
const requestStarted = performance.now();
|
|
610
612
|
try {
|
|
@@ -663,6 +665,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
663
665
|
safetyRatio,
|
|
664
666
|
requestTimeoutMs: timeoutMs,
|
|
665
667
|
minimumStageDurationMs,
|
|
668
|
+
maximumRequestsPerWorker,
|
|
666
669
|
maxCpuUtilizationPercent,
|
|
667
670
|
maxMemoryUtilizationPercent
|
|
668
671
|
} = validateCapacityCalibrationOptions(options);
|
|
@@ -670,7 +673,7 @@ async function calibrateHttpConcurrency(options, fetcher = fetch, sampler = host
|
|
|
670
673
|
let lastSafe;
|
|
671
674
|
let stopReason = "maximum-tested";
|
|
672
675
|
for (let concurrency = 1;; concurrency = Math.min(maxConcurrency, concurrency * 2)) {
|
|
673
|
-
const measured = await stage(endpoint, concurrency, requestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
676
|
+
const measured = await stage(endpoint, concurrency, requestsPerWorker, maximumRequestsPerWorker, timeoutMs, minimumStageDurationMs, fetcher, sampler);
|
|
674
677
|
stages.push(measured);
|
|
675
678
|
const previous = stages.at(-2);
|
|
676
679
|
const throughputRegressed = Boolean(previous && concurrency > 1 && measured.successfulRequestsPerSecond < previous.successfulRequestsPerSecond * 0.9);
|
|
@@ -771,6 +774,7 @@ function capacityCalibration(value, where) {
|
|
|
771
774
|
"endpoint",
|
|
772
775
|
"maxConcurrency",
|
|
773
776
|
"requestsPerWorker",
|
|
777
|
+
"maximumRequestsPerWorker",
|
|
774
778
|
"maxP95Ms",
|
|
775
779
|
"maxErrorRate",
|
|
776
780
|
"safetyRatio",
|
|
@@ -799,6 +803,7 @@ function capacityCalibration(value, where) {
|
|
|
799
803
|
...Object.fromEntries([
|
|
800
804
|
"maxConcurrency",
|
|
801
805
|
"requestsPerWorker",
|
|
806
|
+
"maximumRequestsPerWorker",
|
|
802
807
|
"maxP95Ms",
|
|
803
808
|
"maxErrorRate",
|
|
804
809
|
"safetyRatio",
|
|
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
132
132
|
throw new Error(`${where} ArangoDB config is invalid`);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// src/capacity-calibration.ts
|
|
136
|
+
function localCalibrationEndpoint(value) {
|
|
137
|
+
const endpoint = new URL(value);
|
|
138
|
+
if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
|
|
139
|
+
throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
|
|
140
|
+
}
|
|
141
|
+
return endpoint;
|
|
142
|
+
}
|
|
143
|
+
function validateCapacityCalibrationOptions(options) {
|
|
144
|
+
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
145
|
+
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
146
|
+
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
147
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
148
|
+
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
149
|
+
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
150
|
+
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
151
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
152
|
+
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
153
|
+
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
154
|
+
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
155
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
156
|
+
throw new Error("Capacity calibration bounds are invalid.");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
endpoint,
|
|
160
|
+
maxConcurrency,
|
|
161
|
+
requestsPerWorker,
|
|
162
|
+
maximumRequestsPerWorker,
|
|
163
|
+
maxP95Ms,
|
|
164
|
+
maxErrorRate,
|
|
165
|
+
safetyRatio,
|
|
166
|
+
requestTimeoutMs,
|
|
167
|
+
minimumStageDurationMs,
|
|
168
|
+
maxCpuUtilizationPercent,
|
|
169
|
+
maxMemoryUtilizationPercent
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
// src/deploy-plan.ts
|
|
136
174
|
var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
|
|
137
175
|
var DEPLOY_PLAN_VERSION = 1;
|
|
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
447
485
|
const row2 = object(value, where);
|
|
448
486
|
const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
|
|
449
487
|
if (row2.kind === "application") {
|
|
450
|
-
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
488
|
+
exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
|
|
451
489
|
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
452
490
|
const runtimeRequirement = requirements.get(String(runtime.requirement));
|
|
453
491
|
if (!runtimeRequirement)
|
|
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
529
567
|
fail(`${where}.service.websocket`, "must be a boolean.");
|
|
530
568
|
if (service.maximumConnections !== undefined)
|
|
531
569
|
integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
|
|
570
|
+
if (row2.capacityCalibration !== undefined) {
|
|
571
|
+
const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
|
|
572
|
+
exact3(calibration, [
|
|
573
|
+
"path",
|
|
574
|
+
"maxConcurrency",
|
|
575
|
+
"requestsPerWorker",
|
|
576
|
+
"maximumRequestsPerWorker",
|
|
577
|
+
"maxP95Ms",
|
|
578
|
+
"maxErrorRate",
|
|
579
|
+
"safetyRatio",
|
|
580
|
+
"requestTimeoutMs",
|
|
581
|
+
"minimumStageDurationMs",
|
|
582
|
+
"maxCpuUtilizationPercent",
|
|
583
|
+
"maxMemoryUtilizationPercent"
|
|
584
|
+
], `${where}.capacityCalibration`);
|
|
585
|
+
if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
|
|
586
|
+
fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
validateCapacityCalibrationOptions({
|
|
590
|
+
...calibration,
|
|
591
|
+
endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
|
|
592
|
+
});
|
|
593
|
+
} catch {
|
|
594
|
+
fail(`${where}.capacityCalibration`, "contains invalid bounds.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
532
597
|
validateResources(row2.resources, `${where}.resources`);
|
|
533
598
|
if (runtime.kind === "container") {
|
|
534
599
|
const resources = object(row2.resources, `${where}.resources`);
|
|
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
|
|
|
991
1056
|
named(componentName, "deployment.spec.components key");
|
|
992
1057
|
validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
|
|
993
1058
|
}
|
|
1059
|
+
for (const targetName of targetNames) {
|
|
1060
|
+
const calibrated = componentEntries.filter(([, component2]) => {
|
|
1061
|
+
const candidate = component2;
|
|
1062
|
+
return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
|
|
1063
|
+
});
|
|
1064
|
+
if (calibrated.length > 1) {
|
|
1065
|
+
fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
994
1068
|
for (const [targetName, target] of targetDefinitions) {
|
|
995
1069
|
if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
|
|
996
1070
|
continue;
|
package/dist/deploy-plan.js
CHANGED
|
@@ -132,6 +132,44 @@ function validateBuiltInProvider(requirement2, where) {
|
|
|
132
132
|
throw new Error(`${where} ArangoDB config is invalid`);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// src/capacity-calibration.ts
|
|
136
|
+
function localCalibrationEndpoint(value) {
|
|
137
|
+
const endpoint = new URL(value);
|
|
138
|
+
if (endpoint.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(endpoint.hostname) || !endpoint.port || endpoint.username || endpoint.password || endpoint.hash) {
|
|
139
|
+
throw new Error("Capacity calibration requires an explicit loopback HTTP endpoint and port.");
|
|
140
|
+
}
|
|
141
|
+
return endpoint;
|
|
142
|
+
}
|
|
143
|
+
function validateCapacityCalibrationOptions(options) {
|
|
144
|
+
const endpoint = localCalibrationEndpoint(options.endpoint);
|
|
145
|
+
const maxConcurrency = options.maxConcurrency ?? 256;
|
|
146
|
+
const requestsPerWorker = options.requestsPerWorker ?? 8;
|
|
147
|
+
const maximumRequestsPerWorker = options.maximumRequestsPerWorker ?? 100;
|
|
148
|
+
const maxP95Ms = options.maxP95Ms ?? 250;
|
|
149
|
+
const maxErrorRate = options.maxErrorRate ?? 0.01;
|
|
150
|
+
const safetyRatio = options.safetyRatio ?? 0.8;
|
|
151
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? 5000;
|
|
152
|
+
const minimumStageDurationMs = options.minimumStageDurationMs ?? 5000;
|
|
153
|
+
const maxCpuUtilizationPercent = options.maxCpuUtilizationPercent ?? 90;
|
|
154
|
+
const maxMemoryUtilizationPercent = options.maxMemoryUtilizationPercent ?? 90;
|
|
155
|
+
if (!Number.isSafeInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 4096 || !Number.isSafeInteger(requestsPerWorker) || requestsPerWorker < 2 || requestsPerWorker > 100 || !Number.isSafeInteger(maximumRequestsPerWorker) || maximumRequestsPerWorker < requestsPerWorker || maximumRequestsPerWorker > 100 || !Number.isFinite(maxP95Ms) || maxP95Ms < 1 || !Number.isFinite(maxErrorRate) || maxErrorRate < 0 || maxErrorRate > 0.2 || !Number.isFinite(safetyRatio) || safetyRatio < 0.25 || safetyRatio > 0.95 || !Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 100 || requestTimeoutMs > 30000 || !Number.isSafeInteger(minimumStageDurationMs) || minimumStageDurationMs < 100 || minimumStageDurationMs > 60000 || !Number.isFinite(maxCpuUtilizationPercent) || maxCpuUtilizationPercent < 25 || maxCpuUtilizationPercent > 100 || !Number.isFinite(maxMemoryUtilizationPercent) || maxMemoryUtilizationPercent < 25 || maxMemoryUtilizationPercent > 100) {
|
|
156
|
+
throw new Error("Capacity calibration bounds are invalid.");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
endpoint,
|
|
160
|
+
maxConcurrency,
|
|
161
|
+
requestsPerWorker,
|
|
162
|
+
maximumRequestsPerWorker,
|
|
163
|
+
maxP95Ms,
|
|
164
|
+
maxErrorRate,
|
|
165
|
+
safetyRatio,
|
|
166
|
+
requestTimeoutMs,
|
|
167
|
+
minimumStageDurationMs,
|
|
168
|
+
maxCpuUtilizationPercent,
|
|
169
|
+
maxMemoryUtilizationPercent
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
135
173
|
// src/deploy-plan.ts
|
|
136
174
|
var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
|
|
137
175
|
var DEPLOY_PLAN_VERSION = 1;
|
|
@@ -447,7 +485,7 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
447
485
|
const row2 = object(value, where);
|
|
448
486
|
const selectedTarget = targets.get(String(row2.target)) ?? fail(`${where}.target`, "must name an existing target.");
|
|
449
487
|
if (row2.kind === "application") {
|
|
450
|
-
exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
|
|
488
|
+
exact3(row2, ["kind", "target", "runtime", "service", "capacityCalibration", "resources", "storage", "network", "rollout"], where);
|
|
451
489
|
const runtime = object(row2.runtime, `${where}.runtime`);
|
|
452
490
|
const runtimeRequirement = requirements.get(String(runtime.requirement));
|
|
453
491
|
if (!runtimeRequirement)
|
|
@@ -529,6 +567,33 @@ function validateComponent(value, where, targets, requirements) {
|
|
|
529
567
|
fail(`${where}.service.websocket`, "must be a boolean.");
|
|
530
568
|
if (service.maximumConnections !== undefined)
|
|
531
569
|
integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
|
|
570
|
+
if (row2.capacityCalibration !== undefined) {
|
|
571
|
+
const calibration = object(row2.capacityCalibration, `${where}.capacityCalibration`);
|
|
572
|
+
exact3(calibration, [
|
|
573
|
+
"path",
|
|
574
|
+
"maxConcurrency",
|
|
575
|
+
"requestsPerWorker",
|
|
576
|
+
"maximumRequestsPerWorker",
|
|
577
|
+
"maxP95Ms",
|
|
578
|
+
"maxErrorRate",
|
|
579
|
+
"safetyRatio",
|
|
580
|
+
"requestTimeoutMs",
|
|
581
|
+
"minimumStageDurationMs",
|
|
582
|
+
"maxCpuUtilizationPercent",
|
|
583
|
+
"maxMemoryUtilizationPercent"
|
|
584
|
+
], `${where}.capacityCalibration`);
|
|
585
|
+
if (typeof calibration.path !== "string" || !HEALTH_PATH.test(calibration.path) || calibration.path.includes("..") || calibration.path.includes("//")) {
|
|
586
|
+
fail(`${where}.capacityCalibration.path`, "must be one bounded absolute loopback path.");
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
validateCapacityCalibrationOptions({
|
|
590
|
+
...calibration,
|
|
591
|
+
endpoint: `http://127.0.0.1:${String(service.port)}${calibration.path}`
|
|
592
|
+
});
|
|
593
|
+
} catch {
|
|
594
|
+
fail(`${where}.capacityCalibration`, "contains invalid bounds.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
532
597
|
validateResources(row2.resources, `${where}.resources`);
|
|
533
598
|
if (runtime.kind === "container") {
|
|
534
599
|
const resources = object(row2.resources, `${where}.resources`);
|
|
@@ -991,6 +1056,15 @@ function compileDeployment(sourceValue) {
|
|
|
991
1056
|
named(componentName, "deployment.spec.components key");
|
|
992
1057
|
validateComponent(value, `deployment.spec.components.${componentName}`, targetDefinitions, requirementDefinitions);
|
|
993
1058
|
}
|
|
1059
|
+
for (const targetName of targetNames) {
|
|
1060
|
+
const calibrated = componentEntries.filter(([, component2]) => {
|
|
1061
|
+
const candidate = component2;
|
|
1062
|
+
return candidate.kind === "application" && candidate.target === targetName && candidate.capacityCalibration !== undefined;
|
|
1063
|
+
});
|
|
1064
|
+
if (calibrated.length > 1) {
|
|
1065
|
+
fail(`deployment.spec.components`, `may calibrate at most one application on target ${targetName}.`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
994
1068
|
for (const [targetName, target] of targetDefinitions) {
|
|
995
1069
|
if (target.connectivity?.public?.mode !== "cloudflare-tunnel")
|
|
996
1070
|
continue;
|
package/dist/deploy.d.ts
CHANGED
|
@@ -340,6 +340,20 @@ export interface ApplicationComponent {
|
|
|
340
340
|
websocket?: boolean;
|
|
341
341
|
maximumConnections?: number;
|
|
342
342
|
};
|
|
343
|
+
/** Optional bounded loopback workload calibration performed after a healthy release. */
|
|
344
|
+
capacityCalibration?: {
|
|
345
|
+
path: string;
|
|
346
|
+
maxConcurrency?: number;
|
|
347
|
+
requestsPerWorker?: number;
|
|
348
|
+
maximumRequestsPerWorker?: number;
|
|
349
|
+
maxP95Ms?: number;
|
|
350
|
+
maxErrorRate?: number;
|
|
351
|
+
safetyRatio?: number;
|
|
352
|
+
requestTimeoutMs?: number;
|
|
353
|
+
minimumStageDurationMs?: number;
|
|
354
|
+
maxCpuUtilizationPercent?: number;
|
|
355
|
+
maxMemoryUtilizationPercent?: number;
|
|
356
|
+
};
|
|
343
357
|
resources: ResourceDefinition;
|
|
344
358
|
storage?: readonly StorageMount[];
|
|
345
359
|
network?: NetworkDefinition;
|