gitoriaLog in with ident

mpackdb

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit8788872587888725release 1.0.7caramboleyo87888725/tests/concurrent-find-insert.test.js

7.0 KB

  1. import MPackDB, { PrimaryKeyType } from '../src/MPackDB.js';
  2. import { rm } from 'fs/promises';
  3. const TEST_DB_PATH = 'db/concurrent-find-insert';
  4. console.log('--- Concurrent Find-Then-Insert Test ---');
  5. console.log('This simulates the exact pattern from login.js:');
  6. console.log(' 1. find user by email');
  7. console.log(' 2. if not found, insert');
  8. console.log(' Called concurrently for the same email.\n');
  9. class User {
  10. id = null;
  11. name = null;
  12. email = null;
  13. }
  14. class Users extends MPackDB {
  15. _classToUse = User;
  16. _primaryKey = 'id';
  17. _primaryKeyType = PrimaryKeyType.UUID;
  18. }
  19. // --- Raw pattern (no withLock) — demonstrates the race condition ---
  20. async function loginFlowRaw(db, label, email) {
  21. const [user] = await db.find(u => u.email === email);
  22. let userId;
  23. if (user) {
  24. console.log(` [${label}] Found existing user ${user.id} for ${email}`);
  25. userId = user.id;
  26. } else {
  27. console.log(` [${label}] No user found for ${email}, inserting...`);
  28. userId = await db.insert({ email });
  29. console.log(` [${label}] Inserted user ${userId} for ${email}`);
  30. }
  31. return userId;
  32. }
  33. // --- withLock pattern — the fix ---
  34. async function loginFlowLocked(db, label, email) {
  35. return db.withLock(async () => {
  36. const [user] = await db.find(u => u.email === email);
  37. let userId;
  38. if (user) {
  39. console.log(` [${label}] Found existing user ${user.id} for ${email}`);
  40. userId = user.id;
  41. } else {
  42. console.log(` [${label}] No user found for ${email}, inserting...`);
  43. userId = await db.insert({ email });
  44. console.log(` [${label}] Inserted user ${userId} for ${email}`);
  45. }
  46. return userId;
  47. });
  48. }
  49. // ============================================================
  50. // PART 1: Raw pattern — shows the problem
  51. // ============================================================
  52. console.log('=== PART 1: Raw find-then-insert (no lock) ===\n');
  53. // TEST 1: Sequential (should pass even without lock)
  54. await rm(TEST_DB_PATH, { recursive: true, force: true });
  55. const users1 = new Users(`${TEST_DB_PATH}/users`);
  56. console.log('TEST 1: Sequential login calls for same email');
  57. await loginFlowRaw(users1, 'seq', '[email protected]');
  58. await loginFlowRaw(users1, 'seq', '[email protected]');
  59. await loginFlowRaw(users1, 'seq', '[email protected]');
  60. const seqUsers = await users1.find(u => u.email === '[email protected]');
  61. console.log(` Result: ${seqUsers.length} user(s) created (expected: 1)`);
  62. console.log(` ${seqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);
  63. await users1.close();
  64. // TEST 2: 2 concurrent (expected to create duplicates without lock)
  65. await rm(TEST_DB_PATH, { recursive: true, force: true });
  66. const users2 = new Users(`${TEST_DB_PATH}/users`);
  67. console.log('TEST 2: 2 concurrent login calls WITHOUT withLock (demonstrates race)');
  68. await Promise.all([
  69. loginFlowRaw(users2, 'raw2', '[email protected]'),
  70. loginFlowRaw(users2, 'raw2', '[email protected]'),
  71. ]);
  72. const raw2Users = await users2.find(u => u.email === '[email protected]');
  73. console.log(` Result: ${raw2Users.length} user(s) created`);
  74. console.log(` ${raw2Users.length > 1 ? '⚠ RACE CONFIRMED (duplicates without lock)' : '✓ No race (lucky timing)'}\n`);
  75. await users2.close();
  76. // TEST 3: 3 concurrent (expected to create duplicates without lock)
  77. await rm(TEST_DB_PATH, { recursive: true, force: true });
  78. const users3 = new Users(`${TEST_DB_PATH}/users`);
  79. console.log('TEST 3: 3 concurrent login calls WITHOUT withLock (demonstrates race)');
  80. await Promise.all([
  81. loginFlowRaw(users3, 'raw3', '[email protected]'),
  82. loginFlowRaw(users3, 'raw3', '[email protected]'),
  83. loginFlowRaw(users3, 'raw3', '[email protected]'),
  84. ]);
  85. const raw3Users = await users3.find(u => u.email === '[email protected]');
  86. console.log(` Result: ${raw3Users.length} user(s) created`);
  87. console.log(` ${raw3Users.length > 1 ? '⚠ RACE CONFIRMED (duplicates without lock)' : '✓ No race (lucky timing)'}\n`);
  88. await users3.close();
  89. // ============================================================
  90. // PART 2: withLock pattern — proves the fix
  91. // ============================================================
  92. console.log('=== PART 2: withLock find-then-insert (atomic) ===\n');
  93. // TEST 4: Sequential with lock
  94. await rm(TEST_DB_PATH, { recursive: true, force: true });
  95. const users4 = new Users(`${TEST_DB_PATH}/users`);
  96. console.log('TEST 4: Sequential login calls with withLock');
  97. await loginFlowLocked(users4, 'lock-seq', '[email protected]');
  98. await loginFlowLocked(users4, 'lock-seq', '[email protected]');
  99. await loginFlowLocked(users4, 'lock-seq', '[email protected]');
  100. const lockSeqUsers = await users4.find(u => u.email === '[email protected]');
  101. console.log(` Result: ${lockSeqUsers.length} user(s) created (expected: 1)`);
  102. console.log(` ${lockSeqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);
  103. await users4.close();
  104. // TEST 5: 2 concurrent with lock
  105. await rm(TEST_DB_PATH, { recursive: true, force: true });
  106. const users5 = new Users(`${TEST_DB_PATH}/users`);
  107. console.log('TEST 5: 2 concurrent login calls WITH withLock');
  108. await Promise.all([
  109. loginFlowLocked(users5, 'lock2', '[email protected]'),
  110. loginFlowLocked(users5, 'lock2', '[email protected]'),
  111. ]);
  112. const lock2Users = await users5.find(u => u.email === '[email protected]');
  113. console.log(` Result: ${lock2Users.length} user(s) created (expected: 1)`);
  114. console.log(` ${lock2Users.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);
  115. await users5.close();
  116. // TEST 6: 3 concurrent with lock
  117. await rm(TEST_DB_PATH, { recursive: true, force: true });
  118. const users6 = new Users(`${TEST_DB_PATH}/users`);
  119. console.log('TEST 6: 3 concurrent login calls WITH withLock');
  120. await Promise.all([
  121. loginFlowLocked(users6, 'lock3', '[email protected]'),
  122. loginFlowLocked(users6, 'lock3', '[email protected]'),
  123. loginFlowLocked(users6, 'lock3', '[email protected]'),
  124. ]);
  125. const lock3Users = await users6.find(u => u.email === '[email protected]');
  126. console.log(` Result: ${lock3Users.length} user(s) created (expected: 1)`);
  127. console.log(` ${lock3Users.length === 1 ? '✓ PASS' : '✗ FAIL'}\n`);
  128. await users6.close();
  129. // ============================================================
  130. // SUMMARY
  131. // ============================================================
  132. console.log('=== SUMMARY ===');
  133. console.log('Without withLock:');
  134. console.log(` Sequential: ${seqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'} (${seqUsers.length} users)`);
  135. console.log(` 2 concurrent: ${raw2Users.length > 1 ? '⚠ RACE (expected)' : '✓ No race'} (${raw2Users.length} users)`);
  136. console.log(` 3 concurrent: ${raw3Users.length > 1 ? '⚠ RACE (expected)' : '✓ No race'} (${raw3Users.length} users)`);
  137. console.log('With withLock:');
  138. console.log(` Sequential: ${lockSeqUsers.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lockSeqUsers.length} users)`);
  139. console.log(` 2 concurrent: ${lock2Users.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lock2Users.length} users)`);
  140. console.log(` 3 concurrent: ${lock3Users.length === 1 ? '✓ PASS' : '✗ FAIL'} (${lock3Users.length} users)`);
  141. const withLockPass = lockSeqUsers.length === 1 && lock2Users.length === 1 && lock3Users.length === 1;
  142. if (!withLockPass) {
  143. throw new Error('withLock tests FAILED — concurrent find+insert still produces duplicates');
  144. }
  145. console.log('\n✓ withLock correctly prevents concurrent duplicates');

Branches

Latest commits

  • 87888725release 1.0.7caramboleyo
  • c4cdb9b6node: import prefixes (Deno compat) + pre-existing index-state WIPcaramboleyo
  • 0afb8f4bupdate now must be a callbackcaramboleyo
  • cde73eb4release 1.0.6caramboleyo
  • d01dda02add index hints, intersection, boundingBox; remove findByIndexcaramboleyo
  • b8ffc1a0release 1.0.5caramboleyo
  • d47876a1reimplemented lost features like indexed find and more testscaramboleyo
  • 7f08da9afixed insert ignoring model definitioncaramboleyo
  • 705774a9added flush before findcaramboleyo
  • b4db6391initial commitcaramboleyo