Skip to content

Commit d7d7476

Browse files
authored
Merge pull request #289 from ali-ince/1.5-max-connection-pool-size
implemented maxConnectionPoolSize logic
2 parents 649110d + ae5ac46 commit d7d7476

File tree

9 files changed

+494
-175
lines changed

9 files changed

+494
-175
lines changed

src/v1/driver.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ class Driver {
5959
this._createConnection.bind(this),
6060
this._destroyConnection.bind(this),
6161
this._validateConnection.bind(this),
62-
config.connectionPoolSize
62+
{
63+
maxIdleSize: config.connectionPoolSize,
64+
maxSize: config.maxConnectionPoolSize,
65+
acquisitionTimeout: config.connectionAcquisitionTimeout
66+
}
6367
);
6468

6569
/**
@@ -231,17 +235,20 @@ class _ConnectionStreamObserver extends StreamObserver {
231235
* @private
232236
*/
233237
function sanitizeConfig(config) {
234-
const maxConnectionLifetime = config.maxConnectionLifetime;
235-
if (maxConnectionLifetime) {
236-
const sanitizedMaxConnectionLifetime = parseInt(maxConnectionLifetime, 10);
237-
if (sanitizedMaxConnectionLifetime && sanitizedMaxConnectionLifetime > 0) {
238-
config.maxConnectionLifetime = sanitizedMaxConnectionLifetime;
239-
} else {
240-
config.maxConnectionLifetime = null;
241-
}
242-
} else {
243-
config.maxConnectionLifetime = null;
238+
config.maxConnectionLifetime = sanitizeIntValue(config.maxConnectionLifetime);
239+
config.maxConnectionPoolSize = sanitizeIntValue(config.maxConnectionPoolSize);
240+
config.connectionAcquisitionTimeout = sanitizeIntValue(config.connectionAcquisitionTimeout, 60000);
241+
}
242+
243+
function sanitizeIntValue(value, defaultValue=null) {
244+
if (value) {
245+
const sanitizedValue = parseInt(value, 10);
246+
if (sanitizedValue && sanitizedValue > 0) {
247+
return sanitizedValue;
248+
}
244249
}
250+
251+
return defaultValue;
245252
}
246253

247254
export {Driver, READ, WRITE}

src/v1/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
112112
* // Connection will be destroyed if this threshold is exceeded.
113113
* connectionPoolSize: 50,
114114
*
115+
* // The maximum total number of connections allowed to be managed by the connection pool, per host.
116+
* // This includes both in-use and idle connections. No maximum connection pool size is imposed
117+
* // by default.
118+
* maxConnectionPoolSize: 100,
119+
*
115120
* // The maximum allowed lifetime for a pooled connection in milliseconds. Pooled connections older than this
116121
* // threshold will be closed and removed from the pool. Such discarding happens during connection acquisition
117122
* // so that new session is never backed by an old connection. Setting this option to a low value will cause
@@ -121,6 +126,10 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
121126
* // and negative values result in lifetime not being checked.
122127
* maxConnectionLifetime: 30 * 60 * 1000, // 30 minutes
123128
*
129+
* // The maximum amount of time to wait to acquire a connection from the pool (to either create a new
130+
* // connection or borrow an existing one.
131+
* connectionAcquisitionTimeout: 60000, // 1 minute
132+
*
124133
* // Specify the maximum time in milliseconds transactions are allowed to retry via
125134
* // {@link Session#readTransaction()} and {@link Session#writeTransaction()} functions. These functions
126135
* // will retry the given unit of work on `ServiceUnavailable`, `SessionExpired` and transient errors with

src/v1/internal/connection-providers.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ export class DirectConnectionProvider extends ConnectionProvider {
5353
}
5454

5555
acquireConnection(mode) {
56-
const connection = this._connectionPool.acquire(this._address);
57-
const connectionPromise = Promise.resolve(connection);
56+
const connectionPromise = this._connectionPool.acquire(this._address);
5857
return this._withAdditionalOnErrorCallback(connectionPromise, this._driverOnErrorCallback);
5958
}
6059
}
@@ -193,19 +192,21 @@ export class LoadBalancer extends ConnectionProvider {
193192
}
194193

