kimi-code/packages/minidb/test/compaction.test.ts

168 lines
6.1 KiB
TypeScript

// test/compaction.test.js
import { test } from 'vitest';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { MiniDb } from '../src/index.js';
async function tmpDir() {
return fs.mkdtemp(path.join(os.tmpdir(), 'minidb-compact-'));
}
test('manual compact writes a snapshot, shrinks the WAL, and keeps data', async () => {
const dir = await tmpDir();
try {
let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' });
for (let i = 0; i < 500; i++) await db.set(`k${i}`, `value-${i}`);
const walBefore = (await fs.stat(path.join(dir, 'db.wal'))).size;
await db.compact();
assert.equal(db.stats.compactions, 1);
const snap = await fs.stat(path.join(dir, 'db.snapshot'));
const walAfter = (await fs.stat(path.join(dir, 'db.wal'))).size;
assert.ok(snap.size > 0, 'snapshot file exists and is non-empty');
assert.ok(walAfter < walBefore, 'WAL shrank after compaction');
assert.equal(db.size, 500);
await db.close();
// Recovery should load snapshot + (small) WAL and restore everything.
db = await MiniDb.open({ dir, valueCodec: 'string' });
assert.equal(db.recoveryInfo.snapshotFrames, 500);
assert.equal(db.size, 500);
assert.equal(db.get('k0'), 'value-0');
assert.equal(db.get('k499'), 'value-499');
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test('a write issued during compaction is preserved', async () => {
const dir = await tmpDir();
try {
let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' });
for (let i = 0; i < 200; i++) await db.set(`k${i}`, `v${i}`);
const compactP = db.compact();
const setP = db.set('during', 'hello'); // guard should queue behind compaction
await Promise.all([compactP, setP]);
assert.equal(db.get('during'), 'hello');
await db.close();
db = await MiniDb.open({ dir, valueCodec: 'string' });
assert.equal(db.size, 201);
assert.equal(db.get('during'), 'hello');
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test('auto-compaction triggers when the WAL crosses the threshold', async () => {
const dir = await tmpDir();
try {
const db = await MiniDb.open({
dir,
valueCodec: 'string',
fsyncPolicy: 'no',
compactThresholdBytes: 1024, // 1 KiB
});
for (let i = 0; i < 200; i++) await db.set(`k${i}`, `v${i}`.padEnd(50, 'x'));
// Allow the background compaction to finish.
if (db.compacting) await db._compactDone;
assert.ok(db.stats.compactions >= 1, 'at least one auto-compaction ran');
assert.equal(db.size, 200);
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test('del-then-compact drops tombstoned keys from the snapshot', async () => {
const dir = await tmpDir();
try {
let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' });
await db.set('a', '1');
await db.set('b', '2');
await db.del('a');
await db.compact();
await db.close();
db = await MiniDb.open({ dir, valueCodec: 'string' });
assert.equal(db.recoveryInfo.snapshotFrames, 1); // only 'b' survived
assert.equal(db.get('a'), undefined);
assert.equal(db.get('b'), '2');
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test('concurrent SET/UPDATE/DEL during compaction survive recovery', async () => {
// Exercises the fuzzy-snapshot + WAL-tail-replay convergence: writes that
// land while the snapshot is being written must all be reflected after a
// reopen, with last-writer-wins semantics.
const dir = await tmpDir();
try {
let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' });
// 5000 keys span 2 writeSnapshot yield windows (yieldEvery=2000, src/snapshot.ts),
// so the ops below genuinely race an in-progress snapshot.
const N = 5000;
for (let i = 0; i < N; i++) await db.set('k' + i, 'v' + i);
// Even keys are updated, keys == 1 (mod 4) are deleted, keys == 3 (mod 4)
// are left untouched, and a batch of new keys is added — all racing the
// in-progress snapshot.
let deleted = 0;
const M = 1000;
const compactP = db.compact();
const ops: Promise<unknown>[] = [];
for (let i = 0; i < N; i++) {
if (i % 2 === 0) ops.push(db.set('k' + i, 'updated-' + i));
else if (i % 4 === 1) {
deleted++;
ops.push(db.del('k' + i));
}
}
for (let i = 0; i < M; i++) ops.push(db.set('new' + i, 'n' + i));
await Promise.all(ops);
await compactP;
await db.close();
db = await MiniDb.open({ dir, valueCodec: 'string' });
for (let i = 0; i < N; i++) {
if (i % 2 === 0) assert.equal(db.get('k' + i), 'updated-' + i, `updated k${i}`);
else if (i % 4 === 1) assert.equal(db.get('k' + i), undefined, `deleted k${i}`);
else assert.equal(db.get('k' + i), 'v' + i, `unchanged k${i}`);
}
for (let i = 0; i < M; i++) assert.equal(db.get('new' + i), 'n' + i, `new${i}`);
assert.equal(db.size, N - deleted + M);
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}, 15_000);
test('compaction with no concurrent writes produces an empty WAL tail', async () => {
// When nothing is written during compaction, the post-fence WAL tail is empty
// and the new WAL should be zero-length (or near-zero). Data still survives.
const dir = await tmpDir();
try {
let db = await MiniDb.open({ dir, valueCodec: 'string', fsyncPolicy: 'no' });
for (let i = 0; i < 300; i++) await db.set('k' + i, 'v' + i);
await db.compact();
const walSize = (await fs.stat(path.join(dir, 'db.wal'))).size;
assert.equal(walSize, 0, 'WAL tail is empty when no writes raced compaction');
await db.close();
db = await MiniDb.open({ dir, valueCodec: 'string' });
assert.equal(db.size, 300);
assert.equal(db.get('k0'), 'v0');
assert.equal(db.get('k299'), 'v299');
await db.close();
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});