Skip to content

Commit ca21462

Browse files
committed
Merge pull request brianc#15 from slickmb/bug/close_race
Avoid race when stream closed while fetching
2 parents edfe1aa + d1ac31c commit ca21462

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

index.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ for(var key in Cursor.prototype) {
3232
}
3333
}
3434

35-
QueryStream.prototype.close = function() {
35+
QueryStream.prototype.close = function(cb) {
3636
this._closing = true
3737
var self = this
3838
Cursor.prototype.close.call(this, function(err) {
39+
if (cb) { cb(err); }
3940
if(err) return self.emit('error', err)
4041
process.nextTick(function() {
4142
self.push(null)
@@ -51,6 +52,9 @@ QueryStream.prototype._read = function(n) {
5152
if(err) {
5253
return self.emit('error', err)
5354
}
55+
56+
if (self._closing) { return; }
57+
5458
if(!rows.length) {
5559
process.nextTick(function() {
5660
self.push(null)

test/close.js

+65
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,68 @@ helper('early close', function(client) {
3333
})
3434
})
3535
})
36+
37+
helper('should not throw errors after early close', function(client) {
38+
it('can be closed early without error', function(done) {
39+
var stream = new QueryStream('SELECT * FROM generate_series(0, 2000) num');
40+
var query = client.query(stream);
41+
var fetchCount = 0;
42+
var errorCount = 0;
43+
44+
45+
function waitForErrors() {
46+
47+
setTimeout(function () {
48+
assert(errorCount === 0, 'should not throw a ton of errors');
49+
done();
50+
}, 10);
51+
}
52+
53+
// hack internal _fetch function to force query.close immediately after _fetch is called (simulating the race condition)
54+
// race condition: if close is called immediately after _fetch is called, but before results are returned, errors are thrown
55+
// when the fetch results are pushed to the readable stream after its already closed.
56+
query._fetch = (function (_fetch) {
57+
return function () {
58+
59+
// wait for the second fetch. closing immediately after the first fetch throws an entirely different error :(
60+
if (fetchCount++ === 0) {
61+
return _fetch.apply(this, arguments);
62+
}
63+
64+
var results = _fetch.apply(this, arguments);
65+
66+
query.close();
67+
waitForErrors();
68+
69+
query._fetch = _fetch; // we're done with our hack, so restore the original _fetch function.
70+
71+
return results;
72+
}
73+
}(query._fetch));
74+
75+
query.on('error', function () { errorCount++; });
76+
77+
query.on('readable', function () {
78+
query.read();
79+
});
80+
});
81+
});
82+
83+
helper('close callback', function (client) {
84+
it('notifies an optional callback when the conneciton is closed', function (done) {
85+
var stream = new QueryStream('SELECT * FROM generate_series(0, $1) num', [10], {batchSize: 2, highWaterMark: 2});
86+
var query = client.query(stream);
87+
query.once('readable', function() { // only reading once
88+
query.read();
89+
});
90+
query.once('readable', function() {
91+
query.close(function () {
92+
// nothing to assert. This test will time out if the callback does not work.
93+
done();
94+
});
95+
});
96+
query.on('close', function () {
97+
assert(false, "close event should not fire"); // no close event because we did not read to the end of the stream.
98+
});
99+
});
100+
});

0 commit comments

Comments
 (0)