feat(storage): reserve capacity before writes

This commit is contained in:
Sucukdeluxe
2026-08-12 02:09:21 +02:00
parent ebfd98226b
commit 51b422fa10
5 changed files with 829 additions and 42 deletions
+140
View File
@@ -0,0 +1,140 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
DiskReservationCoordinator,
calculateExtractionReservationBytes,
calculateRemainingReservationBytes
} from "../src/main/disk-space";
describe("disk reservation coordinator", () => {
it("reserves only the remaining known bytes and keeps the configured safety margin free", async () => {
const coordinator = new DiskReservationCoordinator({
safetyBytes: 100,
now: () => 10,
statVolume: async (targetPath) => ({
path: targetPath,
volumeKey: "volume-a",
freeBytes: 1_000,
totalBytes: 2_000
})
});
const first = await coordinator.reserve({
phase: "download",
ownerId: "item-a",
targetPath: path.join("C:\\", "downloads", "a.bin"),
requiredBytes: 800,
alreadyPresentBytes: 300
});
expect(first.reservedBytes).toBe(500);
await expect(coordinator.reserve({
phase: "download",
ownerId: "item-b",
targetPath: path.join("C:\\", "downloads", "b.bin"),
requiredBytes: 450,
alreadyPresentBytes: 0
})).rejects.toMatchObject({
event: expect.objectContaining({
phase: "download",
ownerId: "item-b",
volumeKey: "volume-a",
requiredBytes: 450,
availableBytes: 400,
deficitBytes: 50,
safetyBytes: 100,
retryAt: 30_010
})
});
});
it("lets unknown sizes pass without consuming volume capacity", async () => {
const coordinator = new DiskReservationCoordinator({
safetyBytes: 100,
statVolume: async () => {
throw new Error("capacity lookup should not run for unknown sizes");
}
});
const lease = await coordinator.reserve({
phase: "download",
ownerId: "unknown-item",
targetPath: path.join("D:\\", "downloads", "unknown.bin"),
requiredBytes: null,
alreadyPresentBytes: 0
});
expect(lease.reservedBytes).toBe(0);
expect(coordinator.getReservedBytesByVolume().size).toBe(0);
});
it("updates and releases lease pressure on the owning volume", async () => {
const coordinator = new DiskReservationCoordinator({
safetyBytes: 100,
statVolume: async (targetPath) => ({
path: targetPath,
volumeKey: "volume-b",
freeBytes: 1_200,
totalBytes: 2_000
})
});
const lease = await coordinator.reserve({
phase: "download",
ownerId: "item-c",
targetPath: path.join("E:\\", "downloads", "c.bin"),
requiredBytes: 900,
alreadyPresentBytes: 100
});
await lease.update({ requiredBytes: 600, alreadyPresentBytes: 400 });
expect(lease.reservedBytes).toBe(200);
expect(coordinator.getReservedBytesByVolume().get("volume-b")).toBe(200);
lease.release();
expect(lease.released).toBe(true);
expect(coordinator.getReservedBytesByVolume().get("volume-b")).toBe(0);
});
it("serializes parallel reservations so one volume cannot be overbooked", async () => {
const coordinator = new DiskReservationCoordinator({
safetyBytes: 100,
statVolume: async (targetPath) => ({
path: targetPath,
volumeKey: "volume-c",
freeBytes: 1_000,
totalBytes: 2_000
})
});
const results = await Promise.allSettled([
coordinator.reserve({
phase: "download",
ownerId: "item-d",
targetPath: path.join("F:\\", "downloads", "d.bin"),
requiredBytes: 600,
alreadyPresentBytes: 0
}),
coordinator.reserve({
phase: "download",
ownerId: "item-e",
targetPath: path.join("F:\\", "downloads", "e.bin"),
requiredBytes: 600,
alreadyPresentBytes: 0
})
]);
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
expect(results.filter((result) => result.status === "rejected")).toHaveLength(1);
expect(coordinator.getReservedBytesByVolume().get("volume-c")).toBe(600);
});
it("calculates conservative download and extraction reservation sizes", () => {
expect(calculateRemainingReservationBytes(1_000, 250)).toBe(750);
expect(calculateRemainingReservationBytes(1_000, 1_500)).toBe(0);
expect(calculateRemainingReservationBytes(null, 250)).toBeNull();
expect(calculateExtractionReservationBytes([500, null, 200, 0])).toBe(700);
expect(calculateExtractionReservationBytes([null, 0])).toBeNull();
});
});