Skip to content

Add support for error cause in Neo4jError and Routing errors #960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
this.forgetWriter(address, database || DEFAULT_DB_NAME)
return newError(
'No longer possible to write to server at ' + address,
SESSION_EXPIRED
SESSION_EXPIRED,
error
)
}

Expand Down Expand Up @@ -338,7 +339,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
) {
// we start with seed router, no routers were probed before
const seenRouters = []
let newRoutingTable = await this._fetchRoutingTableUsingSeedRouter(
let [newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter(
seenRouters,
this._seedRouter,
currentRoutingTable,
Expand All @@ -350,18 +351,21 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
this._useSeedRouter = false
} else {
// seed router did not return a valid routing table - try to use other known routers
newRoutingTable = await this._fetchRoutingTableUsingKnownRouters(
const [newRoutingTable2, error2] = await this._fetchRoutingTableUsingKnownRouters(
knownRouters,
currentRoutingTable,
bookmarks,
impersonatedUser
)
newRoutingTable = newRoutingTable2
error = error2 || error
}

return await this._applyRoutingTableIfPossible(
currentRoutingTable,
newRoutingTable,
onDatabaseNameResolved
onDatabaseNameResolved,
error
)
}

Expand All @@ -372,7 +376,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
impersonatedUser,
onDatabaseNameResolved
) {
let newRoutingTable = await this._fetchRoutingTableUsingKnownRouters(
let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters(
knownRouters,
currentRoutingTable,
bookmarks,
Expand All @@ -381,7 +385,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider

if (!newRoutingTable) {
// none of the known routers returned a valid routing table - try to use seed router address for rediscovery
newRoutingTable = await this._fetchRoutingTableUsingSeedRouter(
[newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter(
knownRouters,
this._seedRouter,
currentRoutingTable,
Expand All @@ -393,7 +397,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
return await this._applyRoutingTableIfPossible(
currentRoutingTable,
newRoutingTable,
onDatabaseNameResolved
onDatabaseNameResolved,
error
)
}

Expand All @@ -403,7 +408,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
bookmarks,
impersonatedUser
) {
const newRoutingTable = await this._fetchRoutingTable(
const [newRoutingTable, error] = await this._fetchRoutingTable(
knownRouters,
currentRoutingTable,
bookmarks,
Expand All @@ -412,7 +417,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider

if (newRoutingTable) {
// one of the known routers returned a valid routing table - use it
return newRoutingTable
return [newRoutingTable, null]
}

// returned routing table was undefined, this means a connection error happened and the last known
Expand All @@ -424,7 +429,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
lastRouterIndex
)

return null
return [null, error]
}

async _fetchRoutingTableUsingSeedRouter (
Expand Down Expand Up @@ -453,14 +458,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
return [].concat.apply([], dnsResolvedAddresses)
}

_fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) {
async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) {
return routerAddresses.reduce(
async (refreshedTablePromise, currentRouter, currentIndex) => {
const newRoutingTable = await refreshedTablePromise
const [newRoutingTable] = await refreshedTablePromise

if (newRoutingTable) {
// valid routing table was fetched - just return it, try next router otherwise
return newRoutingTable
return [newRoutingTable, null]
} else {
// returned routing table was undefined, this means a connection error happened and we need to forget the
// previous router and try the next one
Expand All @@ -473,19 +478,19 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
}

// try next router
const session = await this._createSessionForRediscovery(
const [session, error] = await this._createSessionForRediscovery(
currentRouter,
bookmarks,
impersonatedUser
)
if (session) {
try {
return await this._rediscovery.lookupRoutingTableOnRouter(
return [await this._rediscovery.lookupRoutingTableOnRouter(
session,
routingTable.database,
currentRouter,
impersonatedUser
)
), null]
} catch (error) {
return this._handleRediscoveryError(error, currentRouter)
} finally {
Expand All @@ -494,10 +499,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
} else {
// unable to acquire connection and create session towards the current router
// return null to signal that the next router should be tried
return null
return [null, error]
}
},
Promise.resolve(null)
Promise.resolve([null, null])
)
}

Expand All @@ -522,13 +527,13 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
})
}

return new Session({
return [new Session({
mode: READ,
database: SYSTEM_DB_NAME,
bookmarks,
connectionProvider,
impersonatedUser
})
}), null]
} catch (error) {
return this._handleRediscoveryError(error, routerAddress)
}
Expand All @@ -541,21 +546,23 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
// throw when getServers procedure not found because this is clearly a configuration issue
throw newError(
`Server at ${routerAddress.asHostPort()} can't perform routing. Make sure you are connecting to a causal cluster`,
SERVICE_UNAVAILABLE
SERVICE_UNAVAILABLE,
error
)
}
this._log.warn(
`unable to fetch routing table because of an error ${error}`
)
return null
return [null, error]
}

async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable, onDatabaseNameResolved) {
async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable, onDatabaseNameResolved, error) {
if (!newRoutingTable) {
// none of routing servers returned valid routing table, throw exception
throw newError(
`Could not perform discovery. No routing servers available. Known routing table: ${currentRoutingTable}`,
SERVICE_UNAVAILABLE
SERVICE_UNAVAILABLE,
error
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1701,6 +1701,8 @@ describe('#unit RoutingConnectionProvider', () => {
'Server at server-non-existing-seed-router:7687 can\'t ' +
'perform routing. Make sure you are connecting to a causal cluster'
)
// Error should be the cause of the given capturedError
expect(capturedError).toEqual(newError(capturedError.message, capturedError.code, error))
}

expect(completed).toBe(false)
Expand Down Expand Up @@ -1728,6 +1730,9 @@ describe('#unit RoutingConnectionProvider', () => {
'Could not perform discovery. ' +
'No routing servers available. Known routing table: '
))

// Error should be the cause of the given capturedError
expect(capturedError).toEqual(newError(capturedError.message, capturedError.code, error))
}

expect(completed).toBe(false)
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ class Neo4jError extends Error {
* @param {string} message - the error message
* @param {string} code - Optional error code. Will be populated when error originates in the database.
*/
constructor (message: string, code: Neo4jErrorCode) {
super(message)
constructor (message: string, code: Neo4jErrorCode, cause?: Error) {
// @ts-expect-error
super(message, cause != null ? { cause } : undefined)
this.constructor = Neo4jError
// eslint-disable-next-line no-proto
this.__proto__ = Neo4jError.prototype
Expand Down Expand Up @@ -105,8 +106,8 @@ class Neo4jError extends Error {
* @return {Neo4jError} an {@link Neo4jError}
* @private
*/
function newError (message: string, code?: Neo4jErrorCode): Neo4jError {
return new Neo4jError(message, code ?? NOT_AVAILABLE)
function newError (message: string, code?: Neo4jErrorCode, cause?: Error): Neo4jError {
return new Neo4jError(message, code ?? NOT_AVAILABLE, cause)
}

/**
Expand Down
33 changes: 33 additions & 0 deletions packages/core/test/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ import {
} from '../src/error'

describe('newError', () => {
let supportsCause = false
beforeAll(() => {
// @ts-expect-error
const error = new Error('a', { cause: new Error('a') })
// @ts-expect-error
supportsCause = error.cause != null
})

;[PROTOCOL_ERROR, SERVICE_UNAVAILABLE, SESSION_EXPIRED].forEach(
expectedCode => {
test(`should create Neo4jError for code ${expectedCode}`, () => {
Expand All @@ -43,6 +51,31 @@ describe('newError', () => {
expect(error.message).toEqual('some error')
expect(error.code).toEqual('N/A')
})

test('should create Neo4jErro with cause', () => {
const cause = new Error('cause')
const error: Neo4jError = newError('some error', undefined, cause)

expect(error.message).toEqual('some error')
expect(error.code).toEqual('N/A')
if (supportsCause) {
// @ts-expect-error
expect(error.cause).toBe(cause)
} else {
// @ts-expect-error
expect(error.cause).toBeUndefined()
}
})

test.each([null, undefined])('should create Neo4jErro without cause (%s)', (cause) => {
// @ts-expect-error
const error: Neo4jError = newError('some error', undefined, cause)

expect(error.message).toEqual('some error')
expect(error.code).toEqual('N/A')
// @ts-expect-error
expect(error.cause).toBeUndefined()
})
})

describe('isRetriableError()', () => {
Expand Down