Skip to content

Commit 5bb4f85

Browse files
authored
Add .retriable to the Neo4jError (#901)
This property should helps users to implement custom retries logics. Along with the property, the static function `Neo4jError.isRetriable(error)` and the function `isRetriableError(error)` were added for saving boilerplates and avoid the error type checking to be spread across the client and driver code base. The internal function `canRetryOn(error)` and the `retry-strategy` file were removed since the logic implemented there was moved to the `error` module replaced by the `isRetriableError(error)` usage.
1 parent 4441857 commit 5bb4f85

File tree

11 files changed

+167
-75
lines changed

11 files changed

+167
-75
lines changed

packages/bolt-connection/test/connection/connection-channel.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ describe('ChannelConnection', () => {
169169
expect(loggerFunction).toHaveBeenCalledWith(
170170
'error',
171171
`${connection} experienced a fatal error caused by Neo4jError: some error ` +
172-
'({"code":"C","name":"Neo4jError"})'
172+
'({"code":"C","name":"Neo4jError","retriable":false})'
173173
)
174174
})
175175
})
@@ -217,7 +217,7 @@ describe('ChannelConnection', () => {
217217
expect(loggerFunction).toHaveBeenCalledWith(
218218
'error',
219219
`${connection} experienced a fatal error caused by Neo4jError: current failure ` +
220-
'({"code":"ongoing","name":"Neo4jError"})'
220+
'({"code":"ongoing","name":"Neo4jError","retriable":false})'
221221
)
222222
})
223223
})

packages/core/src/error.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Neo4jError extends Error {
6262
* Optional error code. Will be populated when error originates in the database.
6363
*/
6464
code: Neo4jErrorCode
65+
retriable: boolean
6566
__proto__: Neo4jError
6667

6768
/**
@@ -76,6 +77,24 @@ class Neo4jError extends Error {
7677
this.__proto__ = Neo4jError.prototype
7778
this.code = code
7879
this.name = 'Neo4jError'
80+
/**
81+
* Indicates if the error is retriable.
82+
* @type {boolean} - true if the error is retriable
83+
*/
84+
this.retriable = _isRetriableCode(code)
85+
}
86+
87+
/**
88+
* Verifies if the given error is retriable.
89+
*
90+
* @param {object|undefined|null} error the error object
91+
* @returns {boolean} true if the error is retriable
92+
*/
93+
static isRetriable(error?: any | null): boolean {
94+
return error !== null &&
95+
error !== undefined &&
96+
error instanceof Neo4jError &&
97+
error.retriable
7998
}
8099
}
81100

@@ -90,8 +109,62 @@ function newError (message: string, code?: Neo4jErrorCode): Neo4jError {
90109
return new Neo4jError(message, code ?? NOT_AVAILABLE)
91110
}
92111

