Skip to content

Commit cc2fc96

Browse files
committed
TransactionExecutor checks if tx is active
To make sure it does not try to commit transaction that has already been committed or rolled back. This commit also adds a new API function `Transaction#isOpen()` which checks internal transaction state and returns `true` when transaction has not been committed/rolled back and `false` otherwise.
1 parent 20deb99 commit cc2fc96

File tree

5 files changed

+255
-25
lines changed

5 files changed

+255
-25
lines changed

src/v1/internal/transaction-executor.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,21 @@ export default class TransactionExecutor {
8686
const resultPromise = Promise.resolve(transactionWorkResult);
8787

8888
resultPromise.then(result => {
89-
// transaction work returned resolved promise, try to commit the transaction
90-
tx.commit().then(() => {
91-
// transaction was committed, return result to the user
89+
if (tx.isOpen()) {
90+
// transaction work returned resolved promise and transaction has not been committed/rolled back
91+
// try to commit the transaction
92+
tx.commit().then(() => {
93+
// transaction was committed, return result to the user
94+
resolve(result);
95+
}).catch(error => {
96+
// transaction failed to commit, propagate the failure
97+
reject(error);
98+
});
99+
} else {
100+
// transaction work returned resolved promise and transaction is already committed/rolled back
101+
// return the result returned by given transaction work
92102
resolve(result);
93-
}).catch(error => {
94-
// transaction failed to commit, propagate the failure
95-
reject(error);
96-
});
103+
}
97104
}).catch(error => {
98105
// transaction work returned rejected promise, propagate the failure
99106
reject(error);

src/v1/transaction.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class Transaction {
6161
* or with the statement and parameters as separate arguments.
6262
* @param {mixed} statement - Cypher statement to execute
6363
* @param {Object} parameters - Map with parameters to use in statement
64-
* @return {Result} - New Result
64+
* @return {Result} New Result
6565
*/
6666
run(statement, parameters) {
6767
if(typeof statement === 'object' && statement.text) {
@@ -78,7 +78,7 @@ class Transaction {
7878
*
7979
* After committing the transaction can no longer be used.
8080
*
81-
* @returns {Result} - New Result
81+
* @returns {Result} New Result
8282
*/
8383
commit() {
8484
let committed = this._state.commit(this._connectionHolder, new _TransactionStreamObserver(this));
@@ -93,7 +93,7 @@ class Transaction {
9393
*
9494
* After rolling back, the transaction can no longer be used.
9595
*
96-
* @returns {Result} - New Result
96+
* @returns {Result} New Result
9797
*/
9898
rollback() {
9999
let committed = this._state.rollback(this._connectionHolder, new _TransactionStreamObserver(this));
@@ -103,8 +103,16 @@ class Transaction {
103103
return committed.result;
104104
}
105105

106+
/**
107+
* Check if this transaction is active, which means commit and rollback did not happen.
108+
* @return {boolean} <code>true</code> when not committed and not rolled back, <code>false</code> otherwise.
109+
*/
110+
isOpen() {
111+
return this._state == _states.ACTIVE;
112+
}
113+
106114
_onError() {
107-
if (this._state == _states.ACTIVE) {
115+
if (this.isOpen()) {
108116
// attempt to rollback, useful when Transaction#run() failed
109117
return this.rollback().catch(ignoredError => {
110118
// ignore all errors because it is best effort and transaction might already be rolled back

test/internal/transaction-executor.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ class FakeTransaction {
317317
this._commitErrorCode = commitErrorCode;
318318
}
319319

320+
isOpen() {
321+
return true;
322+
}
323+
320324
commit() {
321325
if (this._commitErrorCode) {
322326
return Promise.reject(error(this._commitErrorCode));

test/v1/session.test.js

Lines changed: 188 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ describe('session', () => {
2828

2929
let driver;
3030
let session;
31-
let server;
31+
let serverMetadata;
3232
let originalTimeout;
3333

3434
beforeEach(done => {
3535
driver = neo4j.driver('bolt://localhost', neo4j.auth.basic('neo4j', 'neo4j'));
3636
driver.onCompleted = meta => {
37-
server = meta['server'];
37+
serverMetadata = meta['server'];
3838
};
3939
session = driver.session();
4040
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
@@ -228,11 +228,7 @@ describe('session', () => {
228228
});
229229

230230
it('should expose server info on successful query', done => {
231-
//lazy way of checking the version number
232-
//if server has been set we know it is at least
233-
//3.1 (todo actually parse the version string)
234-
if (!server) {
235-
done();
231+
if (!serverIs31OrLater(done)) {
236232
return;
237233
}
238234

@@ -251,14 +247,10 @@ describe('session', () => {
251247
});
252248

253249
it('should expose execution time information when using 3.1 and onwards', done => {
254-
255-
//lazy way of checking the version number
256-
//if server has been set we know it is at least
257-
//3.1 (todo actually parse the version string)
258-
if (!server) {
259-
done();
250+
if (!serverIs31OrLater(done)) {
260251
return;
261252
}
253+
262254
// Given
263255
const statement = 'UNWIND range(1,10000) AS n RETURN n AS number';
264256
// When & Then
@@ -632,6 +624,184 @@ describe('session', () => {
632624
});
633625
});
634626

627+
it('should commit read transaction', done => {
628+
if (!serverIs31OrLater(done)) {
629+
return;
630+
}
631+
632+
const bookmarkBefore = session.lastBookmark();
633+
const resultPromise = session.readTransaction(tx => tx.run('RETURN 42 AS answer'));
634+
635+
resultPromise.then(result => {
636+
expect(result.records.length).toEqual(1);
637+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
638+
639+
const bookmarkAfter = session.lastBookmark();
640+
verifyBookmark(bookmarkAfter);
641+
expect(bookmarkAfter).not.toEqual(bookmarkBefore);
642+
643+
done();
644+
});
645+
});
646+
647+
it('should commit write transaction', done => {
648+
if (!serverIs31OrLater(done)) {
649+
return;
650+
}
651+
652+
const bookmarkBefore = session.lastBookmark();
653+
const resultPromise = session.writeTransaction(tx => tx.run('CREATE (n:Node {id: 42}) RETURN n.id AS answer'));
654+
655+
resultPromise.then(result => {
656+
expect(result.records.length).toEqual(1);
657+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
658+
expect(result.summary.counters.nodesCreated()).toEqual(1);
659+
660+
const bookmarkAfter = session.lastBookmark();
661+
verifyBookmark(bookmarkAfter);
662+
expect(bookmarkAfter).not.toEqual(bookmarkBefore);
663+
664+
countNodes('Node', 'id', 42).then(count => {
665+
expect(count).toEqual(1);
666+
done();
667+
});
668+
});
669+
});
670+
671+
it('should not commit already committed read transaction', done => {
672+
if (!serverIs31OrLater(done)) {
673+
return;
674+
}
675+
676+
const resultPromise = session.readTransaction(tx => {
677+
return new Promise((resolve, reject) => {
678+
tx.run('RETURN 42 AS answer').then(result => {
679+
tx.commit().then(() => {
680+
resolve({result: result, bookmark: session.lastBookmark()});
681+
}).catch(error => reject(error));
682+
}).catch(error => reject(error));
683+
});
684+
});
685+
686+
resultPromise.then(outcome => {
687+
const bookmark = outcome.bookmark;
688+
const result = outcome.result;
689+
690+
verifyBookmark(bookmark);
691+
expect(session.lastBookmark()).toEqual(bookmark); // expect bookmark to not change
692+
693+
expect(result.records.length).toEqual(1);
694+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
695+
696+
done();
697+
});
698+
});
699+
700+
it('should not commit already committed write transaction', done => {
701+
if (!serverIs31OrLater(done)) {
702+
return;
703+
}
704+
705+
const resultPromise = session.readTransaction(tx => {
706+
return new Promise((resolve, reject) => {
707+
tx.run('CREATE (n:Node {id: 42}) RETURN n.id AS answer').then(result => {
708+
tx.commit().then(() => {
709+
resolve({result: result, bookmark: session.lastBookmark()});
710+
}).catch(error => reject(error));
711+
}).catch(error => reject(error));
712+
});
713+
});
714+
715+
resultPromise.then(outcome => {
716+
const bookmark = outcome.bookmark;
717+
const result = outcome.result;
718+
719+
verifyBookmark(bookmark);
720+
expect(session.lastBookmark()).toEqual(bookmark); // expect bookmark to not change
721+
722+
expect(result.records.length).toEqual(1);
723+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
724+
expect(result.summary.counters.nodesCreated()).toEqual(1);
725+
726+
countNodes('Node', 'id', 42).then(count => {
727+
expect(count).toEqual(1);
728+
done();
729+
});
730+
});
731+
});
732+
733+
it('should not commit rolled back read transaction', done => {
734+
if (!serverIs31OrLater(done)) {
735+
return;
736+
}
737+
738+
const bookmarkBefore = session.lastBookmark();
739+
const resultPromise = session.readTransaction(tx => {
740+
return new Promise((resolve, reject) => {
741+
tx.run('RETURN 42 AS answer').then(result => {
742+
tx.rollback().then(() => {
743+
resolve(result);
744+
}).catch(error => reject(error));
745+
}).catch(error => reject(error));
746+
});
747+
});
748+
749+
resultPromise.then(result => {
750+
expect(result.records.length).toEqual(1);
751+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
752+
expect(session.lastBookmark()).toBe(bookmarkBefore); // expect bookmark to not change
753+
754+
done();
755+
});
756+
});
757+
758+
it('should not commit rolled back write transaction', done => {
759+
if (!serverIs31OrLater(done)) {
760+
return;
761+
}
762+
763+
const bookmarkBefore = session.lastBookmark();
764+
const resultPromise = session.readTransaction(tx => {
765+
return new Promise((resolve, reject) => {
766+
tx.run('CREATE (n:Node {id: 42}) RETURN n.id AS answer').then(result => {
767+
tx.rollback().then(() => {
768+
resolve(result);
769+
}).catch(error => reject(error));
770+
}).catch(error => reject(error));
771+
});
772+
});
773+
774+
resultPromise.then(result => {
775+
expect(result.records.length).toEqual(1);
776+
expect(result.records[0].get('answer').toNumber()).toEqual(42);
777+
expect(result.summary.counters.nodesCreated()).toEqual(1);
778+
expect(session.lastBookmark()).toBe(bookmarkBefore); // expect bookmark to not change
779+
780+
countNodes('Node', 'id', 42).then(count => {
781+
expect(count).toEqual(0);
782+
done();
783+
});
784+
});
785+
});
786+
787+
function serverIs31OrLater(done) {
788+
// lazy way of checking the version number
789+
// if server has been set we know it is at least 3.1
790+
if (!serverMetadata) {
791+
done();
792+
return false;
793+
}
794+
return true;
795+
}
796+
797+
function countNodes(label, propertyKey, propertyValue) {
798+
return new Promise((resolve, reject) => {
799+
session.run(`MATCH (n: ${label} {${propertyKey}: ${propertyValue}}) RETURN count(n) AS count`).then(result => {
800+
resolve(result.records[0].get('count').toNumber());
801+
}).catch(error => reject(error));
802+
});
803+
}
804+
635805
function withQueryInTmpSession(driver, callback) {
636806
const tmpSession = driver.session();
637807
return tmpSession.run('RETURN 1').then(() => {
@@ -653,4 +823,9 @@ describe('session', () => {
653823
const idleConnections = connectionPool._pools[address];
654824
return idleConnections.length;
655825
}
826+
827+
function verifyBookmark(bookmark) {
828+
expect(bookmark).toBeDefined();
829+
expect(bookmark).not.toBeNull();
830+
}
656831
});

test/v1/transaction.test.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import neo4j from "../../lib/v1";
19+
import neo4j from '../../lib/v1';
2020

2121
describe('transaction', () => {
2222

@@ -449,6 +449,42 @@ describe('transaction', () => {
449449
}).catch(console.log);
450450
});
451451

452+
it('should be open when neither committed nor rolled back', () => {
453+
const tx = session.beginTransaction();
454+
expect(tx.isOpen()).toBeTruthy();
455+
});
456+
457+
it('should not be open after commit', done => {
458+
const tx = session.beginTransaction();
459+
460+
tx.run('CREATE ()').then(() => {
461+
tx.commit().then(() => {
462+
expect(tx.isOpen()).toBeFalsy();
463+
done();
464+
});
465+
});
466+
});
467+
468+
it('should not be open after rollback', done => {
469+
const tx = session.beginTransaction();
470+
471+
tx.run('CREATE ()').then(() => {
472+
tx.rollback().then(() => {
473+
expect(tx.isOpen()).toBeFalsy();
474+
done();
475+
});
476+
});
477+
});
478+
479+
it('should not be open after run error', done => {
480+
const tx = session.beginTransaction();
481+
482+
tx.run('RETURN').catch(() => {
483+
expect(tx.isOpen()).toBeFalsy();
484+
done();
485+
});
486+
});
487+
452488
function expectSyntaxError(error) {
453489
expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError');
454490
}

0 commit comments

Comments
 (0)