Skip to content

Commit b3a0394

Browse files
authored
Transaction functions should retry on AuthorizationExpired error (#730)
`Neo.ClientError.Security.AuthorizationExpired` is a transient error should be object of retry in transaction function scenarios since it could be resolve by get another connection.
1 parent 646339f commit b3a0394

File tree

7 files changed

+92
-75
lines changed

7 files changed

+92
-75
lines changed

core/src/internal/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import * as logger from './logger'
3030
import * as urlUtil from './url-util'
3131
import * as serverAddress from './server-address'
3232
import * as resolver from './resolver'
33+
import * as retryStrategy from './retry-strategy'
3334

3435
export {
3536
util,
@@ -44,5 +45,6 @@ export {
4445
logger,
4546
urlUtil,
4647
serverAddress,
47-
resolver
48+
resolver,
49+
retryStrategy
4850
}

core/src/internal/retry-strategy.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { Neo4jError, SERVICE_UNAVAILABLE, SESSION_EXPIRED } from '../error'
21+
22+
/**
23+
* Verified error and returns if it could be retried or not
24+
*
25+
* @param _error The error
26+
* @returns If the transaction could be retried.
27+
*/
28+
function canRetryOn (_error: any): boolean {
29+
return (
30+
_error &&
31+
_error instanceof Neo4jError &&
32+
_error.code &&
33+
(_error.code === SERVICE_UNAVAILABLE ||
34+
_error.code === SESSION_EXPIRED ||
35+
_isAuthorizationExpired(_error) ||
36+
_isTransientError(_error))
37+
)
38+
}
39+
40+
function _isTransientError (error: Neo4jError): boolean {
41+
// Retries should not happen when transaction was explicitly terminated by the user.
42+
// Termination of transaction might result in two different error codes depending on where it was
43+
// terminated. These are really client errors but classification on the server is not entirely correct and
44+
// they are classified as transient.
45+
46+
const code = error.code
47+
if (code.indexOf('TransientError') >= 0) {
48+
if (
49+
code === 'Neo.TransientError.Transaction.Terminated' ||
50+
code === 'Neo.TransientError.Transaction.LockClientStopped'
51+
) {
52+
return false
53+
}
54+
return true
55+
}
56+
return false
57+
}
58+
59+
function _isAuthorizationExpired (error: Neo4jError): boolean {
60+
return error.code === 'Neo.ClientError.Security.AuthorizationExpired'
61+
}
62+
63+
export { canRetryOn }

core/src/internal/transaction-executor.ts

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

20-
import {
21-
newError,
22-
Neo4jError,
23-
SERVICE_UNAVAILABLE,
24-
SESSION_EXPIRED
25-
} from '../error'
20+
import { newError } from '../error'
2621
import Transaction from '../transaction'
22+
import { canRetryOn } from './retry-strategy'
2723

2824
const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds
2925
const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds
@@ -109,10 +105,7 @@ export class TransactionExecutor {
109105
): Promise<T> {
110106
const elapsedTimeMs = Date.now() - retryStartTime
111107

112-
if (
113-
elapsedTimeMs > this._maxRetryTimeMs ||
114-
!TransactionExecutor._canRetryOn(error)
115-
) {
108+
if (elapsedTimeMs > this._maxRetryTimeMs || !canRetryOn(error)) {
116109
return Promise.reject(error)
117110
}
118111

@@ -229,34 +222,6 @@ export class TransactionExecutor {
229222
return Math.random() * (max - min) + min
230223
}
231224

232-
static _canRetryOn(error: any): boolean {
233-
return (error &&
234-
error instanceof Neo4jError &&
235-
error.code &&
236-
(error.code === SERVICE_UNAVAILABLE ||
237-
error.code === SESSION_EXPIRED ||
238-
this._isTransientError(error))) as boolean
239-
}
240-
241-
static _isTransientError(error: Neo4jError): boolean {
242-
// Retries should not happen when transaction was explicitly terminated by the user.
243-
// Termination of transaction might result in two different error codes depending on where it was
244-
// terminated. These are really client errors but classification on the server is not entirely correct and
245-
// they are classified as transient.
246-
247-
const code = error.code
248-
if (code.indexOf('TransientError') >= 0) {
249-
if (
250-
code === 'Neo.TransientError.Transaction.Terminated' ||
251-
code === 'Neo.TransientError.Transaction.LockClientStopped'
252-
) {
253-
return false
254-
}
255-
return true
256-
}
257-
return false
258-
}
259-
260225
_verifyAfterConstruction() {
261226
if (this._maxRetryTimeMs < 0) {
262227
throw newError('Max retry time should be >= 0: ' + this._maxRetryTimeMs)

src/internal/retry-logic-rx.js

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const {
2525
logger: {
2626
// eslint-disable-next-line no-unused-vars
2727
Logger
28-
}
28+
},
29+
retryStrategy: { canRetryOn }
2930
} = internal
3031

3132
const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error
@@ -80,7 +81,7 @@ export default class RxRetryLogic {
8081

8182
return failedWork.pipe(
8283
flatMap(err => {
83-
if (!RxRetryLogic._canRetryOn(err)) {
84+
if (!canRetryOn(err)) {
8485
return throwError(err)
8586
}
8687

@@ -119,35 +120,6 @@ export default class RxRetryLogic {
119120
const jitter = delay * this._delayJitter
120121
return delay - jitter + 2 * jitter * Math.random()
121122
}
122-
123-
static _canRetryOn (error) {
124-
return (
125-
error &&
126-
error.code &&
127-
(error.code === SERVICE_UNAVAILABLE ||
128-
error.code === SESSION_EXPIRED ||
129-
this._isTransientError(error))
130-
)
131-
}
132-
133-
static _isTransientError (error) {
134-
// Retries should not happen when transaction was explicitly terminated by the user.
135-
// Termination of transaction might result in two different error codes depending on where it was
136-
// terminated. These are really client errors but classification on the server is not entirely correct and
137-
// they are classified as transient.
138-
139-
const code = error.code
140-
if (code.indexOf('TransientError') >= 0) {
141-
if (
142-
code === 'Neo.TransientError.Transaction.Terminated' ||
143-
code === 'Neo.TransientError.Transaction.LockClientStopped'
144-
) {
145-
return false
146-
}
147-
return true
148-
}
149-
return false
150-
}
151123
}
152124

153125
function valueOrDefault (value, defaultValue) {

test/internal/retry-logic-rx.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ describe('#unit-rx retrylogic', () => {
109109
verifyRetry(newError('service unavailable', SERVICE_UNAVAILABLE))
110110
})
111111

112+
it('a Neo.ClientError.Security.AuthorizationExpired error', () => {
113+
verifyRetry(
114+
newError(
115+
'service unavailable',
116+
'Neo.ClientError.Security.AuthorizationExpired'
117+
)
118+
)
119+
})
120+
112121
function verifyRetry (error) {
113122
scheduler.run(helpers => {
114123
const retryLogic = new RxRetryLogic({ maxRetryTimeout: 5000 })

test/internal/transaction-executor.test.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ describe('#unit TransactionExecutor', () => {
190190
TRANSIENT_ERROR_2,
191191
SESSION_EXPIRED,
192192
SERVICE_UNAVAILABLE,
193-
TRANSIENT_ERROR_1
193+
TRANSIENT_ERROR_1,
194+
'Neo.ClientError.Security.AuthorizationExpired'
194195
])
195196
}, 30000)
196197

@@ -203,7 +204,8 @@ describe('#unit TransactionExecutor', () => {
203204
SERVICE_UNAVAILABLE,
204205
TRANSIENT_ERROR_2,
205206
TRANSIENT_ERROR_2,
206-
SESSION_EXPIRED
207+
SESSION_EXPIRED,
208+
'Neo.ClientError.Security.AuthorizationExpired'
207209
])
208210
}, 30000)
209211

@@ -214,7 +216,8 @@ describe('#unit TransactionExecutor', () => {
214216
TRANSIENT_ERROR_2,
215217
SESSION_EXPIRED,
216218
TRANSIENT_ERROR_1,
217-
SESSION_EXPIRED
219+
SESSION_EXPIRED,
220+
'Neo.ClientError.Security.AuthorizationExpired'
218221
])
219222
}, 30000)
220223

@@ -230,7 +233,8 @@ describe('#unit TransactionExecutor', () => {
230233
TRANSIENT_ERROR_1,
231234
SESSION_EXPIRED,
232235
SERVICE_UNAVAILABLE,
233-
TRANSIENT_ERROR_2
236+
TRANSIENT_ERROR_2,
237+
'Neo.ClientError.Security.AuthorizationExpired'
234238
])
235239
}, 30000)
236240

@@ -239,6 +243,7 @@ describe('#unit TransactionExecutor', () => {
239243
[
240244
SERVICE_UNAVAILABLE,
241245
TRANSIENT_ERROR_2,
246+
'Neo.ClientError.Security.AuthorizationExpired',
242247
SESSION_EXPIRED,
243248
SESSION_EXPIRED
244249
],

testkit-backend/src/skipped-tests.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const skippedTests = [
3333
),
3434
ifEndsWith(
3535
'test_should_retry_write_until_success_with_leader_change_using_tx_function'
36-
)
36+
),
37+
ifEndsWith('test_should_retry_on_auth_expired_on_begin_using_tx_function')
3738
),
3839
skip(
3940
'Driver is creating a dedicated connection to check the MultiDBSupport',

0 commit comments

Comments
 (0)