UNPKG

indinis

Version:

A storage library using LSM trees for storage and B-trees for indices with MVCC support

170 lines (144 loc) 8.03 kB
// tssrc/test/lsm_adaptive_compaction.test.ts import { Indinis, IndinisOptions, ThreadPoolStatsJs } from '../index'; import * as fs from 'fs'; import * as path from 'path'; import { rimraf } from 'rimraf'; const TEST_DATA_DIR_BASE = path.resolve(__dirname, '..', '..', '.test-data', 'indinis-adaptive-compaction'); const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const s = (obj: any): string => JSON.stringify(obj); interface CompactionTestDoc { id: string; data: string; batch: number; } // Helper to parse SSTable filenames to check levels function parseSSTableFilename(filename: string): { level: number; id: number } | null { const match = filename.match(/^sstable_L(\d+)_(\d+)\.dat$/); if (match) { return { level: parseInt(match[1], 10), id: parseInt(match[2], 10) }; } return null; } // Helper to list SSTables on disk by their level async function listSSTablesByLevel(lsmDataDir: string): Promise<Map<number, string[]>> { const levelsMap = new Map<number, string[]>(); if (!fs.existsSync(lsmDataDir)) return levelsMap; const files = await fs.promises.readdir(lsmDataDir); for (const file of files) { const parsed = parseSSTableFilename(file); if (parsed) { if (!levelsMap.has(parsed.level)) { levelsMap.set(parsed.level, []); } levelsMap.get(parsed.level)!.push(file); } } return levelsMap; } describe('LSM-Tree Adaptive and Prioritized Compaction', () => { let db: Indinis | null = null; let testDataDir: string; let lsmDataDir: string; const colPath = 'adaptive_compaction_docs'; const L0_COMPACTION_TRIGGER_FROM_CPP = 4; const CPP_SCHEDULER_INTERVAL_SECONDS = 5; const testOptions: IndinisOptions = { checkpointIntervalSeconds: 2, minCompactionThreads: 1, // Start with a minimum of 1 maxCompactionThreads: 4, // Allow scaling up to 4 // Use a small segment size to avoid WAL files getting too large during the test walOptions: { segment_size: 128 * 1024 } }; jest.setTimeout(90000); // 90 seconds for this I/O heavy test beforeEach(async () => { const randomSuffix = `${Date.now()}-${Math.random().toString(36).substring(7)}`; testDataDir = path.join(TEST_DATA_DIR_BASE, `test-${randomSuffix}`); lsmDataDir = path.join(testDataDir, 'data', colPath); // Path to the specific LSM store's data await fs.promises.mkdir(testDataDir, { recursive: true }); console.log(`\n[ADAPTIVE COMPACTION TEST START] Using data directory: ${testDataDir}`); }); afterEach(async () => { if (db) await db.close(); db = null; if (fs.existsSync(testDataDir)) await rimraf(testDataDir); }); afterAll(async () => { if (fs.existsSync(TEST_DATA_DIR_BASE)) await rimraf(TEST_DATA_DIR_BASE); }); it('should use the adaptive thread pool, trigger L0->L1 compaction, and maintain data integrity', async () => { db = new Indinis(testDataDir, testOptions); const allWrittenDocs = new Map<string, CompactionTestDoc>(); const docsPerFlush = 10; // Enough data to ensure memtable is not empty console.log(`--- Phase 1: Creating ${L0_COMPACTION_TRIGGER_FROM_CPP} L0 SSTables to meet the compaction threshold ---`); for (let i = 0; i < L0_COMPACTION_TRIGGER_FROM_CPP; i++) { const batchNum = i + 1; await db.transaction(async tx => { for (let j = 0; j < docsPerFlush; j++) { const key = `${colPath}/batch${batchNum}_doc${j}`; const doc: CompactionTestDoc = { id: key, data: `Data for ${key}`, batch: batchNum }; await tx.set(key, s(doc)); allWrittenDocs.set(key, doc); } }); // Force a checkpoint, which also flushes the active memtable, creating an L0 SSTable. await db.forceCheckpoint(); console.log(` Batch ${batchNum}: Data committed and memtable flushed via checkpoint.`); } let levelsState = await listSSTablesByLevel(lsmDataDir); expect(levelsState.get(0)?.length ?? 0).toBe(L0_COMPACTION_TRIGGER_FROM_CPP); console.log(` Verified: ${L0_COMPACTION_TRIGGER_FROM_CPP} L0 SSTables now exist on disk.`); // --- Phase 2: Verify Thread Pool Stats and Trigger Compaction --- let poolStats = await db.debug_getThreadPoolStats(); console.log(" Initial thread pool stats:", poolStats); expect(poolStats).not.toBeNull(); if (poolStats) { expect(poolStats.minThreads).toBe(testOptions.minCompactionThreads); expect(poolStats.maxThreads).toBe(testOptions.maxCompactionThreads); expect(poolStats.currentThreadCount).toBe(testOptions.minCompactionThreads); } console.log("\n--- Phase 2: Writing one more batch to trigger the compaction scheduler ---"); await db.transaction(async tx => { const key = `${colPath}/trigger_doc`; const doc: CompactionTestDoc = { id: key, data: "trigger data", batch: 99 }; await tx.set(key, s(doc)); allWrittenDocs.set(key, doc); }); await db.forceCheckpoint(); levelsState = await listSSTablesByLevel(lsmDataDir); const l0countBeforeCompaction = levelsState.get(0)?.length ?? 0; console.log(` Trigger batch flushed. L0 SSTable count is now ${l0countBeforeCompaction}.`); expect(l0countBeforeCompaction).toBeGreaterThanOrEqual(L0_COMPACTION_TRIGGER_FROM_CPP); // --- Phase 3: Wait for Compaction and Verify --- const waitTime = (CPP_SCHEDULER_INTERVAL_SECONDS * 1000) + 3000; // Wait for scheduler + some execution time console.log(`\n--- Phase 3: Waiting ${waitTime / 1000}s for the compaction to run... ---`); await delay(waitTime); poolStats = await db.debug_getThreadPoolStats(); console.log(" Thread pool stats during/after compaction:", poolStats); // We can't deterministically know if it scaled up/down in this short time, // but we can check it's still within bounds. if (poolStats) { expect(poolStats.currentThreadCount).toBeGreaterThanOrEqual(testOptions.minCompactionThreads!); expect(poolStats.currentThreadCount).toBeLessThanOrEqual(testOptions.maxCompactionThreads!); } levelsState = await listSSTablesByLevel(lsmDataDir); console.log(" SSTable levels after waiting:", levelsState); const l0FilesAfter = levelsState.get(0) || []; const l1FilesAfter = levelsState.get(1) || []; expect(l1FilesAfter.length).toBeGreaterThan(0); // The key result: L1 files were created. expect(l0FilesAfter.length).toBeLessThan(l0countBeforeCompaction); // L0 files were consumed. console.log(` Verified: Compaction occurred. L0 files: ${l0FilesAfter.length}, L1 files: ${l1FilesAfter.length}.`); // --- Phase 4: Final Data Integrity Check --- console.log("\n--- Phase 4: Verifying data integrity after compaction ---"); await db.transaction(async tx => { for (const [key, expectedDoc] of allWrittenDocs.entries()) { const retrievedStr = await tx.get(key); if (!retrievedStr) { throw new Error(`Data integrity failure: Key "${key}" not found after compaction.`); } const retrievedDoc = JSON.parse(retrievedStr as string) as CompactionTestDoc; expect(retrievedDoc.data).toEqual(expectedDoc.data); } }); console.log(` All ${allWrittenDocs.size} documents verified successfully.`); }); });