Skip to content

Commit 720e5c6

Browse files
bigmontzMaxAake
andauthored
Fix maxConnectionPoolSize verification (#1216)
* Fix `maxConnectionPoolSize` verification Under high load of sessions, the connection pool is allowing new connections to be created. This is happening because, while the development of `AuthTokenManager` and `connection liveness check`, the methods do validate connection on acquired and return to the pool need to be async. This changes creates a situation of racing condition which doesn't exist the original code and this introduces this bug. Increasing the resouce acquired count before validating the connection solves the issue, since the race condition is removed. However, the pool needs also to release the resource when the validation return `false` or fails for some reason. This is important to avoid broken connection still be count for the max pool size. Co-Authored-By: Max Gustafsson <[email protected]> * Adjusting test for be more resilient The test were to tight and any slowness might cause the test to fail. Increase the time waiting for the connection get release to pool reduces the likehood of this problem happens. --------- Co-authored-by: Max Gustafsson <[email protected]>
1 parent c517fa9 commit 720e5c6

File tree

3 files changed

+164
-5
lines changed

3 files changed

+164
-5
lines changed

packages/core/src/internal/pool/pool.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,32 @@ class Pool<R extends unknown = unknown> {
251251
continue
252252
}
253253

254+
resourceAcquired(key, this._activeResourceCounts)
255+
254256
if (this._removeIdleObserver != null) {
255257
this._removeIdleObserver(resource)
256258
}
257259

258-
if (await this._validateOnAcquire(acquisitionContext, resource)) {
260+
let valid = false
261+
262+
try {
263+
valid = await this._validateOnAcquire(acquisitionContext, resource)
264+
} catch (e) {
265+
if (this._log.isErrorEnabled()) {
266+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
267+
this._log.error(`Failure on validate ${resource}. This is a bug, please report it. Caused by: ${e.message}`)
268+
}
269+
}
270+
271+
if (valid) {
259272
// idle resource is valid and can be acquired
260-
resourceAcquired(key, this._activeResourceCounts)
261273
if (this._log.isDebugEnabled()) {
262274
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
263275
this._log.debug(`${resource} acquired from the pool ${key}`)
264276
}
265277
return { resource, pool }
266278
} else {
279+
resourceReleased(key, this._activeResourceCounts)
267280
pool.removeInUse(resource)
268281
await this._destroy(resource)
269282
}

packages/core/test/internal/pool/pool.test.ts

+134-1
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,139 @@ describe('#unit Pool', () => {
905905
expect(conns.length).toEqual(1)
906906
})
907907

908+
it('should count connection on validation process when eval max pool size', async () => {
909+
const conns: any[] = []
910+
const pool = new Pool<any>({
911+
// Hook into connection creation to track when and what connections that are
912+
// created.
913+
create: async (_, server, release) => {
914+
// Create a fake connection that makes it possible control when it's connected
915+
// and released from the outer scope.
916+
const conn: any = {
917+
server,
918+
release
919+
}
920+
conns.push(conn)
921+
return conn
922+
},
923+
validateOnAcquire: async (context, resource: any) => {
924+
const promise = new Promise<boolean>((resolve, reject) => {
925+
if (resource.promises == null) {
926+
resource.promises = []
927+
}
928+
resource.promises.push({
929+
resolve,
930+
reject
931+
})
932+
})
933+
934+
return await promise
935+
},
936+
// Setup pool to only allow one connection
937+
config: new PoolConfig(1, 100000)
938+
})
939+
940+
// Make the first request for a connection, this will return a connection instantaneously
941+
const conn0 = await pool.acquire({}, address)
942+
expect(conns.length).toEqual(1)
943+
944+
// Releasing connection back to the pool, so it can be re-acquired.
945+
await conn0.release(address, conn0)
946+
947+
// Request the same connection again, it will wait until resolve get called.
948+
const req0 = pool.acquire({}, address)
949+
expect(conns.length).toEqual(1)
950+
951+
// Request other connection, this should also resolve the same connection1.
952+
const req1 = pool.acquire({}, address)
953+
expect(conns.length).toEqual(1)
954+
955+
// connection 1 is valid
956+
conns[0].promises[0].resolve(true)
957+
958+
// getting the connection 1
959+
const conn1 = await req0
960+
expect(conn0).toBe(conn1)
961+
await conn1.release(address, conn1)
962+
963+
// connection 2 is valid
964+
conns[0].promises[1].resolve(true)
965+
966+
// getting the connection 2
967+
const conn2 = await req1
968+
expect(conn0).toBe(conn2)
969+
await conn2.release(address, conn2)
970+
})
971+
972+
it.each([
973+
['is not valid', (promise: any) => promise.resolve(false)],
974+
['validation fails', (promise: any) => promise.reject(new Error('failed'))]
975+
])('should create new connection if the current one when %s', async (_, resolver) => {
976+
const conns: any[] = []
977+
const pool = new Pool<any>({
978+
// Hook into connection creation to track when and what connections that are
979+
// created.
980+
create: async (_, server, release) => {
981+
// Create a fake connection that makes it possible control when it's connected
982+
// and released from the outer scope.
983+
const conn: any = {
984+
server,
985+
release
986+
}
987+
conns.push(conn)
988+
return conn
989+
},
990+
validateOnAcquire: async (context, resource: any) => {
991+
const promise = new Promise<boolean>((resolve, reject) => {
992+
if (resource.promises == null) {
993+
resource.promises = []
994+
}
995+
resource.promises.push({
996+
resolve,
997+
reject
998+
})
999+
})
1000+
1001+
return await promise
1002+
},
1003+
// Setup pool to only allow one connection
1004+
config: new PoolConfig(1, 100000)
1005+
})
1006+
1007+
// Make the first request for a connection, this will return a connection instantaneously
1008+
const conn0 = await pool.acquire({}, address)
1009+
expect(conns.length).toEqual(1)
1010+
1011+
// Releasing connection back to the pool, so it can be re-acquired.
1012+
await conn0.release(address, conn0)
1013+
1014+
// Request the same connection again, it will wait until resolve get called.
1015+
const req0 = pool.acquire({}, address)
1016+
expect(conns.length).toEqual(1)
1017+
1018+
// Request other connection, this should also resolve the same connection2.
1019+
const req1 = pool.acquire({}, address)
1020+
expect(conns.length).toEqual(1)
1021+
1022+
// should resolve the promise with the configured value
1023+
resolver(conns[0].promises[0])
1024+
1025+
// getting the connection 1
1026+
const conn1 = await req0
1027+
expect(conn0).not.toBe(conn1)
1028+
await conn1.release(address, conn1)
1029+
expect(conns.length).toEqual(2)
1030+
1031+
// connection 2 is valid
1032+
conns[1].promises[0].resolve(true)
1033+
1034+
// getting the connection 2
1035+
const conn2 = await req1
1036+
expect(conn1).toBe(conn2)
1037+
await conn2.release(address, conn2)
1038+
expect(conns.length).toEqual(2)
1039+
})
1040+
9081041
it('should not time out if max pool size is not set', async () => {
9091042
let counter = 0
9101043

@@ -1157,7 +1290,7 @@ describe('#unit Pool', () => {
11571290
)
11581291

11591292
const numberOfIdleResourceAfterResourceGetCreated = await new Promise(resolve =>
1160-
setTimeout(() => resolve(idleResources(pool, address)), 11))
1293+
setTimeout(() => resolve(idleResources(pool, address)), 15))
11611294

11621295
expect(numberOfIdleResourceAfterResourceGetCreated).toEqual(1)
11631296
expect(counter).toEqual(1)

packages/neo4j-driver-deno/lib/core/internal/pool/pool.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,32 @@ class Pool<R extends unknown = unknown> {
251251
continue
252252
}
253253

254+
resourceAcquired(key, this._activeResourceCounts)
255+
254256
if (this._removeIdleObserver != null) {
255257
this._removeIdleObserver(resource)
256258
}
257259

258-
if (await this._validateOnAcquire(acquisitionContext, resource)) {
260+
let valid = false
261+
262+
try {
263+
valid = await this._validateOnAcquire(acquisitionContext, resource)
264+
} catch (e) {
265+
if (this._log.isErrorEnabled()) {
266+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
267+
this._log.error(`Failure on validate ${resource}. This is a bug, please report it. Caused by: ${e.message}`)
268+
}
269+
}
270+
271+
if (valid) {
259272
// idle resource is valid and can be acquired
260-
resourceAcquired(key, this._activeResourceCounts)
261273
if (this._log.isDebugEnabled()) {
262274
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
263275
this._log.debug(`${resource} acquired from the pool ${key}`)
264276
}
265277
return { resource, pool }
266278
} else {
279+
resourceReleased(key, this._activeResourceCounts)
267280
pool.removeInUse(resource)
268281
await this._destroy(resource)
269282
}

0 commit comments

Comments
 (0)