@specific.dev/spectest 0.131.4 → 0.131.6
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/daemon.js
CHANGED
|
@@ -47,6 +47,7 @@ import { isDesktopApp } from "./desktop.js";
|
|
|
47
47
|
// each easy to restate subtly differently.
|
|
48
48
|
import { buildArgFlags, buildContentKey as computeBuildContentKey, buildTargetFlags, imageTag, isGeneratedDockerignore, pickProjectIgnore, serviceDockerignore as composeServiceDockerignore, unionDockerignore, } from "./harness/build-context.js";
|
|
49
49
|
import { startServices } from "./harness/service-graph.js";
|
|
50
|
+
import { ensureStickyBuildHeadroom } from "./harness/sticky-consumer.js";
|
|
50
51
|
import { fakeFunctions, fakeFunctionParameters, callFakeFunction } from "./harness/fake-functions.js";
|
|
51
52
|
import { dispatchFake } from "./harness/fake-websocket.js";
|
|
52
53
|
import { casesMetadata as catalogueCases, groupsMetadata as catalogueGroups, } from "./harness/catalogue.js";
|
|
@@ -2667,6 +2668,7 @@ async function bootstrapInner(prepared, identity) {
|
|
|
2667
2668
|
if (identity !== undefined) {
|
|
2668
2669
|
// No application services, setup hooks, ingress, or runtime state in the builder.
|
|
2669
2670
|
await Promise.all(prep.values());
|
|
2671
|
+
await ensureStickyBuildHeadroom();
|
|
2670
2672
|
const images = Object.create(null);
|
|
2671
2673
|
for (const svc of tasks) {
|
|
2672
2674
|
const out = await docker(["image", "inspect", "--format", "{{.Id}}", tags.get(svc.name)], 30_000);
|
|
@@ -2759,6 +2761,7 @@ async function bootstrapInner(prepared, identity) {
|
|
|
2759
2761
|
ti.setupMs = svc.setup ? Date.now() - tSetup : 0;
|
|
2760
2762
|
}
|
|
2761
2763
|
});
|
|
2764
|
+
await ensureStickyBuildHeadroom();
|
|
2762
2765
|
// Containers now have IPs — re-seed so service-targeted wildcards (e.g.
|
|
2763
2766
|
// k3s ingressDomains → the cluster container) resolve. Captured into the
|
|
2764
2767
|
// warm template, so warm starts inherit the resolved entries.
|
|
@@ -19,8 +19,18 @@ export declare function validateStickyConfig(config: StickyConsumerConfig): void
|
|
|
19
19
|
* only minFreeSpace, BuildKit 0.32.2 sets keepBytes == totalSize when healthy,
|
|
20
20
|
* and pruneOnce's strict '<' check can still evict a record on every GC pass.
|
|
21
21
|
* Actual filesystem capacity is smaller than the device due to metadata. */
|
|
22
|
-
export declare function stickyBuildkitConfig(config: StickyConsumerConfig): string;
|
|
22
|
+
export declare function stickyBuildkitConfig(config: StickyConsumerConfig, reservedBytes?: number): string;
|
|
23
23
|
export declare function initializeStickyConsumer(config: StickyConsumerConfig): Promise<void>;
|
|
24
|
+
/** Builds can finish before automatic GC restores its free-space reserve.
|
|
25
|
+
* Reclaim only unused BuildKit records before project setup adds runtime data.
|
|
26
|
+
* Docker images, containers, volumes and nested Kubernetes stores stay live.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureBuildHeadroom(config: StickyConsumerConfig, run?: typeof exec, usage?: () => Promise<{
|
|
29
|
+
freeBytes: number;
|
|
30
|
+
totalBytes: number;
|
|
31
|
+
reservedBytes: number;
|
|
32
|
+
}>): Promise<void>;
|
|
33
|
+
export declare function ensureStickyBuildHeadroom(): Promise<void>;
|
|
24
34
|
/** Filesystem pressure takes precedence over cache age/BuildKit record sizes.
|
|
25
35
|
* Only called after runtime containers/volumes have been removed on the
|
|
26
36
|
* isolated publication clone. Never removes files behind the daemons' backs.
|
|
@@ -28,6 +38,7 @@ export declare function initializeStickyConsumer(config: StickyConsumerConfig):
|
|
|
28
38
|
export declare function collectStickyCache(config: StickyConsumerConfig, retention: StickyImageRetention, run?: typeof exec, usage?: () => Promise<{
|
|
29
39
|
freeBytes: number;
|
|
30
40
|
totalBytes: number;
|
|
41
|
+
reservedBytes?: number;
|
|
31
42
|
}>, kubernetes?: KubernetesCacheMaintenance): Promise<void>;
|
|
32
43
|
/** Only on an isolated maintenance clone; never on the live ready environment. */
|
|
33
44
|
export declare function prepareStickyConsumer(artifact?: boolean): Promise<{
|
|
@@ -45,7 +45,7 @@ export function validateStickyConfig(config) {
|
|
|
45
45
|
* only minFreeSpace, BuildKit 0.32.2 sets keepBytes == totalSize when healthy,
|
|
46
46
|
* and pruneOnce's strict '<' check can still evict a record on every GC pass.
|
|
47
47
|
* Actual filesystem capacity is smaller than the device due to metadata. */
|
|
48
|
-
export function stickyBuildkitConfig(config) {
|
|
48
|
+
export function stickyBuildkitConfig(config, reservedBytes = 0) {
|
|
49
49
|
validateStickyConfig(config);
|
|
50
50
|
return `root = "${config.path}/buildkit"
|
|
51
51
|
# This standalone BuildKit does not inherit dockerd's registry mirrors.
|
|
@@ -65,7 +65,7 @@ export function stickyBuildkitConfig(config) {
|
|
|
65
65
|
[[worker.containerd.gcpolicy]]
|
|
66
66
|
all = true
|
|
67
67
|
maxUsedSpace = ${config.capacityBytes}
|
|
68
|
-
minFreeSpace = ${config.headroomBytes}
|
|
68
|
+
minFreeSpace = ${config.headroomBytes + reservedBytes}
|
|
69
69
|
`;
|
|
70
70
|
}
|
|
71
71
|
export async function initializeStickyConsumer(config) {
|
|
@@ -102,7 +102,7 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
102
102
|
dockerConfig.features = { "containerd-snapshotter": true };
|
|
103
103
|
dockerConfig["storage-driver"] = "overlayfs";
|
|
104
104
|
await writeFile(`${STICKY_RUNTIME}/docker.json`, JSON.stringify(dockerConfig));
|
|
105
|
-
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config));
|
|
105
|
+
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config, (await diskUsage(config.path)).reservedBytes));
|
|
106
106
|
const start = async (unit, program, args) => {
|
|
107
107
|
await exec("systemd-run", ["--quiet", "--unit", unit, "-p", "TimeoutStopSec=30",
|
|
108
108
|
// Pinned BuildKit 0.32.2 returns 1 after its normal first-SIGTERM
|
|
@@ -121,14 +121,47 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
121
121
|
await exec("docker", ["buildx", "create", "--name", "spectest-local", "--driver", "remote", "tcp://127.0.0.1:1234"]);
|
|
122
122
|
await exec("docker", ["buildx", "inspect", "--bootstrap", "spectest-local"]);
|
|
123
123
|
}
|
|
124
|
+
async function diskUsage(path) {
|
|
125
|
+
const fs = await statfs(path);
|
|
126
|
+
return { freeBytes: fs.bavail * fs.bsize, totalBytes: fs.blocks * fs.bsize,
|
|
127
|
+
reservedBytes: Math.max(0, fs.bfree - fs.bavail) * fs.bsize };
|
|
128
|
+
}
|
|
129
|
+
// BuildKit 0.32.2 compares MinFreeSpace against statfs.bfree, whereas
|
|
130
|
+
// applications and kubelet need bavail. Include ext4's reserved blocks so
|
|
131
|
+
// pruning restores the required usable space instead of stopping early.
|
|
132
|
+
function pruneTarget(config, usage) {
|
|
133
|
+
return String(Math.ceil((config.headroomBytes + (usage.reservedBytes ?? 0)) / 1e6));
|
|
134
|
+
}
|
|
135
|
+
/** Builds can finish before automatic GC restores its free-space reserve.
|
|
136
|
+
* Reclaim only unused BuildKit records before project setup adds runtime data.
|
|
137
|
+
* Docker images, containers, volumes and nested Kubernetes stores stay live.
|
|
138
|
+
*/
|
|
139
|
+
export async function ensureBuildHeadroom(config, run = exec, usage = () => diskUsage(config.path)) {
|
|
140
|
+
validateStickyConfig(config);
|
|
141
|
+
let fs = await usage();
|
|
142
|
+
for (let pass = 0; fs.freeBytes < config.headroomBytes && pass < 3; pass++) {
|
|
143
|
+
await run("buildctl", ["--addr", "tcp://127.0.0.1:1234", "prune", "--all", "--free-storage",
|
|
144
|
+
pruneTarget(config, fs)], 120_000);
|
|
145
|
+
fs = await usage();
|
|
146
|
+
}
|
|
147
|
+
if (fs.freeBytes < config.headroomBytes) {
|
|
148
|
+
throw new Error(`environment build left insufficient disk headroom: ${Math.floor(fs.freeBytes / 2 ** 20)} MiB available, ${Math.ceil(config.headroomBytes / 2 ** 20)} MiB required`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export async function ensureStickyBuildHeadroom() {
|
|
152
|
+
const saved = await readFile(`${STICKY_RUNTIME}/consumer.json`, "utf8").catch((error) => {
|
|
153
|
+
if (error.code === "ENOENT")
|
|
154
|
+
return undefined;
|
|
155
|
+
throw error;
|
|
156
|
+
});
|
|
157
|
+
if (saved !== undefined)
|
|
158
|
+
await ensureBuildHeadroom(JSON.parse(saved));
|
|
159
|
+
}
|
|
124
160
|
/** Filesystem pressure takes precedence over cache age/BuildKit record sizes.
|
|
125
161
|
* Only called after runtime containers/volumes have been removed on the
|
|
126
162
|
* isolated publication clone. Never removes files behind the daemons' backs.
|
|
127
163
|
*/
|
|
128
|
-
export async function collectStickyCache(config, retention, run = exec, usage =
|
|
129
|
-
const fs = await statfs(config.path);
|
|
130
|
-
return { freeBytes: fs.bavail * fs.bsize, totalBytes: fs.blocks * fs.bsize };
|
|
131
|
-
}, kubernetes) {
|
|
164
|
+
export async function collectStickyCache(config, retention, run = exec, usage = () => diskUsage(config.path), kubernetes) {
|
|
132
165
|
validateStickyConfig(config);
|
|
133
166
|
const buildctl = ["--addr", "tcp://127.0.0.1:1234", "prune"];
|
|
134
167
|
// Docker 29's containerd until filter uses local image-record creation;
|
|
@@ -156,7 +189,7 @@ export async function collectStickyCache(config, retention, run = exec, usage =
|
|
|
156
189
|
// a per-consumer cap evicts useful cache even when the disk is mostly empty.
|
|
157
190
|
const pruneBuildkit = async () => {
|
|
158
191
|
await run("buildctl", [...buildctl, "--all", "--free-storage",
|
|
159
|
-
|
|
192
|
+
pruneTarget(config, fs)], 120_000);
|
|
160
193
|
fs = await usage();
|
|
161
194
|
};
|
|
162
195
|
await pruneBuildkit();
|
package/package.json
CHANGED
package/src/daemon.ts
CHANGED
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
unionDockerignore,
|
|
100
100
|
} from "./harness/build-context.js";
|
|
101
101
|
import { startServices } from "./harness/service-graph.js";
|
|
102
|
+
import { ensureStickyBuildHeadroom } from "./harness/sticky-consumer.js";
|
|
102
103
|
import { fakeFunctions, fakeFunctionParameters, callFakeFunction } from "./harness/fake-functions.js";
|
|
103
104
|
import { dispatchFake } from "./harness/fake-websocket.js";
|
|
104
105
|
import {
|
|
@@ -3167,6 +3168,7 @@ async function bootstrapInner(prepared?: PreparedImages, identity?: string): Pro
|
|
|
3167
3168
|
if (identity !== undefined) {
|
|
3168
3169
|
// No application services, setup hooks, ingress, or runtime state in the builder.
|
|
3169
3170
|
await Promise.all(prep.values());
|
|
3171
|
+
await ensureStickyBuildHeadroom();
|
|
3170
3172
|
const images: PreparedImages["images"] = Object.create(null);
|
|
3171
3173
|
for (const svc of tasks) {
|
|
3172
3174
|
const out = await docker(["image", "inspect", "--format", "{{.Id}}", tags.get(svc.name)!], 30_000);
|
|
@@ -3258,6 +3260,8 @@ async function bootstrapInner(prepared?: PreparedImages, identity?: string): Pro
|
|
|
3258
3260
|
}
|
|
3259
3261
|
});
|
|
3260
3262
|
|
|
3263
|
+
await ensureStickyBuildHeadroom();
|
|
3264
|
+
|
|
3261
3265
|
// Containers now have IPs — re-seed so service-targeted wildcards (e.g.
|
|
3262
3266
|
// k3s ingressDomains → the cluster container) resolve. Captured into the
|
|
3263
3267
|
// warm template, so warm starts inherit the resolved entries.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
|
-
import { STICKY_COMPATIBILITY, validateStickyConfig, collectStickyCache, stickyBuildkitConfig } from "./sticky-consumer";
|
|
2
|
+
import { STICKY_COMPATIBILITY, validateStickyConfig, collectStickyCache, stickyBuildkitConfig, ensureBuildHeadroom } from "./sticky-consumer";
|
|
3
3
|
|
|
4
4
|
import type { StickyImageRetention } from "./sticky-image-retention";
|
|
5
5
|
|
|
@@ -12,6 +12,41 @@ const config = { path: "/var/lib/spectest-sticky", capacityBytes: 256 * GiB,
|
|
|
12
12
|
headroomBytes: 32 * GiB,
|
|
13
13
|
compatibility: STICKY_COMPATIBILITY };
|
|
14
14
|
|
|
15
|
+
test("completed builds preserve healthy caches and restore only missing runtime headroom", async () => {
|
|
16
|
+
let free = 32 * GiB;
|
|
17
|
+
const calls: string[][] = [];
|
|
18
|
+
const run = async (command: string, args: string[]) => {
|
|
19
|
+
calls.push([command, ...args]);
|
|
20
|
+
free += 10 * GiB;
|
|
21
|
+
return "";
|
|
22
|
+
};
|
|
23
|
+
await ensureBuildHeadroom(config, run, async () => ({ freeBytes: free, totalBytes: 251 * GiB, reservedBytes: 0 }));
|
|
24
|
+
expect(calls).toEqual([]);
|
|
25
|
+
// Tangent's build left 13 GiB before native scratch crossed the
|
|
26
|
+
// kubelet eviction threshold. Containerd reclamation can take passes.
|
|
27
|
+
free = 13 * GiB;
|
|
28
|
+
await ensureBuildHeadroom(config, run, async () => ({ freeBytes: free, totalBytes: 251 * GiB, reservedBytes: 0 }));
|
|
29
|
+
expect(free).toBe(33 * GiB);
|
|
30
|
+
expect(calls).toEqual(Array.from({ length: 2 }, () => ["buildctl", "--addr", "tcp://127.0.0.1:1234",
|
|
31
|
+
"prune", "--all", "--free-storage", String(Math.ceil(32 * GiB / 1e6))]));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("unreclaimable build storage fails before project setup without deleting runtime state", async () => {
|
|
35
|
+
const commands: string[] = [];
|
|
36
|
+
await expect(ensureBuildHeadroom(config, async (command) => {
|
|
37
|
+
commands.push(command);
|
|
38
|
+
return "";
|
|
39
|
+
}, async () => ({ freeBytes: 13 * GiB, totalBytes: 251 * GiB, reservedBytes: 0 }))).rejects.toThrow("13312 MiB available, 32768 MiB required");
|
|
40
|
+
expect(commands).toEqual(["buildctl", "buildctl", "buildctl"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("build headroom checks propagate failed GC and disk observations", async () => {
|
|
44
|
+
await expect(ensureBuildHeadroom(config, async () => { throw new Error("GC unavailable"); },
|
|
45
|
+
async () => ({ freeBytes: 13 * GiB, totalBytes: 251 * GiB, reservedBytes: 0 }))).rejects.toThrow("GC unavailable");
|
|
46
|
+
await expect(ensureBuildHeadroom(config, async () => { throw new Error("must not run"); },
|
|
47
|
+
async () => { throw new Error("filesystem unavailable"); })).rejects.toThrow("filesystem unavailable");
|
|
48
|
+
});
|
|
49
|
+
|
|
15
50
|
test("sticky consumer validates disk capacity and required free space", () => {
|
|
16
51
|
expect(() => validateStickyConfig(config)).not.toThrow();
|
|
17
52
|
// Retention has no per-consumer size caps.
|
|
@@ -160,3 +195,30 @@ test("GC does not remove an image while it supplies the Kubernetes maintenance d
|
|
|
160
195
|
});
|
|
161
196
|
expect(removed).toEqual(["old"]);
|
|
162
197
|
});
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
test("BuildKit thresholds include ext4 reserved blocks for build and publication GC", async () => {
|
|
201
|
+
// The failed Tangent build had 23 GiB available but more than 32 GiB
|
|
202
|
+
// physically free, so an unadjusted BuildKit request reclaimed nothing.
|
|
203
|
+
const reservedBytes = 13 * GiB;
|
|
204
|
+
const worker = (Bun.TOML.parse(stickyBuildkitConfig(config, reservedBytes)) as any).worker.containerd;
|
|
205
|
+
expect(worker.gcpolicy[0].minFreeSpace).toBe(45 * GiB);
|
|
206
|
+
for (const build of [true, false]) {
|
|
207
|
+
let freeBytes = 23 * GiB;
|
|
208
|
+
const requests: number[] = [];
|
|
209
|
+
const run = async (_command: string, args: string[]) => {
|
|
210
|
+
const index = args.indexOf("--free-storage");
|
|
211
|
+
if (index >= 0) {
|
|
212
|
+
requests.push(Number(args[index + 1]) * 1e6);
|
|
213
|
+
freeBytes = 33 * GiB;
|
|
214
|
+
}
|
|
215
|
+
return "";
|
|
216
|
+
};
|
|
217
|
+
const usage = async () => ({ freeBytes, totalBytes: 251 * GiB, reservedBytes });
|
|
218
|
+
if (build) await ensureBuildHeadroom(config, run, usage);
|
|
219
|
+
else await collectStickyCache(config, retention(), run, usage);
|
|
220
|
+
expect(requests).toHaveLength(1);
|
|
221
|
+
expect(requests[0]).toBeGreaterThanOrEqual(45 * GiB);
|
|
222
|
+
expect(requests[0]).toBeLessThan(45 * GiB + 1e6);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
@@ -50,7 +50,7 @@ export function validateStickyConfig(config: StickyConsumerConfig): void {
|
|
|
50
50
|
* only minFreeSpace, BuildKit 0.32.2 sets keepBytes == totalSize when healthy,
|
|
51
51
|
* and pruneOnce's strict '<' check can still evict a record on every GC pass.
|
|
52
52
|
* Actual filesystem capacity is smaller than the device due to metadata. */
|
|
53
|
-
export function stickyBuildkitConfig(config: StickyConsumerConfig): string {
|
|
53
|
+
export function stickyBuildkitConfig(config: StickyConsumerConfig, reservedBytes = 0): string {
|
|
54
54
|
validateStickyConfig(config);
|
|
55
55
|
return `root = "${config.path}/buildkit"
|
|
56
56
|
# This standalone BuildKit does not inherit dockerd's registry mirrors.
|
|
@@ -70,7 +70,7 @@ export function stickyBuildkitConfig(config: StickyConsumerConfig): string {
|
|
|
70
70
|
[[worker.containerd.gcpolicy]]
|
|
71
71
|
all = true
|
|
72
72
|
maxUsedSpace = ${config.capacityBytes}
|
|
73
|
-
minFreeSpace = ${config.headroomBytes}
|
|
73
|
+
minFreeSpace = ${config.headroomBytes + reservedBytes}
|
|
74
74
|
`;
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -105,7 +105,7 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
105
105
|
dockerConfig.features = { "containerd-snapshotter": true };
|
|
106
106
|
dockerConfig["storage-driver"] = "overlayfs";
|
|
107
107
|
await writeFile(`${STICKY_RUNTIME}/docker.json`, JSON.stringify(dockerConfig));
|
|
108
|
-
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config));
|
|
108
|
+
await writeFile(`${STICKY_RUNTIME}/buildkit.toml`, stickyBuildkitConfig(config, (await diskUsage(config.path)).reservedBytes));
|
|
109
109
|
const start = async (unit: string, program: string, args: string[]) => {
|
|
110
110
|
await exec("systemd-run", ["--quiet", "--unit", unit, "-p", "TimeoutStopSec=30",
|
|
111
111
|
// Pinned BuildKit 0.32.2 returns 1 after its normal first-SIGTERM
|
|
@@ -125,6 +125,48 @@ disabled_plugins = ["io.containerd.grpc.v1.cri"]
|
|
|
125
125
|
await exec("docker", ["buildx", "inspect", "--bootstrap", "spectest-local"]);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
async function diskUsage(path: string) {
|
|
129
|
+
const fs = await statfs(path);
|
|
130
|
+
return { freeBytes: fs.bavail * fs.bsize, totalBytes: fs.blocks * fs.bsize,
|
|
131
|
+
reservedBytes: Math.max(0, fs.bfree - fs.bavail) * fs.bsize };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// BuildKit 0.32.2 compares MinFreeSpace against statfs.bfree, whereas
|
|
135
|
+
// applications and kubelet need bavail. Include ext4's reserved blocks so
|
|
136
|
+
// pruning restores the required usable space instead of stopping early.
|
|
137
|
+
function pruneTarget(config: StickyConsumerConfig, usage: { reservedBytes?: number }): string {
|
|
138
|
+
return String(Math.ceil((config.headroomBytes + (usage.reservedBytes ?? 0)) / 1e6));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Builds can finish before automatic GC restores its free-space reserve.
|
|
142
|
+
* Reclaim only unused BuildKit records before project setup adds runtime data.
|
|
143
|
+
* Docker images, containers, volumes and nested Kubernetes stores stay live.
|
|
144
|
+
*/
|
|
145
|
+
export async function ensureBuildHeadroom(
|
|
146
|
+
config: StickyConsumerConfig,
|
|
147
|
+
run: typeof exec = exec,
|
|
148
|
+
usage = () => diskUsage(config.path),
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
validateStickyConfig(config);
|
|
151
|
+
let fs = await usage();
|
|
152
|
+
for (let pass = 0; fs.freeBytes < config.headroomBytes && pass < 3; pass++) {
|
|
153
|
+
await run("buildctl", ["--addr", "tcp://127.0.0.1:1234", "prune", "--all", "--free-storage",
|
|
154
|
+
pruneTarget(config, fs)], 120_000);
|
|
155
|
+
fs = await usage();
|
|
156
|
+
}
|
|
157
|
+
if (fs.freeBytes < config.headroomBytes) {
|
|
158
|
+
throw new Error(`environment build left insufficient disk headroom: ${Math.floor(fs.freeBytes / 2 ** 20)} MiB available, ${Math.ceil(config.headroomBytes / 2 ** 20)} MiB required`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function ensureStickyBuildHeadroom(): Promise<void> {
|
|
163
|
+
const saved = await readFile(`${STICKY_RUNTIME}/consumer.json`, "utf8").catch((error: NodeJS.ErrnoException) => {
|
|
164
|
+
if (error.code === "ENOENT") return undefined;
|
|
165
|
+
throw error;
|
|
166
|
+
});
|
|
167
|
+
if (saved !== undefined) await ensureBuildHeadroom(JSON.parse(saved));
|
|
168
|
+
}
|
|
169
|
+
|
|
128
170
|
/** Filesystem pressure takes precedence over cache age/BuildKit record sizes.
|
|
129
171
|
* Only called after runtime containers/volumes have been removed on the
|
|
130
172
|
* isolated publication clone. Never removes files behind the daemons' backs.
|
|
@@ -133,10 +175,7 @@ export async function collectStickyCache(
|
|
|
133
175
|
config: StickyConsumerConfig,
|
|
134
176
|
retention: StickyImageRetention,
|
|
135
177
|
run: typeof exec = exec,
|
|
136
|
-
usage
|
|
137
|
-
const fs = await statfs(config.path);
|
|
138
|
-
return { freeBytes: fs.bavail * fs.bsize, totalBytes: fs.blocks * fs.bsize };
|
|
139
|
-
},
|
|
178
|
+
usage: () => Promise<{ freeBytes: number; totalBytes: number; reservedBytes?: number }> = () => diskUsage(config.path),
|
|
140
179
|
kubernetes?: KubernetesCacheMaintenance,
|
|
141
180
|
): Promise<void> {
|
|
142
181
|
validateStickyConfig(config);
|
|
@@ -164,7 +203,7 @@ export async function collectStickyCache(
|
|
|
164
203
|
// a per-consumer cap evicts useful cache even when the disk is mostly empty.
|
|
165
204
|
const pruneBuildkit = async () => {
|
|
166
205
|
await run("buildctl", [...buildctl, "--all", "--free-storage",
|
|
167
|
-
|
|
206
|
+
pruneTarget(config, fs)], 120_000);
|
|
168
207
|
fs = await usage();
|
|
169
208
|
};
|
|
170
209
|
await pruneBuildkit();
|