Skip to content

Commit 46601b3

Browse files
authored
Verify transaction begin before hand-over it to the tx function work (#875)
Calling the transaction work with a broken transaction can waste machine resources with transactions which will fail.
1 parent 13e9238 commit 46601b3

File tree

4 files changed

+76
-24
lines changed

4 files changed

+76
-24
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@
1919

2020
import { newError } from '../error'
2121
import Transaction from '../transaction'
22+
import TransactionPromise from '../transaction-promise'
2223
import { canRetryOn } from './retry-strategy'
2324

2425
const DEFAULT_MAX_RETRY_TIME_MS = 30 * 1000 // 30 seconds
2526
const DEFAULT_INITIAL_RETRY_DELAY_MS = 1000 // 1 seconds
2627
const DEFAULT_RETRY_DELAY_MULTIPLIER = 2.0
2728
const DEFAULT_RETRY_DELAY_JITTER_FACTOR = 0.2
2829

29-
type TransactionCreator = () => Transaction
30+
type TransactionCreator = () => TransactionPromise
3031
type TransactionWork<T> = (tx: Transaction) => T | Promise<T>
3132
type Resolve<T> = (value: T | PromiseLike<T>) => void
3233
type Reject = (value: any) => void
@@ -138,15 +139,15 @@ export class TransactionExecutor {
138139
})
139140
}
140141

