@specific.dev/spectest 0.116.1 → 0.116.2
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.
|
@@ -127,18 +127,23 @@ export async function collectStickyCache(config, retention, run = exec, usage =
|
|
|
127
127
|
}, kubernetes) {
|
|
128
128
|
validateStickyConfig(config);
|
|
129
129
|
const buildctl = ["--addr", "tcp://127.0.0.1:1234", "prune"];
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
130
|
+
// Docker 29's containerd until filter uses local image-record creation;
|
|
131
|
+
// inspect's Created uses the publisher's build date. Neither tracks use.
|
|
132
|
+
// Never prune a just-used image merely because either timestamp is old.
|
|
133
133
|
const protectedImage = (id) => retention.protectedImages.has(id) || kubernetes?.protectedImages.has(id);
|
|
134
134
|
const images = [...retention.images].sort((a, b) => a.lastUsed - b.lastUsed || a.id.localeCompare(b.id));
|
|
135
135
|
const expired = new Set();
|
|
136
136
|
for (const image of images) {
|
|
137
|
-
if (protectedImage(image.id) || image.lastUsed >= retention.observedAt -
|
|
137
|
+
if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 6 * 60 * 60 * 1000)
|
|
138
138
|
continue;
|
|
139
139
|
await run("docker", ["image", "rm", "-f", image.id], 120_000);
|
|
140
140
|
expired.add(image.id);
|
|
141
141
|
}
|
|
142
|
+
// Drop expired image references first so they no longer pin obsolete build
|
|
143
|
+
// layers. Cache mounts need a longer window: a cached RUN skips mounting
|
|
144
|
+
// its package cache, even though a later dependency change will need it.
|
|
145
|
+
await run("buildctl", [...buildctl, "--keep-duration", "6h", "--filter", "type!=exec.cachemount"], 120_000);
|
|
146
|
+
await run("buildctl", [...buildctl, "--keep-duration", "168h", "--filter", "type==exec.cachemount"], 120_000);
|
|
142
147
|
const withinTarget = (fs) => fs.freeBytes >= config.headroomBytes;
|
|
143
148
|
let fs = await usage();
|
|
144
149
|
if (withinTarget(fs))
|
package/package.json
CHANGED
|
@@ -88,18 +88,32 @@ test("healthy cache keeps recently used images and a cache that cannot shrink is
|
|
|
88
88
|
.rejects.toThrow("cannot reclaim required write headroom");
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test("
|
|
91
|
+
test("six-hour image expiry uses last observation and respects the boundary and image protection", async () => {
|
|
92
92
|
const removed: string[] = [];
|
|
93
93
|
const run = async (command: string, args: string[]) => {
|
|
94
94
|
if (command === "docker" && args[1] === "rm") removed.push(args.at(-1)!);
|
|
95
95
|
return "";
|
|
96
96
|
};
|
|
97
97
|
await collectStickyCache(config, retention([
|
|
98
|
-
{ id: "unused-for-
|
|
98
|
+
{ id: "unused-for-seven-hours", lastUsed: now - 7 * 3600_000 },
|
|
99
|
+
{ id: "exactly-six-hours", lastUsed: now - 6 * 3600_000 },
|
|
100
|
+
{ id: "used-five-hours-ago", lastUsed: now - 5 * 3600_000 },
|
|
99
101
|
{ id: "used-today", lastUsed: now },
|
|
100
102
|
{ id: "protected", lastUsed: 0 },
|
|
101
103
|
], ["protected"]), run, async () => ({ totalBytes: 256 * GiB, freeBytes: 220 * GiB }));
|
|
102
|
-
expect(removed).toEqual(["unused-for-
|
|
104
|
+
expect(removed).toEqual(["unused-for-seven-hours"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("publication removes expired images before pruning build records and gives cache mounts seven days", async () => {
|
|
108
|
+
const calls: string[][] = [];
|
|
109
|
+
const run = async (command: string, args: string[]) => { calls.push([command, ...args]); return ""; };
|
|
110
|
+
await collectStickyCache(config, retention([{ id: "superseded", lastUsed: now - 7 * 3600_000 }]),
|
|
111
|
+
run, async () => ({ totalBytes: 251 * GiB, freeBytes: 170 * GiB }));
|
|
112
|
+
expect(calls).toEqual([
|
|
113
|
+
["docker", "image", "rm", "-f", "superseded"],
|
|
114
|
+
["buildctl", "--addr", "tcp://127.0.0.1:1234", "prune", "--keep-duration", "6h", "--filter", "type!=exec.cachemount"],
|
|
115
|
+
["buildctl", "--addr", "tcp://127.0.0.1:1234", "prune", "--keep-duration", "168h", "--filter", "type==exec.cachemount"],
|
|
116
|
+
]);
|
|
103
117
|
});
|
|
104
118
|
|
|
105
119
|
test("pressure cannot evict the images used by the environment being published", async () => {
|
|
@@ -137,17 +137,22 @@ export async function collectStickyCache(
|
|
|
137
137
|
): Promise<void> {
|
|
138
138
|
validateStickyConfig(config);
|
|
139
139
|
const buildctl = ["--addr", "tcp://127.0.0.1:1234", "prune"];
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
140
|
+
// Docker 29's containerd until filter uses local image-record creation;
|
|
141
|
+
// inspect's Created uses the publisher's build date. Neither tracks use.
|
|
142
|
+
// Never prune a just-used image merely because either timestamp is old.
|
|
143
143
|
const protectedImage = (id: string) => retention.protectedImages.has(id) || kubernetes?.protectedImages.has(id);
|
|
144
144
|
const images = [...retention.images].sort((a, b) => a.lastUsed - b.lastUsed || a.id.localeCompare(b.id));
|
|
145
145
|
const expired = new Set<string>();
|
|
146
146
|
for (const image of images) {
|
|
147
|
-
if (protectedImage(image.id) || image.lastUsed >= retention.observedAt -
|
|
147
|
+
if (protectedImage(image.id) || image.lastUsed >= retention.observedAt - 6 * 60 * 60 * 1000) continue;
|
|
148
148
|
await run("docker", ["image", "rm", "-f", image.id], 120_000);
|
|
149
149
|
expired.add(image.id);
|
|
150
150
|
}
|
|
151
|
+
// Drop expired image references first so they no longer pin obsolete build
|
|
152
|
+
// layers. Cache mounts need a longer window: a cached RUN skips mounting
|
|
153
|
+
// its package cache, even though a later dependency change will need it.
|
|
154
|
+
await run("buildctl", [...buildctl, "--keep-duration", "6h", "--filter", "type!=exec.cachemount"], 120_000);
|
|
155
|
+
await run("buildctl", [...buildctl, "--keep-duration", "168h", "--filter", "type==exec.cachemount"], 120_000);
|
|
151
156
|
const withinTarget = (fs: { freeBytes: number }) => fs.freeBytes >= config.headroomBytes;
|
|
152
157
|
let fs = await usage();
|
|
153
158
|
if (withinTarget(fs)) return;
|