Prose and benchmark tables use MultiforaDB; the binary, the CLI usage line, the log-message prefix and the default database file use multiforadb. Two consequences worth noting: - build.zig.zon's fingerprint is derived from the package name, so it had to change with it (Zig refuses to build otherwise). A consumer pinning this package by fingerprint needs updating. - the default --db path is now multiforadb.log, and getCmdLineOpts reports it as dbpath. An existing mongo-lite.log has to be passed explicitly with --db. The e2e harness abbreviated the old name as ML_; that is now MFDB_, including the documented ML_BIN override (MFDB_BIN) and the scratch file names. MD_ (mongod) is untouched. compare-run.sh spawned the server by absolute path under a sandbox/mongo-lite directory that no longer exists; that block already runs from tests/e2e, so it uses a relative path now. The archived reports under tests/e2e/results/ keep the old name: they record what the old binary measured.
139 lines
5.2 KiB
JavaScript
139 lines
5.2 KiB
JavaScript
// E2E part 4: TTL indexes through the official Node driver.
|
|
// createIndex({expireAfterSeconds}), the option round-tripping through
|
|
// listIndexes, background expiry of past-dated documents, and the specs the
|
|
// server must reject with CannotCreateIndex (67).
|
|
//
|
|
// The server must run with a short sweep interval, e.g.
|
|
// zig-out/bin/multiforadb --port 27020 --db /tmp/mfdb-e2e.log --ttl-sweep-secs 1
|
|
const { MongoClient } = require('mongodb');
|
|
|
|
const URL = 'mongodb://127.0.0.1:27020';
|
|
const results = [];
|
|
function check(name, cond, detail = '') {
|
|
results.push({ name, ok: !!cond, detail: String(detail) });
|
|
if (!cond) console.error(` ✗ ${name} ${detail}`);
|
|
}
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
/// Poll until `fn()` is true or the deadline passes; expiry is a background
|
|
/// sweep, so the exact moment a document disappears is not fixed.
|
|
async function waitFor(fn, timeoutMs = 15000, stepMs = 250) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (await fn()) return true;
|
|
await sleep(stepMs);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function expectCode(fn, code, name) {
|
|
let err = null;
|
|
try {
|
|
await fn();
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
check(name, err && err.code === code, err ? `code ${err.code}: ${err.message}` : 'no error');
|
|
}
|
|
|
|
async function main() {
|
|
const client = new MongoClient(URL, { serverSelectionTimeoutMS: 5000 });
|
|
await client.connect();
|
|
const db = client.db('e2e4');
|
|
const coll = db.collection('sessions');
|
|
await coll.drop().catch(() => {});
|
|
|
|
// --- createIndex + listIndexes round-trip ---
|
|
await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 });
|
|
const idxs = await coll.indexes();
|
|
const ttlIdx = idxs.find((i) => i.name === 'expireAt_1');
|
|
check('TTL index listed', !!ttlIdx, JSON.stringify(idxs));
|
|
check('expireAfterSeconds reported', ttlIdx && Number(ttlIdx.expireAfterSeconds) === 1, JSON.stringify(ttlIdx));
|
|
check('_id_ has no expireAfterSeconds', idxs.find((i) => i.name === '_id_').expireAfterSeconds === undefined);
|
|
|
|
// Idempotent re-create of the same spec.
|
|
await coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 1 });
|
|
check('idempotent re-create', (await coll.indexes()).filter((i) => i.name === 'expireAt_1').length === 1);
|
|
|
|
// --- expiry ---
|
|
const now = Date.now();
|
|
await coll.insertMany([
|
|
{ _id: 'past', expireAt: new Date(now - 60_000) },
|
|
{ _id: 'future', expireAt: new Date(now + 3_600_000) },
|
|
{ _id: 'string', expireAt: 'tomorrow' },
|
|
{ _id: 'missing' },
|
|
// An array of dates expires on its earliest member.
|
|
{ _id: 'array', expireAt: [new Date(now - 60_000), new Date(now + 3_600_000)] },
|
|
]);
|
|
check('all five inserted', (await coll.countDocuments({})) === 5);
|
|
|
|
const gone = await waitFor(async () => (await coll.countDocuments({ _id: 'past' })) === 0);
|
|
check('past-dated doc expired', gone);
|
|
check('array doc expired on earliest date', (await coll.countDocuments({ _id: 'array' })) === 0);
|
|
const survivors = (await coll.find({}).toArray()).map((d) => d._id).sort();
|
|
check('future/string/missing survive', JSON.stringify(survivors) === '["future","missing","string"]', JSON.stringify(survivors));
|
|
|
|
// Expiry is a real delete: it holds after the sweeper has run again.
|
|
await sleep(1500);
|
|
check('expired docs stay deleted', (await coll.countDocuments({})) === 3);
|
|
|
|
// --- rejected specs ---
|
|
const bad = db.collection('bad');
|
|
await expectCode(
|
|
() => bad.createIndex({ a: 1, b: 1 }, { expireAfterSeconds: 60 }),
|
|
67,
|
|
'compound TTL rejected with 67',
|
|
);
|
|
await expectCode(
|
|
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: -1 }),
|
|
67,
|
|
'negative expireAfterSeconds rejected with 67',
|
|
);
|
|
await expectCode(
|
|
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: 'soon' }),
|
|
67,
|
|
'non-numeric expireAfterSeconds rejected with 67',
|
|
);
|
|
await expectCode(
|
|
() => bad.createIndex({ a: 1 }, { expireAfterSeconds: 2147483648 }),
|
|
67,
|
|
'expireAfterSeconds past 2147483647 rejected with 67',
|
|
);
|
|
await expectCode(
|
|
() => bad.createIndex({ _id: 1 }, { expireAfterSeconds: 60 }),
|
|
197,
|
|
'TTL on _id rejected with 197',
|
|
);
|
|
// Same name and key with a different expiry: IndexOptionsConflict (85),
|
|
// exactly as MongoDB does (the change belongs to collMod).
|
|
await expectCode(
|
|
() => coll.createIndex({ expireAt: 1 }, { expireAfterSeconds: 90 }),
|
|
85,
|
|
'changed expiry gives IndexOptionsConflict',
|
|
);
|
|
|
|
// expireAfterSeconds 0 is legal: expire at exactly the stored instant.
|
|
const zero = db.collection('zero');
|
|
await zero.drop().catch(() => {});
|
|
await zero.createIndex({ at: 1 }, { expireAfterSeconds: 0 });
|
|
await zero.insertOne({ _id: 'now', at: new Date(Date.now() - 1000) });
|
|
check('expireAfterSeconds 0 accepted', (await zero.indexes()).some((i) => Number(i.expireAfterSeconds) === 0));
|
|
check('expireAfterSeconds 0 expires', await waitFor(async () => (await zero.countDocuments({})) === 0));
|
|
|
|
await client.close();
|
|
|
|
const failed = results.filter((r) => !r.ok);
|
|
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
|
if (failed.length) {
|
|
console.log('FAILED:', failed.map((f) => f.name).join(', '));
|
|
process.exit(1);
|
|
}
|
|
console.log('E2E4_OK');
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error('E2E4_FAIL', e);
|
|
process.exit(1);
|
|
});
|