Skip to content
File

Blob: test/compaction-plan.test.ts

typescript172 lines
1import { describe, expect, test } from "vitest";
2 
3import type { PackCatalogRow } from "@/worker/do/repo/db/schema";
4import {
5 AUTO_COMPACTION_MAX_SOURCE_BYTES,
6 AUTO_COMPACTION_MAX_SOURCE_OBJECTS,
7 catalogNeedsCompaction,
8 selectCompactionWork,
9} from "@/worker/do/repo/catalog/compaction/plan";
10 
11function packRow(args: {
12 seqLo: number;
13 seqHi?: number;
14 tier?: number;
15 kind?: PackCatalogRow["kind"];
16 objectCount?: number;
17 packBytes?: number;
18}): PackCatalogRow {
19 const seqHi = args.seqHi ?? args.seqLo;
20 const tier = args.tier ?? 0;
21 return {
22 packKey: `pack-t${tier}-${args.seqLo}-${seqHi}.pack`,
23 kind: args.kind ?? "receive",
24 state: "active",
25 tier,
26 seqLo: args.seqLo,
27 seqHi,
28 objectCount: args.objectCount ?? 10,
29 packBytes: args.packBytes ?? 1024,
30 idxBytes: 128,
31 createdAt: args.seqLo,
32 supersededBy: null,
33 };
34}
35 
36function selectedSeqs(rows: PackCatalogRow[]): number[] {
37 const selection = selectCompactionWork(rows);
38 expect(selection.status).toBe("ready");
39 if (selection.status !== "ready") return [];
40 return selection.plan.sourcePacks.map((pack) => pack.seqLo);
41}
42 
43describe("automatic compaction planning", () => {
44 test("skips an oversized oldest base pack and compacts the next closed window", () => {
45 const rows = [
46 packRow({ seqLo: 5, objectCount: 113, packBytes: 71_514 }),
47 packRow({ seqLo: 4, objectCount: 1_142, packBytes: 283_549 }),
48 packRow({ seqLo: 3, objectCount: 2_042, packBytes: 673_559 }),
49 packRow({ seqLo: 2, objectCount: 1_025, packBytes: 348_376 }),
50 packRow({
51 seqLo: 1,
52 objectCount: 79_056,
53 packBytes: AUTO_COMPACTION_MAX_SOURCE_BYTES + 1,
54 }),
55 ];
56 
57 expect(selectedSeqs(rows)).toEqual([2, 3, 4, 5]);
58 expect(catalogNeedsCompaction(rows)).toBe(true);
59 });
60 
61 test("preserves oldest-first behavior when the oldest closed window fits the budget", () => {
62 const rows = [
63 packRow({ seqLo: 5 }),
64 packRow({ seqLo: 4 }),
65 packRow({ seqLo: 3 }),
66 packRow({ seqLo: 2 }),
67 packRow({ seqLo: 1 }),
68 ];
69 
70 expect(selectedSeqs(rows)).toEqual([1, 2, 3, 4]);
71 });
72 
73 test("rejects a window whose sequence interval overlaps an unselected active pack", () => {
74 const rows = [
75 packRow({ seqLo: 6 }),
76 packRow({ seqLo: 5 }),
77 packRow({ seqLo: 4 }),
78 packRow({ seqLo: 3 }),
79 packRow({ seqLo: 1 }),
80 packRow({ seqLo: 2, tier: 1 }),
81 ];
82 
83 expect(selectedSeqs(rows)).toEqual([3, 4, 5, 6]);
84 });
85 
86 test("rejects source windows overlapped by a compact range from another tier", () => {
87 const rows = [
88 packRow({ seqLo: 8 }),
89 packRow({ seqLo: 7 }),
90 packRow({ seqLo: 6 }),
91 packRow({ seqLo: 5 }),
92 packRow({ seqLo: 4 }),
93 packRow({ seqLo: 3 }),
94 packRow({ seqLo: 2 }),
95 packRow({ seqLo: 1 }),
96 packRow({ seqLo: 1, seqHi: 4, tier: 1, kind: "compact" }),
97 ];
98 
99 expect(selectedSeqs(rows)).toEqual([5, 6, 7, 8]);
100 });
101 
102 test("uses a later overflowing tier when an earlier tier only has over-budget windows", () => {
103 const perPackObjects = Math.floor(AUTO_COMPACTION_MAX_SOURCE_OBJECTS / 4) + 1;
104 const rows = [
105 packRow({ seqLo: 5, objectCount: perPackObjects }),
106 packRow({ seqLo: 4, objectCount: perPackObjects }),
107 packRow({ seqLo: 3, objectCount: perPackObjects }),
108 packRow({ seqLo: 2, objectCount: perPackObjects }),
109 packRow({ seqLo: 1, objectCount: perPackObjects }),
110 packRow({ seqLo: 14, tier: 1 }),
111 packRow({ seqLo: 13, tier: 1 }),
112 packRow({ seqLo: 12, tier: 1 }),
113 packRow({ seqLo: 11, tier: 1 }),
114 packRow({ seqLo: 10, tier: 1 }),
115 ];
116 
117 const selection = selectCompactionWork(rows);
118 expect(selection.status).toBe("ready");
119 if (selection.status !== "ready") return;
120 expect(selection.plan.sourceTier).toBe(1);
121 expect(selection.plan.sourcePacks.map((pack) => pack.seqLo)).toEqual([10, 11, 12, 13]);
122 });
123 
124 test("finds a later closed window in a large catalog with many overlapped windows", () => {
125 const rows: PackCatalogRow[] = [];
126 for (let seq = 604; seq >= 1; seq--) {
127 rows.push(packRow({ seqLo: seq }));
128 }
129 rows.push(packRow({ seqLo: 1, seqHi: 600, tier: 1, kind: "compact" }));
130 
131 expect(selectedSeqs(rows)).toEqual([601, 602, 603, 604]);
132 });
133 
134 test("blocks automatic compaction when every closed window exceeds the budget", () => {
135 const perPackObjects = Math.floor(AUTO_COMPACTION_MAX_SOURCE_OBJECTS / 4) + 1;
136 const rows = [
137 packRow({ seqLo: 5, objectCount: perPackObjects }),
138 packRow({ seqLo: 4, objectCount: perPackObjects }),
139 packRow({ seqLo: 3, objectCount: perPackObjects }),
140 packRow({ seqLo: 2, objectCount: perPackObjects }),
141 packRow({ seqLo: 1, objectCount: perPackObjects }),
142 ];
143 
144 const selection = selectCompactionWork(rows);
145 expect(selection.status).toBe("blocked");
146 if (selection.status !== "blocked") return;
147 expect(selection.blocked.reason).toBe("over-budget");
148 expect(selection.blocked.smallestWindowObjects).toBe(perPackObjects * 4);
149 expect(catalogNeedsCompaction(rows)).toBe(false);
150 });
151 
152 test("blocks automatic compaction when no closed sequence window exists", () => {
153 const rows = [
154 packRow({ seqLo: 9 }),
155 packRow({ seqLo: 7 }),
156 packRow({ seqLo: 5 }),
157 packRow({ seqLo: 3 }),
158 packRow({ seqLo: 1 }),
159 packRow({ seqLo: 8, tier: 1 }),
160 packRow({ seqLo: 6, tier: 1 }),
161 packRow({ seqLo: 4, tier: 1 }),
162 packRow({ seqLo: 2, tier: 1 }),
163 ];
164 
165 const selection = selectCompactionWork(rows);
166 expect(selection.status).toBe("blocked");
167 if (selection.status !== "blocked") return;
168 expect(selection.blocked.reason).toBe("non-contiguous-window");
169 expect(catalogNeedsCompaction(rows)).toBe(false);
170 });
171});