Skip to content

Commit b43c046

Browse files
committed
Fixed timeout when acquiring connection from the pool
Previously it was possible for timeout to kick in after connection acquisition promise is resolved, but before timeout itself is cleared. Pseudocode would be: ``` const acquisitionTimeout = xxx; const f1 = () => {/* fail and clear the acquisition attempt */}; const acquisitionTimeoutId = setTimeout(f1, acquisitionTimeout); const f2 = () => clearTimeout(acquisitionTimeoutId); connectionAcquisitionPromise.then(f1); ``` This code allows `connectionAcquisitionPromise` to get resolved and `f1` to get executed before `f2`. It is possible because installed callbacks are only scheduled, not executed when promise is resolved. This commit fixes the problem by moving `clearTimeout` to the place that resolves the acquisition request, i.e. `Pool#release()` call.
1 parent 2b27814 commit b43c046

File tree

4 files changed

+69
-115
lines changed

4 files changed

+69
-115
lines changed

src/v1/internal/pool.js

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
* limitations under the License.
1818
*/
1919

20-
import {promiseOrTimeout} from './util';
2120
import PoolConfig from './pool-config';
21+
import {newError} from '../error';
2222

2323
class Pool {
2424
/**
@@ -65,20 +65,17 @@ class Pool {
6565
allRequests[key] = [];
6666
}
6767

68-
let request;
68+
return new Promise((resolve, reject) => {
69+
let request;
6970

70-
return promiseOrTimeout(
71-
this._acquisitionTimeout,
72-
new Promise(
73-
(resolve, reject) => {
74-
request = new PendingRequest(resolve);
71+
const timeoutId = setTimeout(() => {
72+
allRequests[key] = allRequests[key].filter(item => item !== request);
73+
reject(newError(`Connection acquisition timed out in ${this._acquisitionTimeout} ms.`));
74+
}, this._acquisitionTimeout);
7575

76-
allRequests[key].push(request);
77-
}
78-
), () => {
79-
allRequests[key] = allRequests[key].filter(item => item !== request);
80-
}
81-
);
76+
request = new PendingRequest(resolve, timeoutId);
77+
allRequests[key].push(request);
78+
});
8279
}
8380

8481
/**
@@ -208,11 +205,13 @@ function resourceReleased(key, activeResourceCounts) {
208205

209206
class PendingRequest {
210207

211-
constructor(resolve) {
208+
constructor(resolve, timeoutId) {
212209
this._resolve = resolve;
210+
this._timeoutId = timeoutId;
213211
}
214212

215213
resolve(resource) {
214+
clearTimeout(this._timeoutId);
216215
this._resolve(resource);
217216
}
218217

src/v1/internal/util.js

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
* limitations under the License.
1818
*/
1919

20-
import {newError} from '../error';
21-
2220
const ENCRYPTION_ON = "ENCRYPTION_ON";
2321
const ENCRYPTION_OFF = "ENCRYPTION_OFF";
2422

@@ -64,42 +62,11 @@ function isString(str) {
6462
return Object.prototype.toString.call(str) === '[object String]';
6563
}
6664

67-
function promiseOrTimeout(timeout, otherPromise, onTimeout) {
68-
let resultPromise = null;
69-
70-
const timeoutPromise = new Promise((resolve, reject) => {
71-
const id = setTimeout(() => {
72-
if (onTimeout && typeof onTimeout === 'function') {
73-
onTimeout();
74-
}
75-
76-
reject(newError(`Operation timed out in ${timeout} ms.`));
77-
}, timeout);
78-
79-
// this "executor" function is executed immediately, even before the Promise constructor returns
80-
// thus it's safe to initialize resultPromise variable here, where timeout id variable is accessible
81-
resultPromise = otherPromise.then(result => {
82-
clearTimeout(id);
83-
return result;
84-
}).catch(error => {
85-
clearTimeout(id);
86-
throw error;
87-
});
88-
});
89-
90-
if (resultPromise == null) {
91-
throw new Error('Result promise not initialized');
92-
}
93-
94-
return Promise.race([resultPromise, timeoutPromise]);
95-
}
96-
9765
export {
9866
isEmptyObjectOrNull,
9967
isString,
10068
assertString,
10169
assertCypherStatement,
102-
promiseOrTimeout,
10370
ENCRYPTION_ON,
10471
ENCRYPTION_OFF
10572
}

test/internal/pool.test.js

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,10 @@ describe('Pool', () => {
419419
done();
420420
});
421421

422-
setTimeout(() => r1.close(), 1000);
422+
setTimeout(() => {
423+
expectNumberOfAcquisitionRequests(pool, key, 1);
424+
r1.close();
425+
}, 1000);
423426
});
424427
});
425428