195194
// try next router
196-
const session = this._createSessionForRediscovery(currentRouter);
197-
return this._rediscovery.lookupRoutingTableOnRouter(session, currentRouter);
195+
return this._createSessionForRediscovery(currentRouter).then(session => {
196+
return this._rediscovery.lookupRoutingTableOnRouter(session, currentRouter)
197+
});
198198
});
199199
}, Promise.resolve(null));
200200
}
201201

202202
_createSessionForRediscovery(routerAddress) {
203-
const connection = this._connectionPool.acquire(routerAddress);
204-
// initialized connection is required for routing procedure call
205-
// server version needs to be known to decide which routing procedure to use
206-
const initializedConnectionPromise = connection.initializationCompleted();
207-
const connectionProvider = new SingleConnectionProvider(initializedConnectionPromise);
208-
return new Session(READ, connectionProvider);
203+
return this._connectionPool.acquire(routerAddress).then(connection => {
204+
// initialized connection is required for routing procedure call
205+
// server version needs to be known to decide which routing procedure to use
206+
const initializedConnectionPromise = connection.initializationCompleted();
207+
const connectionProvider = new SingleConnectionProvider(initializedConnectionPromise);
208+
return new Session(READ, connectionProvider);
209+
});
209210
}
210211

211212
_applyRoutingTableIfPossible(newRoutingTable) {

src/v1/internal/pool.js

+89-19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
* limitations under the License.
1818
*/
1919

20+
import { newError } from "../error";
21+
import { promiseOrTimeout } from "./util";
22+
2023
class Pool {
2124
/**
2225
* @param create an allocation function that creates a new resource. It's given
@@ -30,12 +33,15 @@ class Pool {
3033
* @param maxIdle the max number of resources that are allowed idle in the pool at
3134
* any time. If this threshold is exceeded, resources will be evicted.
3235
*/
33-
constructor(create, destroy=(()=>true), validate=(()=>true), maxIdle=50) {
36+
constructor(create, destroy=(()=>true), validate=(()=>true), config={}) {
3437
this._create = create;
3538
this._destroy = destroy;
3639
this._validate = validate;
37-
this._maxIdle = maxIdle;
40+
this._maxIdleSize = config.maxIdleSize;
41+
this._maxSize = config.maxSize;
42+
this._acquisitionTimeout = config.acquisitionTimeout;
3843
this._pools = {};
44+
this._acquireRequests = {};
3945
this._activeResourceCounts = {};
4046
this._release = this._release.bind(this);
4147
}
@@ -46,26 +52,35 @@ class Pool {
4652
* @return {object} resource that is ready to use.
4753
*/
4854
acquire(key) {
49-
let pool = this._pools[key];
50-
if (!pool) {
51-
pool = [];
52-
this._pools[key] = pool;
55+
const resource = this._acquire(key);
56+
57+
if (resource) {
58+
resourceAcquired(key, this._activeResourceCounts);
59+
60+
return Promise.resolve(resource);
5361
}
54-
while (pool.length) {
55-
const resource = pool.pop();
5662

57-
if (this._validate(resource)) {
58-
// idle resource is valid and can be acquired
59-
resourceAcquired(key, this._activeResourceCounts);
60-
return resource;
61-
} else {
62-
this._destroy(resource);
63-
}
63+
// We're out of resources and will try to acquire later on when an existing resource is released.
64+
const allRequests = this._acquireRequests;
65+
const requests = allRequests[key];
66+
if (!requests) {
67+
allRequests[key] = [];
6468
}
6569

66-
// there exist no idle valid resources, create a new one for acquisition
67-
resourceAcquired(key, this._activeResourceCounts);
68-
return this._create(key, this._release);
70+
let request;
71+
72+
return promiseOrTimeout(
73+
this._acquisitionTimeout,
74+
new Promise(
75+
(resolve, reject) => {
76+
request = new PendingRequest(resolve);
77+
78+
allRequests[key].push(request);
79+
}
80+
), () => {
81+
allRequests[key] = allRequests[key].filter(item => item !== request);
82+
}
83+
);
6984
}
7085

7186
/**
@@ -106,12 +121,37 @@ class Pool {
106121
return this._activeResourceCounts[key] || 0;
107122
}
108123

124+
_acquire(key) {
125+
let pool = this._pools[key];
126+
if (!pool) {
127+
pool = [];
128+
this._pools[key] = pool;
129+
}
130+
while (pool.length) {
131+
const resource = pool.pop();
132+
133+
if (this._validate(resource)) {
134+
// idle resource is valid and can be acquired
135+
return resource;
136+
} else {
137+
this._destroy(resource);
138+
}
139+
}
140+
141+
if (this._maxSize && this.activeResourceCount(key) >= this._maxSize) {
142+
return null;
143+
}
144+
145+
// there exist no idle valid resources, create a new one for acquisition
146+
return this._create(key, this._release);
147+
}
148+
109149
_release(key, resource) {
110150
const pool = this._pools[key];
111151

112152
if (pool) {
113153
// there exist idle connections for the given key
114-
if (pool.length >= this._maxIdle || !this._validate(resource)) {
154+
if (pool.length >= this._maxIdleSize || !this._validate(resource)) {
115155
this._destroy(resource);
116156
} else {
117157
pool.push(resource);
@@ -121,6 +161,23 @@ class Pool {
121161
this._destroy(resource);
122162
}
123163

164+
// check if there are any pending requests
165+
const requests = this._acquireRequests[key];
166+
if (requests) {
167+
var pending = requests.shift();
168+
169+
if (pending) {
170+
var resource = this._acquire(key);
171+
if (resource) {
172+
pending.resolve(resource);
173+
174+
return;
175+
}
176+
} else {
177+
delete this._acquireRequests[key];
178+
}
179+
}
180+
124181
resourceReleased(key, this._activeResourceCounts);
125182
}
126183
}
@@ -143,11 +200,24 @@ function resourceAcquired(key, activeResourceCounts) {
143200
function resourceReleased(key, activeResourceCounts) {
144201
const currentCount = activeResourceCounts[key] || 0;
145202
const nextCount = currentCount - 1;
203+
146204
if (nextCount > 0) {
147205
activeResourceCounts[key] = nextCount;
148206
} else {
149207
delete activeResourceCounts[key];
150208
}
151209
}
152210

211+
class PendingRequest {
212+
213+
constructor(resolve) {
214+
this._resolve = resolve;
215+
}
216+
217+
resolve(resource) {
218+
this._resolve(resource);
219+
}
220+
221+
}
222+
153223
export default Pool

src/v1/internal/util.js

+19
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
* limitations under the License.
1818
*/
1919

20+
import { newError } from "../error";
21+
2022
const ENCRYPTION_ON = "ENCRYPTION_ON";
2123
const ENCRYPTION_OFF = "ENCRYPTION_OFF";
2224

@@ -122,6 +124,22 @@ function trimAndVerify(string, name, url) {
122124
return result;
123125
}
124126

127+
function promiseOrTimeout(timeout, otherPromise, onTimeout) {
128+
const timeoutPromise = new Promise((resolve, reject) => {
129+
const id = setTimeout(() => {
130+
if (onTimeout && typeof onTimeout === 'function') {
131+
onTimeout();
132+
}
133+
134+
reject(newError(`Operation timed out in ${timeout} ms.`));
135+
}, timeout);
136+
137+
otherPromise.then(() => clearTimeout(id), () => clearTimeout(id));
138+
});
139+
140+
return Promise.race([ otherPromise, timeoutPromise ]);
141+
}
142+
125143
export {
126144
isEmptyObjectOrNull,
127145
isString,
@@ -132,6 +150,7 @@ export {
132150
parseHost,
133151
parsePort,
134152
parseRoutingContext,
153+
promiseOrTimeout,
135154
ENCRYPTION_ON,
136155
ENCRYPTION_OFF
137156
}

0 commit comments

Comments
 (0)