transaction.test.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267
  1. 'use strict';
  2. const chai = require('chai');
  3. const expect = chai.expect;
  4. const Support = require('./support');
  5. const dialect = Support.getTestDialect();
  6. const {
  7. DataTypes,
  8. IsolationLevel,
  9. Op,
  10. QueryTypes,
  11. Transaction,
  12. TransactionType,
  13. } = require('@sequelize/core');
  14. const sinon = require('sinon');
  15. const current = Support.sequelize;
  16. const delay = require('delay');
  17. const pSettle = require('p-settle');
  18. describe(Support.getTestDialectTeaser('Transaction'), () => {
  19. if (!current.dialect.supports.transactions) {
  20. return;
  21. }
  22. beforeEach(function () {
  23. this.sinon = sinon.createSandbox();
  24. });
  25. afterEach(function () {
  26. this.sinon.restore();
  27. });
  28. describe('constructor', () => {
  29. it('stores options', function () {
  30. const transaction = new Transaction(this.sequelize);
  31. expect(transaction.options).to.be.an.instanceOf(Object);
  32. });
  33. it('generates an identifier', function () {
  34. const transaction = new Transaction(this.sequelize);
  35. expect(transaction.id).to.exist;
  36. });
  37. it('should call dialect specific generateTransactionId method', function () {
  38. const transaction = new Transaction(this.sequelize);
  39. expect(transaction.id).to.exist;
  40. if (dialect === 'mssql') {
  41. expect(transaction.id).to.have.lengthOf(20);
  42. }
  43. });
  44. });
  45. describe('commit', () => {
  46. it('is a commit method available', () => {
  47. expect(Transaction).to.respondTo('commit');
  48. });
  49. });
  50. describe('rollback', () => {
  51. it('is a rollback method available', () => {
  52. expect(Transaction).to.respondTo('rollback');
  53. });
  54. });
  55. describe('autoCallback', () => {
  56. it('supports automatically committing', async function () {
  57. await this.sequelize.transaction(async () => {});
  58. });
  59. it('supports automatically rolling back with a thrown error', async function () {
  60. let t;
  61. await expect(
  62. this.sequelize.transaction(transaction => {
  63. t = transaction;
  64. throw new Error('Yolo');
  65. }),
  66. ).to.eventually.be.rejected;
  67. expect(t.finished).to.equal('rollback');
  68. });
  69. it('supports automatically rolling back with a rejection', async function () {
  70. let t;
  71. await expect(
  72. this.sequelize.transaction(async transaction => {
  73. t = transaction;
  74. throw new Error('Swag');
  75. }),
  76. ).to.eventually.be.rejected;
  77. expect(t.finished).to.equal('rollback');
  78. });
  79. it('runs afterCommit & afterTransaction hooks when a transaction is committed', async function () {
  80. const afterCommit = sinon.spy();
  81. const afterTransaction = sinon.spy();
  82. const afterRollback = sinon.spy();
  83. let transaction;
  84. await this.sequelize.transaction(t => {
  85. transaction = t;
  86. transaction.afterCommit(afterCommit);
  87. transaction.afterRollback(afterRollback);
  88. transaction.afterTransaction(afterTransaction);
  89. return this.sequelize.query('SELECT 1+1', { transaction, type: QueryTypes.SELECT });
  90. });
  91. expect(afterCommit).to.have.been.calledOnce;
  92. expect(afterCommit).to.have.been.calledWith(transaction);
  93. expect(afterTransaction).to.have.been.calledOnce;
  94. expect(afterTransaction).to.have.been.calledWith(transaction);
  95. expect(afterRollback).to.not.have.been.called;
  96. });
  97. it('runs afterRollback & afterTransaction hooks when a transaction is rolled back', async function () {
  98. const afterCommit = sinon.spy();
  99. const afterTransaction = sinon.spy();
  100. const afterRollback = sinon.spy();
  101. let transaction;
  102. try {
  103. await this.sequelize.transaction(t => {
  104. transaction = t;
  105. transaction.afterCommit(afterCommit);
  106. transaction.afterRollback(afterRollback);
  107. transaction.afterTransaction(afterTransaction);
  108. throw new Error('Rollback');
  109. });
  110. } catch {
  111. /* ignore */
  112. }
  113. expect(afterRollback).to.have.been.calledOnce;
  114. expect(afterRollback).to.have.been.calledWith(transaction);
  115. expect(afterTransaction).to.have.been.calledOnce;
  116. expect(afterTransaction).to.have.been.calledWith(transaction);
  117. expect(afterCommit).to.not.have.been.called;
  118. });
  119. it('does not run hooks when a transaction is rolled back from database', async function () {
  120. this.sinon
  121. .stub(this.sequelize.queryInterface, '_commitTransaction')
  122. .rejects(new Error('Oh no, an error!'));
  123. const hook = sinon.spy();
  124. await expect(
  125. (async function () {
  126. await this.sequelize.transaction(transaction => {
  127. transaction.afterCommit(hook);
  128. });
  129. })(),
  130. ).to.eventually.be.rejected;
  131. expect(hook).to.not.have.been.called;
  132. });
  133. if (dialect === 'postgres') {
  134. // See #3689, #3726 and #6972 (https://github.com/sequelize/sequelize/pull/6972/files#diff-533eac602d424db379c3d72af5089e9345fd9d3bbe0a26344503c22a0a5764f7L75)
  135. it('does not try to rollback a transaction that failed upon committing with SERIALIZABLE isolation level (#3689)', async function () {
  136. // See https://wiki.postgresql.org/wiki/SSI
  137. const hook1 = sinon.spy();
  138. const hook2 = sinon.spy();
  139. const Dots = this.sequelize.define('dots', { color: DataTypes.STRING });
  140. await Dots.sync({ force: true });
  141. const initialData = [
  142. { color: 'red' },
  143. { color: 'green' },
  144. { color: 'green' },
  145. { color: 'red' },
  146. { color: 'green' },
  147. { color: 'red' },
  148. { color: 'green' },
  149. { color: 'green' },
  150. { color: 'green' },
  151. { color: 'red' },
  152. { color: 'red' },
  153. { color: 'red' },
  154. { color: 'green' },
  155. { color: 'red' },
  156. { color: 'red' },
  157. { color: 'red' },
  158. { color: 'green' },
  159. { color: 'red' },
  160. ];
  161. await Dots.bulkCreate(initialData);
  162. const isolationLevel = IsolationLevel.SERIALIZABLE;
  163. let firstTransactionGotNearCommit = false;
  164. let secondTransactionGotNearCommit = false;
  165. const firstTransaction = async () => {
  166. await this.sequelize.transaction({ isolationLevel }, async t => {
  167. t.afterCommit(hook1);
  168. await Dots.update(
  169. { color: 'red' },
  170. {
  171. where: { color: 'green' },
  172. transaction: t,
  173. },
  174. );
  175. await delay(1500);
  176. firstTransactionGotNearCommit = true;
  177. });
  178. };
  179. const secondTransaction = async () => {
  180. await delay(500);
  181. await this.sequelize.transaction({ isolationLevel }, async t => {
  182. t.afterCommit(hook2);
  183. await Dots.update(
  184. { color: 'green' },
  185. {
  186. where: { color: 'red' },
  187. transaction: t,
  188. },
  189. );
  190. // Sanity check - in this test we want this line to be reached before the
  191. // first transaction gets to commit
  192. expect(firstTransactionGotNearCommit).to.be.false;
  193. secondTransactionGotNearCommit = true;
  194. });
  195. };
  196. await expect(
  197. Promise.all([firstTransaction(), secondTransaction()]),
  198. ).to.eventually.be.rejectedWith(
  199. 'could not serialize access due to read/write dependencies among transactions',
  200. );
  201. expect(firstTransactionGotNearCommit).to.be.true;
  202. expect(secondTransactionGotNearCommit).to.be.true;
  203. // Only the second transaction worked
  204. expect(await Dots.count({ where: { color: 'red' } })).to.equal(0);
  205. expect(await Dots.count({ where: { color: 'green' } })).to.equal(initialData.length);
  206. expect(hook1).to.not.have.been.called;
  207. expect(hook2).to.have.been.called;
  208. });
  209. }
  210. });
  211. it('does not allow queries after commit', async function () {
  212. const t = await this.sequelize.startUnmanagedTransaction();
  213. await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true });
  214. await t.commit();
  215. await expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }))
  216. .to.be.eventually.rejectedWith(
  217. Error,
  218. /commit has been called on this transaction\([^)]+\), you can no longer use it\. \(The rejected query is attached as the 'sql' property of this error\)/,
  219. )
  220. .and.have.deep.property('sql')
  221. .that.equal('SELECT 1+1');
  222. });
  223. it('does not allow queries immediately after commit call', async function () {
  224. await expect(
  225. (async () => {
  226. const t = await this.sequelize.startUnmanagedTransaction();
  227. await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true });
  228. await Promise.all([
  229. expect(t.commit()).to.eventually.be.fulfilled,
  230. expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }))
  231. .to.be.eventually.rejectedWith(
  232. Error,
  233. /commit has been called on this transaction\([^)]+\), you can no longer use it\. \(The rejected query is attached as the 'sql' property of this error\)/,
  234. )
  235. .and.have.deep.property('sql')
  236. .that.equal('SELECT 1+1'),
  237. ]);
  238. })(),
  239. ).to.be.eventually.fulfilled;
  240. });
  241. it('does not allow queries after rollback', async function () {
  242. await expect(
  243. (async () => {
  244. const t = await this.sequelize.startUnmanagedTransaction();
  245. await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true });
  246. await t.rollback();
  247. return await this.sequelize.query('SELECT 1+1', { transaction: t, raw: true });
  248. })(),
  249. ).to.eventually.be.rejected;
  250. });
  251. it('should not rollback if connection was not acquired', async function () {
  252. this.sinon
  253. .stub(this.sequelize.dialect.connectionManager, 'connect')
  254. .returns(new Promise(() => {}));
  255. const transaction = new Transaction(this.sequelize);
  256. await expect(transaction.rollback()).to.eventually.be.rejectedWith(
  257. 'Transaction cannot be rolled back because it never started',
  258. );
  259. });
  260. it('does not allow queries immediately after rollback call', async function () {
  261. await expect(
  262. this.sequelize.startUnmanagedTransaction().then(async t => {
  263. await Promise.all([
  264. expect(t.rollback()).to.eventually.be.fulfilled,
  265. expect(this.sequelize.query('SELECT 1+1', { transaction: t, raw: true }))
  266. .to.be.eventually.rejectedWith(
  267. Error,
  268. /rollback has been called on this transaction\([^)]+\), you can no longer use it\. \(The rejected query is attached as the 'sql' property of this error\)/,
  269. )
  270. .and.have.deep.property('sql')
  271. .that.equal('SELECT 1+1'),
  272. ]);
  273. }),
  274. ).to.eventually.be.fulfilled;
  275. });
  276. it('does not allow commits after commit', async function () {
  277. await expect(
  278. (async () => {
  279. const t = await this.sequelize.startUnmanagedTransaction();
  280. await t.commit();
  281. return await t.commit();
  282. })(),
  283. ).to.be.rejectedWith(
  284. 'Transaction cannot be committed because it has been finished with state: commit',
  285. );
  286. });
  287. it('should run hooks if a non-auto callback transaction is committed', async function () {
  288. const hook = sinon.spy();
  289. let transaction;
  290. await expect(
  291. (async () => {
  292. try {
  293. const t = await this.sequelize.startUnmanagedTransaction();
  294. transaction = t;
  295. transaction.afterCommit(hook);
  296. await t.commit();
  297. expect(hook).to.have.been.calledOnce;
  298. expect(hook).to.have.been.calledWith(t);
  299. } catch (error) {
  300. // Cleanup this transaction so other tests don't
  301. // fail due to an open transaction
  302. if (!transaction.finished) {
  303. await transaction.rollback();
  304. throw error;
  305. }
  306. throw error;
  307. }
  308. })(),
  309. ).to.eventually.be.fulfilled;
  310. });
  311. it('should not run hooks if a non-auto callback transaction is rolled back', async function () {
  312. const hook = sinon.spy();
  313. await expect(
  314. (async () => {
  315. const t = await this.sequelize.startUnmanagedTransaction();
  316. t.afterCommit(hook);
  317. await t.rollback();
  318. expect(hook).to.not.have.been.called;
  319. })(),
  320. ).to.eventually.be.fulfilled;
  321. });
  322. it('should not run hooks if a non-auto callback transaction is rolled back in database', async function () {
  323. const hook = sinon.spy();
  324. this.sinon
  325. .stub(this.sequelize.queryInterface, '_commitTransaction')
  326. .rejects(new Error('Oh no, an error!'));
  327. await expect(
  328. (async function () {
  329. const t = await this.sequelize.startUnmanagedTransaction();
  330. t.afterCommit(hook);
  331. await t.commit();
  332. })(),
  333. ).to.eventually.be.rejected;
  334. expect(hook).to.not.have.been.called;
  335. });
  336. it('should throw an error if null is passed to afterCommit', async function () {
  337. const hook = null;
  338. let transaction;
  339. await expect(
  340. (async () => {
  341. try {
  342. const t = await this.sequelize.startUnmanagedTransaction();
  343. transaction = t;
  344. transaction.afterCommit(hook);
  345. return await t.commit();
  346. } catch (error) {
  347. // Cleanup this transaction so other tests don't
  348. // fail due to an open transaction
  349. if (!transaction.finished) {
  350. await transaction.rollback();
  351. throw error;
  352. }
  353. throw error;
  354. }
  355. })(),
  356. ).to.eventually.be.rejectedWith('"callback" must be a function');
  357. });
  358. it('should throw an error if undefined is passed to afterCommit', async function () {
  359. const hook = undefined;
  360. let transaction;
  361. await expect(
  362. (async () => {
  363. try {
  364. const t = await this.sequelize.startUnmanagedTransaction();
  365. transaction = t;
  366. transaction.afterCommit(hook);
  367. return await t.commit();
  368. } catch (error) {
  369. // Cleanup this transaction so other tests don't
  370. // fail due to an open transaction
  371. if (!transaction.finished) {
  372. await transaction.rollback();
  373. throw error;
  374. }
  375. throw error;
  376. }
  377. })(),
  378. ).to.eventually.be.rejectedWith('"callback" must be a function');
  379. });
  380. it('should throw an error if an object is passed to afterCommit', async function () {
  381. const hook = {};
  382. let transaction;
  383. await expect(
  384. (async () => {
  385. try {
  386. const t = await this.sequelize.startUnmanagedTransaction();
  387. transaction = t;
  388. transaction.afterCommit(hook);
  389. return await t.commit();
  390. } catch (error) {
  391. // Cleanup this transaction so other tests don't
  392. // fail due to an open transaction
  393. if (!transaction.finished) {
  394. await transaction.rollback();
  395. throw error;
  396. }
  397. throw error;
  398. }
  399. })(),
  400. ).to.eventually.be.rejectedWith('"callback" must be a function');
  401. });
  402. it('does not allow commits after rollback', async function () {
  403. await expect(
  404. (async () => {
  405. const t = await this.sequelize.startUnmanagedTransaction();
  406. await t.rollback();
  407. return await t.commit();
  408. })(),
  409. ).to.be.rejectedWith(
  410. 'Transaction cannot be committed because it has been finished with state: rollback',
  411. );
  412. });
  413. it('does not allow rollbacks after commit', async function () {
  414. await expect(
  415. (async () => {
  416. const t = await this.sequelize.startUnmanagedTransaction();
  417. await t.commit();
  418. return await t.rollback();
  419. })(),
  420. ).to.be.rejectedWith(
  421. 'Transaction cannot be rolled back because it has been finished with state: commit',
  422. );
  423. });
  424. it('does not allow rollbacks after rollback', async function () {
  425. await expect(
  426. (async () => {
  427. const t = await this.sequelize.startUnmanagedTransaction();
  428. await t.rollback();
  429. return await t.rollback();
  430. })(),
  431. ).to.be.rejectedWith(
  432. 'Transaction cannot be rolled back because it has been finished with state: rollback',
  433. );
  434. });
  435. it('works even if a transaction: null option is passed', async function () {
  436. this.sinon.spy(this.sequelize, 'queryRaw');
  437. const t = await this.sequelize.startUnmanagedTransaction({
  438. transaction: null,
  439. });
  440. await t.commit();
  441. if (this.sequelize.dialect.supports.connectionTransactionMethods) {
  442. expect(this.sequelize.queryRaw.callCount).to.equal(0);
  443. } else {
  444. expect(this.sequelize.queryRaw.callCount).to.be.greaterThan(0);
  445. for (let i = 0; i < this.sequelize.queryRaw.callCount; i++) {
  446. expect(this.sequelize.queryRaw.getCall(i).args[1].transaction).to.equal(t);
  447. }
  448. }
  449. });
  450. it('works even if a transaction: undefined option is passed', async function () {
  451. this.sinon.spy(this.sequelize, 'queryRaw');
  452. const t = await this.sequelize.startUnmanagedTransaction({
  453. transaction: undefined,
  454. });
  455. await t.commit();
  456. if (this.sequelize.dialect.supports.connectionTransactionMethods) {
  457. expect(this.sequelize.queryRaw.callCount).to.equal(0);
  458. } else {
  459. expect(this.sequelize.queryRaw.callCount).to.be.greaterThan(0);
  460. for (let i = 0; i < this.sequelize.queryRaw.callCount; i++) {
  461. expect(this.sequelize.queryRaw.getCall(i).args[1].transaction).to.equal(t);
  462. }
  463. }
  464. });
  465. if (['mysql', 'mariadb'].includes(dialect)) {
  466. describe('deadlock handling', () => {
  467. // Create the `Task` table and ensure it's initialized with 2 rows
  468. const getAndInitializeTaskModel = async sequelize => {
  469. const Task = sequelize.define('task', {
  470. id: {
  471. type: DataTypes.INTEGER,
  472. primaryKey: true,
  473. },
  474. });
  475. await sequelize.sync({ force: true });
  476. await Task.create({ id: 0 });
  477. await Task.create({ id: 1 });
  478. return Task;
  479. };
  480. // Lock the row with id of `from`, and then try to update the row
  481. // with id of `to`
  482. const update = async (sequelize, Task, from, to) => {
  483. await sequelize
  484. .transaction(async transaction => {
  485. try {
  486. try {
  487. await Task.findAll({
  488. where: { id: { [Op.eq]: from } },
  489. lock: transaction.LOCK.UPDATE,
  490. transaction,
  491. });
  492. await delay(10);
  493. await Task.update(
  494. { id: to },
  495. {
  496. where: { id: { [Op.ne]: to } },
  497. lock: transaction.LOCK.UPDATE,
  498. transaction,
  499. },
  500. );
  501. } catch (error) {
  502. console.log(error.message);
  503. }
  504. await Task.create({ id: 2 }, { transaction });
  505. } catch (error) {
  506. console.log(error.message);
  507. }
  508. throw new Error('Rollback!');
  509. })
  510. .catch(() => {});
  511. };
  512. it('should treat deadlocked transaction as rollback', async function () {
  513. const Task = await getAndInitializeTaskModel(this.sequelize);
  514. // This gets called twice simultaneously, and we expect at least one of the calls to encounter a
  515. // deadlock (which effectively rolls back the active transaction).
  516. // We only expect createTask() to insert rows if a transaction is active. If deadlocks are handled
  517. // properly, it should only execute a query if we're actually inside a real transaction. If it does
  518. // execute a query, we expect the newly-created rows to be destroyed when we forcibly rollback by
  519. // throwing an error.
  520. // tl;dr; This test is designed to ensure that this function never inserts and commits a new row.
  521. await Promise.all([update(this.sequelize, Task, 1, 0), update(this.sequelize, Task, 0, 1)]);
  522. const count = await Task.count();
  523. // If we were actually inside a transaction when we called `Task.create({ id: 2 })`, no new rows should be added.
  524. expect(count).to.equal(
  525. 2,
  526. 'transactions were fully rolled-back, and no new rows were added',
  527. );
  528. });
  529. it('should release the connection for a deadlocked transaction (1/2)', async function () {
  530. const Task = await getAndInitializeTaskModel(this.sequelize);
  531. // 1 of 2 queries should deadlock and be rolled back by InnoDB
  532. this.sinon.spy(this.sequelize.pool, 'release');
  533. await Promise.all([update(this.sequelize, Task, 1, 0), update(this.sequelize, Task, 0, 1)]);
  534. // Verify that both of the connections were released
  535. expect(this.sequelize.pool.release.callCount).to.equal(2);
  536. // Verify that a follow-up READ_COMMITTED works as expected.
  537. // For unknown reasons, we need to explicitly rollback on MariaDB,
  538. // even though the transaction should've automatically been rolled
  539. // back.
  540. // Otherwise, this READ_COMMITTED doesn't work as expected.
  541. const User = this.sequelize.define('user', {
  542. username: DataTypes.STRING,
  543. });
  544. await this.sequelize.sync({ force: true });
  545. await this.sequelize.transaction(
  546. { isolationLevel: IsolationLevel.READ_COMMITTED },
  547. async transaction => {
  548. const users0 = await User.findAll({ transaction });
  549. expect(users0).to.have.lengthOf(0);
  550. await User.create({ username: 'jan' }); // Create a User outside of the transaction
  551. const users = await User.findAll({ transaction });
  552. expect(users).to.have.lengthOf(1); // We SHOULD see the created user inside the transaction
  553. },
  554. );
  555. });
  556. // The following code is supposed to cause a deadlock in MariaDB & MySQL
  557. // but starting with MariaDB 10.5.15, this does not happen anymore.
  558. // See https://github.com/sequelize/sequelize/issues/14174
  559. it.skip('should release the connection for a deadlocked transaction (2/2)', async function () {
  560. const verifyDeadlock = async () => {
  561. const User = this.sequelize.define(
  562. 'user',
  563. {
  564. username: DataTypes.STRING,
  565. awesome: DataTypes.BOOLEAN,
  566. },
  567. { timestamps: false },
  568. );
  569. await this.sequelize.sync({ force: true });
  570. const { id } = await User.create({ username: 'jan' });
  571. // First, we start a transaction T1 and perform a SELECT with it using the `LOCK.SHARE` mode (setting a shared mode lock on the row).
  572. // This will cause other sessions to be able to read the row but not modify it.
  573. // So, if another transaction tries to update those same rows, it will wait until T1 commits (or rolls back).
  574. // https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
  575. const t1 = await this.sequelize.startUnmanagedTransaction();
  576. const t1Jan = await User.findByPk(id, { lock: t1.LOCK.SHARE, transaction: t1 });
  577. // Then we start another transaction T2 and see that it can indeed read the same row.
  578. const t2 = await this.sequelize.startUnmanagedTransaction({
  579. isolationLevel: IsolationLevel.READ_COMMITTED,
  580. });
  581. const t2Jan = await User.findByPk(id, { transaction: t2 });
  582. // Then, we want to see that an attempt to update that row from T2 will be queued until T1 commits.
  583. // However, before commiting T1 we will also perform an update via T1 on the same rows.
  584. // This should cause T2 to notice that it can't function anymore, so it detects a deadlock and automatically rolls itself back (and throws an error).
  585. // Meanwhile, T1 should still be ok.
  586. const executionOrder = [];
  587. const [t2AttemptData, t1AttemptData] = await pSettle([
  588. (async () => {
  589. try {
  590. executionOrder.push('Begin attempt to update via T2');
  591. await t2Jan.update({ awesome: false }, { transaction: t2 });
  592. executionOrder.push('Done updating via T2'); // Shouldn't happen
  593. } catch (error) {
  594. executionOrder.push('Failed to update via T2');
  595. throw error;
  596. }
  597. await delay(30);
  598. try {
  599. // We shouldn't reach this point, but if we do, let's at least commit the transaction
  600. // to avoid forever occupying one connection of the pool with a pending transaction.
  601. executionOrder.push('Attempting to commit T2');
  602. await t2.commit();
  603. executionOrder.push('Done committing T2');
  604. } catch {
  605. executionOrder.push('Failed to commit T2');
  606. }
  607. })(),
  608. (async () => {
  609. await delay(100);
  610. try {
  611. executionOrder.push('Begin attempt to update via T1');
  612. await t1Jan.update({ awesome: true }, { transaction: t1 });
  613. executionOrder.push('Done updating via T1');
  614. } catch (error) {
  615. executionOrder.push('Failed to update via T1'); // Shouldn't happen
  616. throw error;
  617. }
  618. await delay(150);
  619. try {
  620. executionOrder.push('Attempting to commit T1');
  621. await t1.commit();
  622. executionOrder.push('Done committing T1');
  623. } catch {
  624. executionOrder.push('Failed to commit T1'); // Shouldn't happen
  625. }
  626. })(),
  627. ]);
  628. expect(t1AttemptData.isFulfilled).to.eq(
  629. true,
  630. 'T1 is not fullfilled, but should have been',
  631. );
  632. expect(t2AttemptData.isRejected).to.eq(true, 'T2 is not rejected, but should have been');
  633. expect(t2AttemptData.reason.message).to.include(
  634. 'Deadlock found when trying to get lock; try restarting transaction',
  635. );
  636. expect(t1.finished).to.equal('commit');
  637. expect(t2.finished).to.equal('rollback');
  638. const expectedExecutionOrder = [
  639. 'Begin attempt to update via T2',
  640. 'Begin attempt to update via T1', // 100ms after
  641. 'Done updating via T1', // right after
  642. 'Failed to update via T2', // right after
  643. 'Attempting to commit T1', // 150ms after
  644. 'Done committing T1', // right after
  645. ];
  646. // The order things happen in the database must be the one shown above. However, sometimes it can happen that
  647. // the calls in the JavaScript event loop that are communicating with the database do not match exactly this order.
  648. // In particular, it is possible that the JS event loop logs `'Failed to update via T2'` before logging `'Done updating via T1'`,
  649. // even though the database updated T1 first (and then rushed to declare a deadlock for T2).
  650. const anotherAcceptableExecutionOrderFromJSPerspective = [
  651. 'Begin attempt to update via T2',
  652. 'Begin attempt to update via T1', // 100ms after
  653. 'Failed to update via T2', // right after
  654. 'Done updating via T1', // right after
  655. 'Attempting to commit T1', // 150ms after
  656. 'Done committing T1', // right after
  657. ];
  658. const executionOrderOk = Support.isDeepEqualToOneOf(executionOrder, [
  659. expectedExecutionOrder,
  660. anotherAcceptableExecutionOrderFromJSPerspective,
  661. ]);
  662. if (!executionOrderOk) {
  663. throw new Error(`Unexpected execution order: ${executionOrder.join(' > ')}`);
  664. }
  665. };
  666. for (let i = 0; i < 3 * Support.getPoolMax(); i++) {
  667. await verifyDeadlock();
  668. await delay(10);
  669. }
  670. });
  671. });
  672. }
  673. if (dialect === 'sqlite3') {
  674. it('provides persistent transactions', async () => {
  675. const sequelize = await Support.createMultiTransactionalTestSequelizeInstance();
  676. Support.destroySequelizeAfterTest(sequelize);
  677. const User = sequelize.define('user', {
  678. username: DataTypes.STRING,
  679. awesome: DataTypes.BOOLEAN,
  680. });
  681. const t1 = await sequelize.startUnmanagedTransaction();
  682. await sequelize.sync({ transaction: t1 });
  683. const t0 = t1;
  684. await User.create({}, { transaction: t0 });
  685. await t0.commit();
  686. const persistentTransaction = await sequelize.startUnmanagedTransaction();
  687. const users = await User.findAll({ transaction: persistentTransaction });
  688. expect(users.length).to.equal(1);
  689. await persistentTransaction.commit();
  690. });
  691. }
  692. if (current.dialect.supports.startTransaction.transactionType) {
  693. describe('transaction types', () => {
  694. it('should support default transaction type DEFERRED', async function () {
  695. const t = await this.sequelize.startUnmanagedTransaction({});
  696. await t.rollback();
  697. expect(t.options.transactionType).to.equal('DEFERRED');
  698. });
  699. for (const key of Object.keys(TransactionType)) {
  700. it(`should allow specification of ${key} type`, async function () {
  701. const t = await this.sequelize.startUnmanagedTransaction({
  702. type: key,
  703. });
  704. await t.rollback();
  705. expect(t.options.transactionType).to.equal(TransactionType[key]);
  706. });
  707. }
  708. });
  709. }
  710. if (dialect === 'sqlite3') {
  711. it('automatically retries on SQLITE_BUSY failure', async function () {
  712. const sequelize = await Support.createSingleTransactionalTestSequelizeInstance(
  713. this.sequelize,
  714. );
  715. const User = sequelize.define('User', { username: DataTypes.STRING });
  716. await User.sync({ force: true });
  717. const newTransactionFunc = async function () {
  718. const t = await sequelize.startUnmanagedTransaction({ type: TransactionType.EXCLUSIVE });
  719. await User.create({}, { transaction: t });
  720. return t.commit();
  721. };
  722. await Promise.all([newTransactionFunc(), newTransactionFunc()]);
  723. const users = await User.findAll();
  724. expect(users.length).to.equal(2);
  725. });
  726. it('fails with SQLITE_BUSY when retry.match is changed', async function () {
  727. const sequelize = await Support.createSingleTransactionalTestSequelizeInstance(
  728. this.sequelize,
  729. );
  730. const User = sequelize.define('User', {
  731. id: { type: DataTypes.INTEGER, primaryKey: true },
  732. username: DataTypes.STRING,
  733. });
  734. await User.sync({ force: true });
  735. const newTransactionFunc = async function () {
  736. const t = await sequelize.startUnmanagedTransaction({
  737. type: TransactionType.EXCLUSIVE,
  738. retry: { match: ['NO_MATCH'] },
  739. });
  740. // introduce delay to force the busy state race condition to fail
  741. await delay(2000);
  742. await User.create({ id: null, username: `test ${t.id}` }, { transaction: t });
  743. return t.commit();
  744. };
  745. await expect(Promise.all([newTransactionFunc(), newTransactionFunc()])).to.be.rejectedWith(
  746. 'SQLITE_BUSY: database is locked',
  747. );
  748. });
  749. }
  750. if (current.dialect.supports.lock) {
  751. describe('row locking', () => {
  752. it('supports for update', async function () {
  753. const User = this.sequelize.define('user', {
  754. username: DataTypes.STRING,
  755. awesome: DataTypes.BOOLEAN,
  756. });
  757. const t1Spy = sinon.spy();
  758. const t2Spy = sinon.spy();
  759. await this.sequelize.sync({ force: true });
  760. await User.create({ username: 'jan' });
  761. const t1 = await this.sequelize.startUnmanagedTransaction();
  762. const t1Jan = await User.findOne({
  763. where: {
  764. username: 'jan',
  765. },
  766. lock: t1.LOCK.UPDATE,
  767. transaction: t1,
  768. });
  769. const t2 = await this.sequelize.startUnmanagedTransaction({
  770. isolationLevel: IsolationLevel.READ_COMMITTED,
  771. });
  772. await Promise.all([
  773. (async () => {
  774. await User.findOne({
  775. where: {
  776. username: 'jan',
  777. },
  778. lock: t2.LOCK.UPDATE,
  779. transaction: t2,
  780. });
  781. t2Spy();
  782. await t2.commit();
  783. expect(t2Spy).to.have.been.calledAfter(t1Spy); // Find should not succeed before t1 has committed
  784. })(),
  785. (async () => {
  786. await t1Jan.update(
  787. {
  788. awesome: true,
  789. },
  790. {
  791. transaction: t1,
  792. },
  793. );
  794. t1Spy();
  795. await delay(2000);
  796. return await t1.commit();
  797. })(),
  798. ]);
  799. });
  800. if (current.dialect.supports.skipLocked) {
  801. it('supports for update with skip locked', async function () {
  802. const User = this.sequelize.define('user', {
  803. username: DataTypes.STRING,
  804. awesome: DataTypes.BOOLEAN,
  805. });
  806. await this.sequelize.sync({ force: true });
  807. await Promise.all([User.create({ username: 'jan' }), User.create({ username: 'joe' })]);
  808. const t1 = await this.sequelize.startUnmanagedTransaction();
  809. const results = await User.findAll({
  810. limit: 1,
  811. lock: true,
  812. transaction: t1,
  813. });
  814. const firstUserId = results[0].id;
  815. const t2 = await this.sequelize.startUnmanagedTransaction();
  816. const secondResults = await User.findAll({
  817. limit: 1,
  818. lock: true,
  819. skipLocked: true,
  820. transaction: t2,
  821. });
  822. expect(secondResults[0].id).to.not.equal(firstUserId);
  823. await Promise.all([t1.commit(), t2.commit()]);
  824. });
  825. }
  826. it('fail locking with outer joins', async function () {
  827. const User = this.sequelize.define('User', { username: DataTypes.STRING });
  828. const Task = this.sequelize.define('Task', {
  829. title: DataTypes.STRING,
  830. active: DataTypes.BOOLEAN,
  831. });
  832. User.belongsToMany(Task, { through: 'UserTasks' });
  833. Task.belongsToMany(User, { through: 'UserTasks' });
  834. await this.sequelize.sync({ force: true });
  835. const [john, task1] = await Promise.all([
  836. User.create({ username: 'John' }),
  837. Task.create({ title: 'Get rich', active: false }),
  838. ]);
  839. await john.setTasks([task1]);
  840. await this.sequelize.transaction(t1 => {
  841. if (current.dialect.supports.lockOuterJoinFailure) {
  842. return expect(
  843. User.findOne({
  844. where: {
  845. username: 'John',
  846. },
  847. include: [Task],
  848. lock: t1.LOCK.UPDATE,
  849. transaction: t1,
  850. }),
  851. ).to.be.rejectedWith(
  852. 'FOR UPDATE cannot be applied to the nullable side of an outer join',
  853. );
  854. }
  855. return User.findOne({
  856. where: {
  857. username: 'John',
  858. },
  859. include: [Task],
  860. lock: t1.LOCK.UPDATE,
  861. transaction: t1,
  862. });
  863. });
  864. });
  865. if (current.dialect.supports.lockOf) {
  866. it('supports for update of table', async function () {
  867. const User = this.sequelize.define(
  868. 'User',
  869. { username: DataTypes.STRING },
  870. { tableName: 'Person' },
  871. );
  872. const Task = this.sequelize.define('Task', {
  873. title: DataTypes.STRING,
  874. active: DataTypes.BOOLEAN,
  875. });
  876. User.belongsToMany(Task, { through: 'UserTasks' });
  877. Task.belongsToMany(User, { through: 'UserTasks' });
  878. await this.sequelize.sync({ force: true });
  879. const [john, task1] = await Promise.all([
  880. User.create({ username: 'John' }),
  881. Task.create({ title: 'Get rich', active: false }),
  882. Task.create({ title: 'Die trying', active: false }),
  883. ]);
  884. await john.setTasks([task1]);
  885. await this.sequelize.transaction(async t1 => {
  886. const t1John = await User.findOne({
  887. where: {
  888. username: 'John',
  889. },
  890. include: [Task],
  891. lock: {
  892. level: t1.LOCK.UPDATE,
  893. of: User,
  894. },
  895. transaction: t1,
  896. });
  897. // should not be blocked by the lock of the other transaction
  898. await this.sequelize.transaction(t2 => {
  899. return Task.update(
  900. {
  901. active: true,
  902. },
  903. {
  904. where: {
  905. active: false,
  906. },
  907. transaction: t2,
  908. },
  909. );
  910. });
  911. return t1John.save({
  912. transaction: t1,
  913. });
  914. });
  915. });
  916. }
  917. if (current.dialect.supports.lockKey) {
  918. it('supports for key share', async function () {
  919. const User = this.sequelize.define('user', {
  920. username: DataTypes.STRING,
  921. awesome: DataTypes.BOOLEAN,
  922. });
  923. const t1Spy = sinon.spy();
  924. const t2Spy = sinon.spy();
  925. await this.sequelize.sync({ force: true });
  926. await User.create({ username: 'jan' });
  927. const t1 = await this.sequelize.startUnmanagedTransaction();
  928. const t1Jan = await User.findOne({
  929. where: {
  930. username: 'jan',
  931. },
  932. lock: t1.LOCK.NO_KEY_UPDATE,
  933. transaction: t1,
  934. });
  935. const t2 = await this.sequelize.startUnmanagedTransaction();
  936. await Promise.all([
  937. (async () => {
  938. await User.findOne({
  939. where: {
  940. username: 'jan',
  941. },
  942. lock: t2.LOCK.KEY_SHARE,
  943. transaction: t2,
  944. });
  945. t2Spy();
  946. return await t2.commit();
  947. })(),
  948. (async () => {
  949. await t1Jan.update(
  950. {
  951. awesome: true,
  952. },
  953. {
  954. transaction: t1,
  955. },
  956. );
  957. await delay(2000);
  958. t1Spy();
  959. expect(t1Spy).to.have.been.calledAfter(t2Spy);
  960. return await t1.commit();
  961. })(),
  962. ]);
  963. });
  964. }
  965. it('supports for share (i.e. `SELECT ... LOCK IN SHARE MODE`)', async function () {
  966. const verifySelectLockInShareMode = async () => {
  967. const User = this.sequelize.define(
  968. 'user',
  969. {
  970. username: DataTypes.STRING,
  971. awesome: DataTypes.BOOLEAN,
  972. },
  973. { timestamps: false },
  974. );
  975. await this.sequelize.sync({ force: true });
  976. const { id } = await User.create({ username: 'jan' });
  977. // First, we start a transaction T1 and perform a SELECT with it using the `LOCK.SHARE` mode (setting a shared mode lock on the row).
  978. // This will cause other sessions to be able to read the row but not modify it.
  979. // So, if another transaction tries to update those same rows, it will wait until T1 commits (or rolls back).
  980. // https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
  981. const t1 = await this.sequelize.startUnmanagedTransaction();
  982. await User.findByPk(id, { lock: t1.LOCK.SHARE, transaction: t1 });
  983. // Then we start another transaction T2 and see that it can indeed read the same row.
  984. const t2 = await this.sequelize.startUnmanagedTransaction({
  985. isolationLevel: IsolationLevel.READ_COMMITTED,
  986. });
  987. const t2Jan = await User.findByPk(id, { transaction: t2 });
  988. // Then, we want to see that an attempt to update that row from T2 will be queued until T1 commits.
  989. const executionOrder = [];
  990. const [t2AttemptData, t1AttemptData] = await pSettle([
  991. (async () => {
  992. try {
  993. executionOrder.push('Begin attempt to update via T2');
  994. await t2Jan.update({ awesome: false }, { transaction: t2 });
  995. executionOrder.push('Done updating via T2');
  996. } catch (error) {
  997. executionOrder.push('Failed to update via T2'); // Shouldn't happen
  998. throw error;
  999. }
  1000. await delay(30);
  1001. try {
  1002. executionOrder.push('Attempting to commit T2');
  1003. await t2.commit();
  1004. executionOrder.push('Done committing T2');
  1005. } catch {
  1006. executionOrder.push('Failed to commit T2'); // Shouldn't happen
  1007. }
  1008. })(),
  1009. (async () => {
  1010. await delay(100);
  1011. try {
  1012. executionOrder.push('Begin attempt to read via T1');
  1013. await User.findAll({ transaction: t1 });
  1014. executionOrder.push('Done reading via T1');
  1015. } catch (error) {
  1016. executionOrder.push('Failed to read via T1'); // Shouldn't happen
  1017. throw error;
  1018. }
  1019. await delay(150);
  1020. try {
  1021. executionOrder.push('Attempting to commit T1');
  1022. await t1.commit();
  1023. executionOrder.push('Done committing T1');
  1024. } catch {
  1025. executionOrder.push('Failed to commit T1'); // Shouldn't happen
  1026. }
  1027. })(),
  1028. ]);
  1029. expect(t1AttemptData.isFulfilled).to.be.true;
  1030. expect(t2AttemptData.isFulfilled).to.be.true;
  1031. expect(t1.finished).to.equal('commit');
  1032. expect(t2.finished).to.equal('commit');
  1033. const expectedExecutionOrder = [
  1034. 'Begin attempt to update via T2',
  1035. 'Begin attempt to read via T1', // 100ms after
  1036. 'Done reading via T1', // right after
  1037. 'Attempting to commit T1', // 150ms after
  1038. 'Done committing T1', // right after
  1039. 'Done updating via T2', // right after
  1040. 'Attempting to commit T2', // 30ms after
  1041. 'Done committing T2', // right after
  1042. ];
  1043. // The order things happen in the database must be the one shown above. However, sometimes it can happen that
  1044. // the calls in the JavaScript event loop that are communicating with the database do not match exactly this order.
  1045. // In particular, it is possible that the JS event loop logs `'Done updating via T2'` before logging `'Done committing T1'`,
  1046. // even though the database committed T1 first (and then rushed to complete the pending update query from T2).
  1047. const anotherAcceptableExecutionOrderFromJSPerspective = [
  1048. 'Begin attempt to update via T2',
  1049. 'Begin attempt to read via T1', // 100ms after
  1050. 'Done reading via T1', // right after
  1051. 'Attempting to commit T1', // 150ms after
  1052. 'Done updating via T2', // right after
  1053. 'Done committing T1', // right after
  1054. 'Attempting to commit T2', // 30ms after
  1055. 'Done committing T2', // right after
  1056. ];
  1057. const executionOrderOk = Support.isDeepEqualToOneOf(executionOrder, [
  1058. expectedExecutionOrder,
  1059. anotherAcceptableExecutionOrderFromJSPerspective,
  1060. ]);
  1061. if (!executionOrderOk) {
  1062. throw new Error(`Unexpected execution order: ${executionOrder.join(' > ')}`);
  1063. }
  1064. };
  1065. for (let i = 0; i < 3 * Support.getPoolMax(); i++) {
  1066. await verifySelectLockInShareMode();
  1067. await delay(10);
  1068. }
  1069. });
  1070. });
  1071. }
  1072. });