mpackdb
All repositories: gitoria
7.0 KB
import MPackDB, { PrimaryKeyType } from '../src/MPackDB.js';import { rm } from 'fs/promises';const TEST_DB_PATH = 'db/concurrent-find-insert';console.log('--- Concurrent Find-Then-Insert Test ---');console.log('This simulates the exact pattern from login.js:');console.log(' 1. find user by email');console.log(' 2. if not found, insert');console.log(' Called concurrently for the same email.\n');class User {id = null;name = null;email = null;}class Users extends MPackDB {_classToUse = User;_primaryKey = 'id';_primaryKeyType = PrimaryKeyType.UUID;}// --- Raw pattern (no withLock) — demonstrates the race condition ---async function loginFlowRaw(db, label, email) {const [user] = await db.find(u => u.email === email);let userId;if (user) {console.log(` [${label}] Found existing user ${user.id} for ${email}`);userId = user.id;} else {console.log(` [${label}] No user found for ${email}, inserting...`);userId = await db.insert({ email });console.log(` [${label}] Inserted user ${userId} for ${email}`);}return userId;}// --- withLock pattern — the fix ---async function loginFlowLocked(db, label, email) {return db.withLock(async () => {const [user] = await db.find(u => u.email === email);let userId;if (user) {console.log(` [${label}] Found existing user ${user.id} for ${email}`);userId = user.id;} else {console.log(` [${label}] No user found for ${email}, inserting...`);userId = await db.insert({ email });console.log(` [${label}] Inserted user ${userId} for ${email}`);}return userId;});}// ============================================================// PART 1: Raw pattern — shows the problem// ============================================================console.log('=== PART 1: Raw find-then-insert (no lock) ===\n');// TEST 1: Sequential (should pass even without lock)await rm(TEST_DB_PATH, { recursive: true, force: true });const users1 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 1: Sequential login calls for same email');await loginFlowRaw(users1, 'seq', '[email protected]');await loginFlowRaw(users1, 'seq', '[email protected]');await loginFlowRaw(users1, 'seq', '[email protected]');const seqUsers = await users1.find(u => u.email === '[email protected]');console.log(` Result: ${seqUsers.length} user(s) created (expected: 1)`);console.log(` ${seqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);await users1.close();// TEST 2: 2 concurrent (expected to create duplicates without lock)await rm(TEST_DB_PATH, { recursive: true, force: true });const users2 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 2: 2 concurrent login calls WITHOUT withLock (demonstrates race)');await Promise.all([loginFlowRaw(users2, 'raw2', '[email protected]'),loginFlowRaw(users2, 'raw2', '[email protected]'),]);const raw2Users = await users2.find(u => u.email === '[email protected]');console.log(` Result: ${raw2Users.length} user(s) created`);console.log(` ${raw2Users.length > 1 ? '⚠ RACE CONFIRMED (duplicates without lock)' : '✓ No race (lucky timing)'}\n`);await users2.close();// TEST 3: 3 concurrent (expected to create duplicates without lock)await rm(TEST_DB_PATH, { recursive: true, force: true });const users3 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 3: 3 concurrent login calls WITHOUT withLock (demonstrates race)');await Promise.all([loginFlowRaw(users3, 'raw3', '[email protected]'),loginFlowRaw(users3, 'raw3', '[email protected]'),loginFlowRaw(users3, 'raw3', '[email protected]'),]);const raw3Users = await users3.find(u => u.email === '[email protected]');console.log(` Result: ${raw3Users.length} user(s) created`);console.log(` ${raw3Users.length > 1 ? '⚠ RACE CONFIRMED (duplicates without lock)' : '✓ No race (lucky timing)'}\n`);await users3.close();// ============================================================// PART 2: withLock pattern — proves the fix// ============================================================console.log('=== PART 2: withLock find-then-insert (atomic) ===\n');// TEST 4: Sequential with lockawait rm(TEST_DB_PATH, { recursive: true, force: true });const users4 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 4: Sequential login calls with withLock');await loginFlowLocked(users4, 'lock-seq', '[email protected]');await loginFlowLocked(users4, 'lock-seq', '[email protected]');await loginFlowLocked(users4, 'lock-seq', '[email protected]');const lockSeqUsers = await users4.find(u => u.email === '[email protected]');console.log(` Result: ${lockSeqUsers.length} user(s) created (expected: 1)`);console.log(` ${lockSeqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);await users4.close();// TEST 5: 2 concurrent with lockawait rm(TEST_DB_PATH, { recursive: true, force: true });const users5 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 5: 2 concurrent login calls WITH withLock');await Promise.all([loginFlowLocked(users5, 'lock2', '[email protected]'),loginFlowLocked(users5, 'lock2', '[email protected]'),]);const lock2Users = await users5.find(u => u.email === '[email protected]');console.log(` Result: ${lock2Users.length} user(s) created (expected: 1)`);console.log(` ${lock2Users.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);await users5.close();// TEST 6: 3 concurrent with lockawait rm(TEST_DB_PATH, { recursive: true, force: true });const users6 = new Users(`${TEST_DB_PATH}/users`);console.log('TEST 6: 3 concurrent login calls WITH withLock');await Promise.all([loginFlowLocked(users6, 'lock3', '[email protected]'),loginFlowLocked(users6, 'lock3', '[email protected]'),loginFlowLocked(users6, 'lock3', '[email protected]'),]);const lock3Users = await users6.find(u => u.email === '[email protected]');console.log(` Result: ${lock3Users.length} user(s) created (expected: 1)`);console.log(` ${lock3Users.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);await users6.close();// ============================================================// SUMMARY// ============================================================console.log('=== SUMMARY ===');console.log('Without withLock:');console.log(` Sequential: ${seqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'} (${seqUsers.length} users)`);console.log(` 2 concurrent: ${raw2Users.length > 1 ? '⚠ RACE (expected)' : '✓ No race'} (${raw2Users.length} users)`);console.log(` 3 concurrent: ${raw3Users.length > 1 ? '⚠ RACE (expected)' : '✓ No race'} (${raw3Users.length} users)`);console.log('With withLock:');console.log(` Sequential: ${lockSeqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lockSeqUsers.length} users)`);console.log(` 2 concurrent: ${lock2Users.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lock2Users.length} users)`);console.log(` 3 concurrent: ${lock3Users.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lock3Users.length} users)`);const withLockPass = lockSeqUsers.length === 1 && lock2Users.length === 1 && lock3Users.length === 1;if (!withLockPass) {throw new Error('withLock tests FAILED — concurrent find+insert still produces duplicates');}console.log('\n✓ withLock correctly prevents concurrent duplicates');
Branches
- mastermain branch