@@ -445,7 +448,7 @@ describe('Pool', () => {
445448

446449
pool.acquire(key).catch(error => {
447450
expect(error.message).toContain('timed out');
448-
451+
expectNumberOfAcquisitionRequests(pool, key, 0);
449452
done();
450453
});
451454
});
@@ -472,14 +475,64 @@ describe('Pool', () => {
472475

473476
pool.acquire(key).then(r2 => {
474477
expect(r2.id).toEqual(2);
475-
478+
expectNoPendingAcquisitionRequests(pool);
476479
done();
477480
});
478481
});
479482
});
480483

484+
it('should work fine when resources released together with acquisition timeout', done => {
485+
const acquisitionTimeout = 1000;
486+
let counter = 0;
487+
488+
const key = 'bolt://localhost:7687';
489+
const pool = new Pool(
490+
(url, release) => new Resource(url, counter++, release),
491+
resource => {
492+
},
493+
() => true,
494+
new PoolConfig(2, acquisitionTimeout)
495+
);
496+
497+
pool.acquire(key).then(resource1 => {
498+
expect(resource1.id).toEqual(0);
499+
500+
pool.acquire(key).then(resource2 => {
501+
expect(resource2.id).toEqual(1);
502+
503+
// try to release both resources around the time acquisition fails with timeout
504+
// double-release used to cause deletion of acquire requests in the pool and failure of the timeout
505+
// such background failure made this test fail, not the existing assertions
506+
setTimeout(() => {
507+
resource1.close();
508+
resource2.close();
509+
}, acquisitionTimeout);
510+
511+
pool.acquire(key).then(someResource => {
512+
expect(someResource).toBeDefined();
513+
expect(someResource).not.toBeNull();
514+
expectNoPendingAcquisitionRequests(pool);
515+
done(); // ok, promise got resolved before the timeout
516+
}).catch(error => {
517+
expect(error).toBeDefined();
518+
expect(error).not.toBeNull();
519+
expectNoPendingAcquisitionRequests(pool);
520+
done(); // also ok, timeout fired before promise got resolved
521+
});
522+
});
523+
});
524+
});
525+
481526
});
482527

528+
function expectNoPendingAcquisitionRequests(pool) {
529+
expect(pool._acquireRequests).toEqual({});
530+
}
531+
532+
function expectNumberOfAcquisitionRequests(pool, key, expectedNumber) {
533+
expect(pool._acquireRequests[key].length).toEqual(expectedNumber);
534+
}
535+
483536
class Resource {
484537

485538
constructor(key, id, release) {

test/internal/util.test.js

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -74,71 +74,6 @@ describe('util', () => {
7474
verifyInvalidCypherStatement(console.log);
7575
});
7676

77-
it('should time out', () => {
78-
expect(() => util.promiseOrTimeout(500, new Promise(), null)).toThrow();
79-
});
80-
81-
it('should not time out', done => {
82-
util.promiseOrTimeout(500, Promise.resolve(0), null).then((result) => {
83-
expect(result).toEqual(0);
84-
done();
85-
})
86-
});
87-
88-
it('should call clear action when timed out', done => {
89-
let marker = 0;
90-
91-
let clearAction = () => {
92-
marker = 1;
93-
};
94-
95-
util.promiseOrTimeout(500, new Promise((resolve, reject) => { }), clearAction).catch((error) => {
96-
expect(marker).toEqual(1);
97-
done();
98-
});
99-
});
100-
101-
it('should not trigger both promise and timeout', done => {
102-
const timeout = 500;
103-
104-
let timeoutFired = false;
105-
let result = null;
106-
let error = null;
107-
108-
const resultPromise = util.promiseOrTimeout(
109-
timeout,
110-
new Promise(resolve => {
111-
setTimeout(() => {
112-
resolve(42);
113-
}, timeout);
114-
}),
115-
() => {
116-
timeoutFired = true;
117-
}
118-
);
119-
120-
resultPromise.then(r => {
121-
result = r;
122-
}).catch(e => {
123-
error = e;
124-
});
125-
126-
setTimeout(() => {
127-
if (timeoutFired) {
128-
// timeout fired - result should not be set, error should be set
129-
expect(result).toBeNull();
130-
expect(error).not.toBeNull();
131-
expect(error.message).toEqual(`Operation timed out in ${timeout} ms.`);
132-
done();
133-
} else {
134-
// timeout did not fire - result should be set, error should not be set
135-
expect(result).toEqual(42);
136-
expect(error).toBeNull();
137-
done();
138-
}
139-
}, timeout * 2);
140-
});
141-
14277
function verifyValidString(str) {
14378
expect(util.assertString(str, 'Test string')).toBe(str);
14479
}

0 commit comments

Comments
 (0)