112+
/**
113+
* Verifies if the given error is retriable.
114+
*
115+
* @public
116+
* @param {object|undefined|null} error the error object
117+
* @returns {boolean} true if the error is retriable
118+
*/
119+
const isRetriableError = Neo4jError.isRetriable
120+
121+
/**
122+
* @private
123+
* @param {string} code the error code
124+
* @returns {boolean} true if the error is a retriable error
125+
*/
126+
function _isRetriableCode (code?: Neo4jErrorCode): boolean {
127+
return code === SERVICE_UNAVAILABLE ||
128+
code === SESSION_EXPIRED ||
129+
_isAuthorizationExpired(code) ||
130+
_isRetriableTransientError(code)
131+
}
132+
133+
/**
134+
* @private
135+
* @param {string} code the error to check
136+
* @return {boolean} true if the error is a transient error
137+
*/
138+
function _isRetriableTransientError (code?: Neo4jErrorCode): boolean {
139+
// Retries should not happen when transaction was explicitly terminated by the user.
140+
// Termination of transaction might result in two different error codes depending on where it was
141+
// terminated. These are really client errors but classification on the server is not entirely correct and
142+
// they are classified as transient.
143+
144+
if (code !== undefined && code.indexOf('TransientError') >= 0) {
145+
if (
146+
code === 'Neo.TransientError.Transaction.Terminated' ||
147+
code === 'Neo.TransientError.Transaction.LockClientStopped'
148+
) {
149+
return false
150+
}
151+
return true
152+
}
153+
return false
154+
}
155+
156+
/**
157+
* @private
158+
* @param {string} code the error to check
159+
* @returns {boolean} true if the error is a service unavailable error
160+
*/
161+
function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean {
162+
return code === 'Neo.ClientError.Security.AuthorizationExpired'
163+
}
164+
93165
export {
94166
newError,
167+
isRetriableError,
95168
Neo4jError,
96169
SERVICE_UNAVAILABLE,
97170
SESSION_EXPIRED,

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import {
2121
newError,
2222
Neo4jError,
23+
isRetriableError,
2324
PROTOCOL_ERROR,
2425
SERVICE_UNAVAILABLE,
2526
SESSION_EXPIRED
@@ -93,6 +94,7 @@ const error = {
9394
const forExport = {
9495
newError,
9596
Neo4jError,
97+
isRetriableError,
9698
error,
9799
Integer,
98100
int,
@@ -150,6 +152,7 @@ const forExport = {
150152
export {
151153
newError,
152154
Neo4jError,
155+
isRetriableError,
153156
error,
154157
Integer,
155158
int,

packages/core/src/internal/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import * as logger from './logger'
2929
import * as urlUtil from './url-util'
3030
import * as serverAddress from './server-address'
3131
import * as resolver from './resolver'
32-
import * as retryStrategy from './retry-strategy'
3332

3433
export {
3534
util,
@@ -43,6 +42,5 @@ export {
4342
logger,
4443
urlUtil,
4544
serverAddress,
46-
resolver,
47-
retryStrategy
45+
resolver
4846
}

packages/core/src/internal/retry-strategy.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

packages/core/src/internal/transaction-executor.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
* limitations under the License.
1818
*/
1919

20-
import { newError } from '../error'
20+
import { newError, isRetriableError } from '../error'
2121
import Transaction from '../transaction'
2222
import TransactionPromise from '../transaction-promise'
23-
import { canRetryOn } from './retry-strategy'
23+
2424

2525
const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds
2626
const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds
@@ -107,7 +107,7 @@ export class TransactionExecutor {
107107
): Promise<T> {
108108
const elapsedTimeMs = Date.now() - retryStartTime
109109

110-
if (elapsedTimeMs > this._maxRetryTimeMs || !canRetryOn(error)) {
110+
if (elapsedTimeMs > this._maxRetryTimeMs || !isRetriableError(error)) {
111111
return Promise.reject(error)
112112
}
113113

packages/core/test/error.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
import {
2020
Neo4jError,
21+
isRetriableError,
2122
newError,
2223
PROTOCOL_ERROR,
2324
SERVICE_UNAVAILABLE,
@@ -44,6 +45,18 @@ describe('newError', () => {
4445
})
4546
})
4647

48+
describe('isRetriableError()', () => {
49+
it.each(getRetriableErrorsFixture())
50+
('should return true for error with code %s', error => {
51+
expect(isRetriableError(error)).toBe(true)
52+
})
53+
54+
it.each(getNonRetriableErrorsFixture())
55+
('should return false for error with code %s', error => {
56+
expect(isRetriableError(error)).toBe(false)
57+
})
58+
})
59+
4760
describe('Neo4jError', () => {
4861
test('should have message', () => {
4962
const error = new Neo4jError('message', 'code')
@@ -76,4 +89,64 @@ describe('Neo4jError', () => {
7689
expect(error.__proto__).toEqual(Neo4jError.prototype)
7790
expect(error.constructor).toEqual(Neo4jError)
7891
})
92+
93+
test.each(getRetriableCodes())
94+
('should define retriable as true for error with code %s', code => {
95+
const error = new Neo4jError('message', code)
96+
97+
expect(error.retriable).toBe(true)
98+
})
99+
100+
test.each(getNonRetriableCodes())
101+
('should define retriable as false for error with code %s', code => {
102+
const error = new Neo4jError('message', code)
103+
104+
expect(error.retriable).toBe(false)
105+
})
106+
107+
describe('.isRetriable()', () => {
108+
it.each(getRetriableErrorsFixture())
109+
('should return true for error with code %s', error => {
110+
expect(Neo4jError.isRetriable(error)).toBe(true)
111+
})
112+
113+
it.each(getNonRetriableErrorsFixture())
114+
('should return false for error with code %s', error => {
115+
expect(Neo4jError.isRetriable(error)).toBe(false)
116+
})
117+
})
79118
})
119+
120+
function getRetriableErrorsFixture () {
121+
return getRetriableCodes().map(code => [newError('message', code)])
122+
}
123+
124+
function getNonRetriableErrorsFixture () {
125+
return [
126+
null,
127+
undefined,
128+
'',
129+
'Neo.TransientError.Transaction.DeadlockDetected',
130+
new Error('Neo.ClientError.Security.AuthorizationExpired'),
131+
...getNonRetriableCodes().map(code => [newError('message', code)])
132+
]
133+
}
134+
135+
function getRetriableCodes () {
136+
return [
137+
SERVICE_UNAVAILABLE,
138+
SESSION_EXPIRED,
139+
'Neo.ClientError.Security.AuthorizationExpired',
140+
'Neo.TransientError.Transaction.DeadlockDetected',
141+
'Neo.TransientError.Network.CommunicationError'
142+
]
143+
}
144+
145+
function getNonRetriableCodes () {
146+
return [
147+
'Neo.TransientError.Transaction.Terminated',
148+
'Neo.DatabaseError.General.UnknownError',
149+
'Neo.TransientError.Transaction.LockClientStopped',
150+
'Neo.DatabaseError.General.OutOfMemoryError'
151+
]
152+
}

packages/neo4j-driver-lite/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { logging } from './logging'
2121

2222
import {
2323
Neo4jError,
24+
isRetriableError,
2425
error,
2526
Integer,
2627
inSafeRange,
@@ -403,6 +404,7 @@ const forExport = {
403404
isDateTime,
404405
integer,
405406
Neo4jError,
407+
isRetriableError,
406408
auth,
407409
logging,
408410
types,
@@ -453,6 +455,7 @@ export {
453455
isDateTime,
454456
integer,
455457
Neo4jError,
458+
isRetriableError,
456459
auth,
457460
logging,
458461
types,

packages/neo4j-driver/src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import VERSION from './version'
2121

2222
import {
2323
Neo4jError,
24+
isRetryableError,
2425
error,
2526
Integer,
2627
inSafeRange,
@@ -383,6 +384,7 @@ const forExport = {
383384
isDateTime,
384385
integer,
385386
Neo4jError,
387+
isRetryableError,
386388
auth,
387389
logging,
388390
types,
@@ -405,6 +407,7 @@ export {
405407
isDateTime,
406408
integer,
407409
Neo4jError,
410+
isRetryableError,
408411
auth,
409412
logging,
410413
types,

0 commit comments

Comments
 (0)