Skip to content

Commit e26619c

Browse files
authored
Avoid Results try to pull more records after transaction fail (#1145)
When two or more queries are running concurrent in the same transaction, a failure happenning in any of the queries implies in a broken transaction and any new message to the server should result in a error since the server state will be in failure. Notifying all open results in the transaction about any error avoids any new message to be send by these objects.
1 parent 3602321 commit e26619c

File tree

7 files changed

+89
-20
lines changed

7 files changed

+89
-20
lines changed

packages/core/src/transaction.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class Transaction {
5959
private readonly _onError: (error: Error) => Promise<Connection | null>
6060
private readonly _onComplete: (metadata: any, previousBookmarks?: Bookmarks) => void
6161
private readonly _fetchSize: number
62-
private readonly _results: any[]
62+
private readonly _results: Result[]
6363
private readonly _impersonatedUser?: string
6464
private readonly _lowRecordWatermak: number
6565
private readonly _highRecordWatermark: number
@@ -291,12 +291,22 @@ class Transaction {
291291
}
292292
}
293293

294-
_onErrorCallback (): Promise<Connection | null> {
294+
_onErrorCallback (error: Error): Promise<Connection | null> {
295295
// error will be "acknowledged" by sending a RESET message
296296
// database will then forget about this transaction and cleanup all corresponding resources
297297
// it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it
298298
this._state = _states.FAILED
299299
this._onClose()
300+
this._results.forEach(result => {
301+
if (result.isOpen()) {
302+
// @ts-expect-error
303+
result._streamObserverPromise
304+
.then(resultStreamObserver => resultStreamObserver.onError(error))
305+
// Nothing to do since we don't have a observer to notify the error
306+
// the result will be already broke in other ways.
307+
.catch((_: Error) => {})
308+
}
309+
})
300310

301311
// release connection back to the pool
302312
return this._connectionHolder.releaseConnection()

packages/core/test/transaction.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
*/
1919

2020
import { ConnectionProvider, newError, NotificationFilter, Transaction, TransactionPromise } from '../src'
21-
import { BeginTransactionConfig } from '../src/connection'
21+
import { BeginTransactionConfig, RunQueryConfig } from '../src/connection'
2222
import { Bookmarks } from '../src/internal/bookmarks'
2323
import { ConnectionHolder } from '../src/internal/connection-holder'
2424
import { Logger } from '../src/internal/logger'
2525
import { TxConfig } from '../src/internal/tx-config'
2626
import FakeConnection from './utils/connection.fake'
2727
import { validNotificationFilters } from './utils/notification-filters.fixtures'
28+
import ResultStreamObserverMock from './utils/result-stream-observer.mock'
2829

2930
testTx('Transaction', newRegularTransaction)
3031

@@ -392,6 +393,48 @@ function testTx<T extends Transaction> (transactionName: string, newTransaction:
392393
})
393394
)
394395
})
396+
397+
it('should cascade errors in a result to other open results', async () => {
398+
const connection = newFakeConnection()
399+
const expectedError = newError('Something right is not wrong, wut?')
400+
const tx = newTransaction({
401+
connection,
402+
fetchSize: 1000,
403+
highRecordWatermark: 700,
404+
lowRecordWatermark: 300
405+
})
406+
407+
const observers: ResultStreamObserverMock[] = []
408+
409+
jest.spyOn(connection, 'run')
410+
.mockImplementation((query: string, parameters: any, config: RunQueryConfig) => {
411+
const steamObserver = new ResultStreamObserverMock({
412+
beforeError: config.beforeError,
413+
afterComplete: config.afterComplete
414+
})
415+
if (query === 'should error') {
416+
steamObserver.onError(expectedError)
417+
} else if (query === 'finished result') {
418+
steamObserver.onCompleted({})
419+
}
420+
421+
observers.push(steamObserver)
422+
423+
return steamObserver
424+
})
425+
426+
tx._begin(async () => Bookmarks.empty(), TxConfig.empty())
427+
428+
const nonConsumedResult = tx.run('RETURN 1')
429+
await tx.run('finished result')
430+
const brokenResult = tx.run('should error')
431+
432+
await expect(brokenResult).rejects.toThrowError(expectedError)
433+
await expect(nonConsumedResult).rejects.toThrowError(expectedError)
434+
expect(observers[0].error).toEqual(expectedError)
435+
expect(observers[1].error).not.toBeDefined()
436+
expect(observers[2].error).toEqual(expectedError)
437+
})
395438
})
396439

