Skip to content

Commit c31eab6

Browse files
committed
Do not retry transaction when it is terminated
This commit makes transaction functions `Session.readTransaction()` and `Session#writeTransaction()` not retry when executed transaction was explicitly terminated by the user. Such termination can happen when `Session#close()` is called or `killQuery` procedure is used. Transaction termination can result in two different status codes on the server, both classified as transient errors which is not entirely correct. So we need additional status code checks in the retying code.
1 parent faca37c commit c31eab6

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

src/v1/internal/transaction-executor.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,23 @@ export default class TransactionExecutor {
122122
return error && error.code &&
123123
(error.code === SERVICE_UNAVAILABLE ||
124124
error.code === SESSION_EXPIRED ||
125-
error.code.indexOf('TransientError') >= 0);
125+
this._isTransientError(error));
126+
}
127+
128+
static _isTransientError(error) {
129+
// Retries should not happen when transaction was explicitly terminated by the user.
130+
// Termination of transaction might result in two different error codes depending on where it was
131+
// terminated. These are really client errors but classification on the server is not entirely correct and
132+
// they are classified as transient.
133+
134+
const code = error.code;
135+
if (code.indexOf('TransientError') >= 0) {
136+
if (code === 'Neo.TransientError.Transaction.Terminated' || code === 'Neo.TransientError.Transaction.LockClientStopped') {
137+
return false;
138+
}
139+
return true;
140+
}
141+
return false;
126142
}
127143