141-
_executeTransactionInsidePromise<T>(
142+
async _executeTransactionInsidePromise<T>(
142143
transactionCreator: TransactionCreator,
143144
transactionWork: TransactionWork<T>,
144145
resolve: Resolve<T>,
145146
reject: Reject
146-
): void {
147+
): Promise<void> {
147148
let tx: Transaction
148149
try {
149-
tx = transactionCreator()
150+
tx = await transactionCreator()
150151
} catch (error) {
151152
// failed to create a transaction
152153
reject(error)

packages/core/test/session.test.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ describe('session', () => {
101101
})
102102

103103
it('run should send watermarks to Transaction when fetchsize if defined (writeTransaction)', async () => {
104-
const connection = newFakeConnection()
104+
const connection = mockBeginWithSuccess(newFakeConnection())
105105
const session = newSessionWithConnection(connection, false, 1000)
106106
const status = { functionCalled: false }
107107

@@ -118,7 +118,7 @@ describe('session', () => {
118118
})
119119

120120
it('run should send watermarks to Transaction when fetchsize is fetch all (writeTransaction)', async () => {
121-
const connection = newFakeConnection()
121+
const connection = mockBeginWithSuccess(newFakeConnection())
122122
const session = newSessionWithConnection(connection, false, FETCH_ALL)
123123
const status = { functionCalled: false }
124124

@@ -135,7 +135,7 @@ describe('session', () => {
135135
})
136136

137137
it('run should send watermarks to Transaction when fetchsize if defined (readTransaction)', async () => {
138-
const connection = newFakeConnection()
138+
const connection = mockBeginWithSuccess(newFakeConnection())
139139
const session = newSessionWithConnection(connection, false, 1000)
140140
const status = { functionCalled: false }
141141

@@ -152,7 +152,7 @@ describe('session', () => {
152152
})
153153

154154
it('run should send watermarks to Transaction when fetchsize is fetch all (readTransaction)', async () => {
155-
const connection = newFakeConnection()
155+
const connection = mockBeginWithSuccess(newFakeConnection())
156156
const session = newSessionWithConnection(connection, false, FETCH_ALL)
157157
const status = { functionCalled: false }
158158

@@ -238,17 +238,8 @@ describe('session', () => {
238238
})
239239

240240
it('should resolves a Transaction', async () => {
241-
const connection = newFakeConnection()
242-
const protocol = connection.protocol()
243-
// @ts-ignore
244-
connection.protocol = () => {
245-
return {
246-
...protocol,
247-
beginTransaction: (params: { afterComplete: () => {} }) => {
248-
params.afterComplete()
249-
}
250-
}
251-
}
241+
const connection = mockBeginWithSuccess(newFakeConnection())
242+
252243
const session = newSessionWithConnection(connection, false, 1000)
253244

254245
const tx: Transaction = await session.beginTransaction()
@@ -258,6 +249,20 @@ describe('session', () => {
258249
})
259250
})
260251

252+
function mockBeginWithSuccess(connection: FakeConnection) {
253+
const protocol = connection.protocol()
254+
// @ts-ignore
255+
connection.protocol = () => {
256+
return {
257+
...protocol,
258+
beginTransaction: (params: { afterComplete: () => {}} ) => {
259+
params.afterComplete()
260+
}
261+
}
262+
}
263+
return connection
264+
}
265+
261266
function newSessionWithConnection(
262267
connection: Connection,
263268
beginTx: boolean = true,

packages/neo4j-driver/test/internal/transaction-executor.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,22 @@ describe('#unit TransactionExecutor', () => {
195195
])
196196
}, 30000)
197197

198+
it('should retry when given transaction creator fail on begin once', async () => {
199+
await testRetryWhenTransactionBeginFails([SERVICE_UNAVAILABLE])
200+
}, 30000)
201+
202+
it('should retry when given transaction creator throws on begin many times', async () => {
203+
await testRetryWhenTransactionBeginFails([
204+
SERVICE_UNAVAILABLE,
205+
SESSION_EXPIRED,
206+
TRANSIENT_ERROR_2,
207+
SESSION_EXPIRED,
208+
SERVICE_UNAVAILABLE,
209+
TRANSIENT_ERROR_1,
210+
'Neo.ClientError.Security.AuthorizationExpired'
211+
])
212+
}, 30000)
213+
198214
it('should retry when given transaction work throws once', async () => {
199215
await testRetryWhenTransactionWorkThrows([SERVICE_UNAVAILABLE])
200216
}, 30000)
@@ -294,6 +310,30 @@ describe('#unit TransactionExecutor', () => {
294310
}
295311
}
296312

313+
async function testRetryWhenTransactionBeginFails (errorCodes) {
314+
const fakeSetTimeout = setTimeoutMock.install()
315+
try {
316+
const executor = new TransactionExecutor()
317+
const transactionCreator = throwingTransactionCreatorOnBegin(
318+
errorCodes,
319+
new FakeTransaction()
320+
)
321+
const usedTransactions = []
322+
323+
const result = await executor.execute(transactionCreator, tx => {
324+
expect(tx).toBeDefined()
325+
usedTransactions.push(tx)
326+
return Promise.resolve(42)
327+
})
328+
329+
expect(usedTransactions.length).toEqual(1)
330+
expect(result).toEqual(42)
331+
verifyRetryDelays(fakeSetTimeout, errorCodes.length)
332+
} finally {
333+
fakeSetTimeout.uninstall()
334+
}
335+
}
336+
297337
async function testRetryWhenTransactionWorkReturnsRejectedPromise (
298338
errorCodes
299339
) {
@@ -451,6 +491,17 @@ function throwingTransactionCreator (errorCodes, result) {
451491
}
452492
}
453493

494+
function throwingTransactionCreatorOnBegin (errorCodes, result) {
495+
const remainingErrorCodes = errorCodes.slice().reverse()
496+
return () => {
497+
if (remainingErrorCodes.length === 0) {
498+
return Promise.resolve(result)
499+
}
500+
const errorCode = remainingErrorCodes.pop()
501+
return Promise.reject(error(errorCode))
502+
}
503+
}
504+
454505
function throwingTransactionWork (errorCodes, result) {
455506
const remainingErrorCodes = errorCodes.slice().reverse()
456507
return () => {

packages/testkit-backend/src/skipped-tests/common.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ const skippedTests = [
77
ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments_multi_query'),
88
ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments_multi_query_nested')
99
),
10-
skip(
11-
'Eager verification not implemented for tx functions',
12-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_eager_begin_on_tx_func_run_with_error_on_begin'),
13-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_eager_begin_on_tx_func_run_with_disconnect_on_begin')
14-
),
1510
skip(
1611
'Fail while enable Temporary::ResultKeys',
1712
ifEquals('neo4j.test_bookmarks.TestBookmarks.test_can_pass_bookmark_into_next_session'),

0 commit comments

Comments
 (0)