397440
describe('.close()', () => {

packages/core/test/utils/result-stream-observer.mock.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ export default class ResultStreamObserverMock implements observer.ResultStreamOb
2828
private readonly _observers: ResultObserver[]
2929
private _error?: Error
3030
private _meta?: any
31+
private readonly _beforeError?: (error: Error) => void
32+
private readonly _afterComplete?: (metadata: any) => void
3133

32-
constructor () {
34+
constructor (observers?: { beforeError?: (error: Error) => void, afterComplete?: (metadata: any) => void }) {
3335
this._queuedRecords = []
3436
this._observers = []
37+
this._beforeError = observers?.beforeError
38+
this._afterComplete = observers?.afterComplete
39+
}
40+
41+
get error (): Error | undefined {
42+
return this._error
3543
}
3644

3745
cancel (): void {}
@@ -88,6 +96,9 @@ export default class ResultStreamObserverMock implements observer.ResultStreamOb
8896

8997
onError (error: Error): void {
9098
this._error = error
99+
if (this._beforeError != null) {
100+
this._beforeError(error)
101+
}
91102
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
92103
this._observers.filter(o => o.onError).forEach(o => o.onError!(error))
93104
}
@@ -98,6 +109,10 @@ export default class ResultStreamObserverMock implements observer.ResultStreamOb
98109
.filter(o => o.onCompleted)
99110
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
100111
.forEach(o => o.onCompleted!(meta))
112+
113+
if (this._afterComplete != null) {
114+
this._afterComplete(meta)
115+
}
101116
}
102117

103118
pause (): void {

packages/neo4j-driver-deno/lib/core/transaction.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class Transaction {
5959
private readonly _onError: (error: Error) => Promise<Connection | null>
6060
private readonly _onComplete: (metadata: any, previousBookmarks?: Bookmarks) => void
6161
private readonly _fetchSize: number
62-
private readonly _results: any[]
62+
private readonly _results: Result[]
6363
private readonly _impersonatedUser?: string
6464
private readonly _lowRecordWatermak: number
6565
private readonly _highRecordWatermark: number
@@ -291,12 +291,22 @@ class Transaction {
291291
}
292292
}
293293

294-
_onErrorCallback (): Promise<Connection | null> {
294+
_onErrorCallback (error: Error): Promise<Connection | null> {
295295
// error will be "acknowledged" by sending a RESET message
296296
// database will then forget about this transaction and cleanup all corresponding resources
297297
// it is thus safe to move this transaction to a FAILED state and disallow any further interactions with it
298298
this._state = _states.FAILED
299299
this._onClose()
300+
this._results.forEach(result => {
301+
if (result.isOpen()) {
302+
// @ts-expect-error
303+
result._streamObserverPromise
304+
.then(resultStreamObserver => resultStreamObserver.onError(error))
305+
// Nothing to do since we don't have a observer to notify the error
306+
// the result will be already broke in other ways.
307+
.catch((_: Error) => {})
308+
}
309+
})
300310

301311
// release connection back to the pool
302312
return this._connectionHolder.releaseConnection()

packages/testkit-backend/deno/controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function newWire(context: Context, reply: Reply): Wire {
3434
name: "DriverError",
3535
data: {
3636
id,
37+
errorType: e.name,
3738
msg: e.message,
3839
// @ts-ignore Code Neo4jError does have code
3940
code: e.code,

packages/testkit-backend/src/controller/local.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default class LocalController extends Controller {
6969
const id = this._contexts.get(contextId).addError(e)
7070
this._writeResponse(contextId, newResponse('DriverError', {
7171
id,
72+
errorType: e.name,
7273
msg: e.message,
7374
code: e.code,
7475
retryable: e.retriable

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

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
import skip, { ifEquals, ifEndsWith, endsWith, ifStartsWith, startsWith, not, or } from './skip.js'
22

33
const skippedTests = [
4-
skip(
5-
"Fixme: transactions don't prevent further actions after failure.",
6-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_discard_after_tx_termination_on_run'),
7-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_pull_after_tx_termination_on_pull'),
8-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_pull_after_tx_termination_on_run'),
9-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_run_after_tx_termination_on_run'),
10-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_run_after_tx_termination_on_pull'),
11-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_rollback_message_after_tx_termination'),
12-
ifEquals('stub.tx_run.test_tx_run.TestTxRun.test_should_prevent_commit_after_tx_termination'),
13-
ifEquals('stub.configuration_hints.test_connection_recv_timeout_seconds.TestDirectConnectionRecvTimeout.test_timeout_unmanaged_tx_should_fail_subsequent_usage_after_timeout')
14-
),
154
skip(
165
'Driver does not return offset for old DateTime implementations',
176
ifStartsWith('stub.types.test_temporal_types.TestTemporalTypes')
@@ -28,7 +17,7 @@ const skippedTests = [
2817
ifEquals('neo4j.datatypes.test_temporal_types.TestDataTypes.test_duration_components')
2918
),
3019
skip(
31-
'Testkit implemenation is deprecated',
20+
'Testkit implementation is deprecated',
3221
ifEquals('stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_node_only_element_id'),
3322
ifEquals('stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_path_element_ids_with_only_string'),
3423
ifEquals('stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_rel_only_element_id')
@@ -38,7 +27,7 @@ const skippedTests = [
3827
ifEndsWith('neo4j.test_summary.TestSummary.test_protocol_version_information')
3928
),
4029
skip(
41-
'Handle qid omission optmization can cause issues in nested queries',
30+
'Handle qid omission optimization can cause issues in nested queries',
4231
ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments'),
4332
ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments_multi_query'),
4433
ifEquals('stub.optimizations.test_optimizations.TestOptimizations.test_uses_implicit_default_arguments_multi_query_nested')
@@ -81,7 +70,7 @@ const skippedTests = [
8170
ifEquals('stub.iteration.test_iteration_session_run.TestIterationSessionRun.test_nested')
8271
),
8372
skip(
84-
'Nested calls does not garauntee order in the records pulling',
73+
'Nested calls does not guarantee order in the records pulling',
8574
ifEquals('stub.iteration.test_iteration_tx_run.TestIterationTxRun.test_nested')
8675
),
8776
skip(

0 commit comments

Comments
 (0)