128144
_verifyAfterConstruction() {

test/internal/transaction-executor.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import {hijackNextDateNowCall, setTimeoutMock} from './timers-util';
2424
const TRANSIENT_ERROR_1 = 'Neo.TransientError.Transaction.DeadlockDetected';
2525
const TRANSIENT_ERROR_2 = 'Neo.TransientError.Network.CommunicationError';
2626
const UNKNOWN_ERROR = 'Neo.DatabaseError.General.UnknownError';
27+
const TX_TERMINATED_ERROR = 'Neo.TransientError.Transaction.Terminated';
28+
const LOCKS_TERMINATED_ERROR = 'Neo.TransientError.Transaction.LockClientStopped';
2729
const OOM_ERROR = 'Neo.DatabaseError.General.OutOfMemoryError';
2830

2931
describe('TransactionExecutor', () => {
@@ -62,6 +64,14 @@ describe('TransactionExecutor', () => {
6264
testNoRetryOnUnknownError([UNKNOWN_ERROR], 1, done);
6365
});
6466

67+
it('should not retry when transaction work returns promise rejected with transaction termination error', done => {
68+
testNoRetryOnUnknownError([TX_TERMINATED_ERROR], 1, done);
69+
});
70+
71+
it('should not retry when transaction work returns promise rejected with locks termination error', done => {
72+
testNoRetryOnUnknownError([LOCKS_TERMINATED_ERROR], 1, done);
73+
});
74+
6575
it('should stop retrying when time expires', done => {
6676
const executor = new TransactionExecutor();
6777
let workInvocationCounter = 0;

test/v1/session.test.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,120 @@ describe('session', () => {
796796
});
797797
});
798798

799+
it('should interrupt query waiting on a lock when closed', done => {
800+
if (!serverIs31OrLater(done)) {
801+
// locks are transaction termination aware by default only in 3.1+
802+
return;
803+
}
804+
805+
session.run('CREATE ()').then(() => {
806+
session.close(() => {
807+
const session1 = driver.session();
808+
const session2 = driver.session();
809+
const tx1 = session1.beginTransaction();
810+
811+
tx1.run('MATCH (n) SET n.id = 1 RETURN 42 AS answer').then(result => {
812+
// node is now locked by tx1
813+
expect(result.records.length).toEqual(1);
814+
expect(result.records[0].get(0).toNumber()).toEqual(42);
815+
816+
// this query should get stuck waiting for the lock
817+
session2.run('MATCH (n) SET n.id = 2 RETURN 42 AS answer').catch(error => {
818+
expectTransactionTerminatedError(error);
819+
tx1.commit().then(() => {
820+
readAllNodeIds().then(ids => {
821+
expect(ids).toEqual([1]);
822+
done();
823+
});
824+
});
825+
});
826+
827+
setTimeout(() => {
828+
// close session after a while
829+
session2.close();
830+
}, 1000);
831+
});
832+
});
833+
});
834+
});
835+
836+
it('should interrupt transaction waiting on a lock when closed', done => {
837+
if (!serverIs31OrLater(done)) {
838+
// locks are transaction termination aware by default only in 3.1+
839+
return;
840+
}
841+
842+
session.run('CREATE ()').then(() => {
843+
session.close(() => {
844+
const session1 = driver.session();
845+
const session2 = driver.session();
846+
const tx1 = session1.beginTransaction();
847+
const tx2 = session2.beginTransaction();
848+
849+
tx1.run('MATCH (n) SET n.id = 1 RETURN 42 AS answer').then(result => {
850+
// node is now locked by tx1
851+
expect(result.records.length).toEqual(1);
852+
expect(result.records[0].get(0).toNumber()).toEqual(42);
853+
854+
// this query should get stuck waiting for the lock
855+
tx2.run('MATCH (n) SET n.id = 2 RETURN 42 AS answer').catch(error => {
856+
expectTransactionTerminatedError(error);
857+
tx1.commit().then(() => {
858+
readAllNodeIds().then(ids => {
859+
expect(ids).toEqual([1]);
860+
done();
861+
});
862+
});
863+
});
864+
865+
setTimeout(() => {
866+
// close session after a while
867+
session2.close();
868+
}, 1000);
869+
});
870+
});
871+
});
872+
});
873+
874+
it('should interrupt transaction function waiting on a lock when closed', done => {
875+
if (!serverIs31OrLater(done)) {
876+
// locks are transaction termination aware by default only in 3.1+
877+
return;
878+
}
879+
880+
session.run('CREATE ()').then(() => {
881+
session.close(() => {
882+
const session1 = driver.session();
883+
const session2 = driver.session();
884+
const tx1 = session1.beginTransaction();
885+
886+
tx1.run('MATCH (n) SET n.id = 1 RETURN 42 AS answer').then(result => {
887+
// node is now locked by tx1
888+
expect(result.records.length).toEqual(1);
889+
expect(result.records[0].get(0).toNumber()).toEqual(42);
890+
891+
session2.writeTransaction(tx2 => {
892+
// this query should get stuck waiting for the lock
893+
return tx2.run('MATCH (n) SET n.id = 2 RETURN 42 AS answer').catch(error => {
894+
expectTransactionTerminatedError(error);
895+
tx1.commit().then(() => {
896+
readAllNodeIds().then(ids => {
897+
expect(ids).toEqual([1]);
898+
done();
899+
});
900+
});
901+
});
902+
});
903+
904+
setTimeout(() => {
905+
// close session after a while
906+
session2.close();
907+
}, 1000);
908+
});
909+
});
910+
});
911+
});
912+
799913
function serverIs31OrLater(done) {
800914
// lazy way of checking the version number
801915
// if server has been set we know it is at least 3.1
@@ -840,4 +954,21 @@ describe('session', () => {
840954
expect(bookmark).toBeDefined();
841955
expect(bookmark).not.toBeNull();
842956
}
957+
958+
function expectTransactionTerminatedError(error) {
959+
expect(error.message.toLowerCase().indexOf('transaction terminated') >= 0).toBeTruthy();
960+
}
961+
962+
function readAllNodeIds() {
963+
return new Promise((resolve, reject) => {
964+
const session = driver.session();
965+
session.run('MATCH (n) RETURN n.id').then(result => {
966+
const ids = result.records.map(record => record.get(0).toNumber());
967+
session.close();
968+
resolve(ids);
969+
}).catch(error => {
970+
reject(error);
971+
});
972+
});
973+
}
843974
});

0 commit comments

Comments
 (0)