Skip to content
This repository was archived by the owner on Dec 1, 2024. It is now read-only.

Commit d84b084

Browse files
authored
Merge pull request #597 from Level/fix-segfaults-on-close-and-gc
Fix segfaults on close and gc
2 parents 3ff48f1 + 195c78c commit d84b084

6 files changed

Lines changed: 187 additions & 32 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ node_modules/
44
build/
55
build-pre-gyp/
66
Release/
7+
deps/*/Debug/
78
libleveldb.so
89
libleveldb.a
910
leakydb

binding.cc

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <napi-macros.h>
44
#include <node_api.h>
5+
#include <assert.h>
56

67
#include <leveldb/db.h>
78
#include <leveldb/write_batch.h>
@@ -289,7 +290,7 @@ struct BaseWorker {
289290
delete self;
290291
}
291292

292-
void DoComplete () {
293+
virtual void DoComplete () {
293294
if (status_.ok()) {
294295
return HandleOKCallback();
295296
}
@@ -308,7 +309,7 @@ struct BaseWorker {
308309
CallFunction(env_, callback, 1, &argv);
309310
}
310311

311-
void Queue () {
312+
virtual void Queue () {
312313
napi_queue_async_work(env_, asyncWork_);
313314
}
314315

@@ -332,6 +333,7 @@ struct Database {
332333
blockCache_(NULL),
333334
filterPolicy_(leveldb::NewBloomFilterPolicy(10)),
334335
currentIteratorId_(0),
336+
priorityWork_(0),
335337
pendingCloseWorker_(NULL) {}
336338

337339
~Database () {
@@ -404,21 +406,41 @@ struct Database {
404406
return db_->ReleaseSnapshot(snapshot);
405407
}
406408

407-
void ReleaseIterator (uint32_t id) {
409+
void AttachIterator (uint32_t id, Iterator* iterator) {
410+
iterators_[id] = iterator;
411+
IncrementPriorityWork();
412+
}
413+
414+
void DetachIterator (uint32_t id) {
408415
iterators_.erase(id);
409-
if (iterators_.empty() && pendingCloseWorker_ != NULL) {
416+
DecrementPriorityWork();
417+
}
418+
419+
void IncrementPriorityWork () {
420+
++priorityWork_;
421+
}
422+
423+
void DecrementPriorityWork () {
424+
if (--priorityWork_ == 0 && pendingCloseWorker_ != NULL) {
410425
pendingCloseWorker_->Queue();
411426
pendingCloseWorker_ = NULL;
412427
}
413428
}
414429

430+
bool HasPriorityWork () {
431+
return priorityWork_ > 0;
432+
}
433+
415434
napi_env env_;
416435
leveldb::DB* db_;
417436
leveldb::Cache* blockCache_;
418437
const leveldb::FilterPolicy* filterPolicy_;
419438
uint32_t currentIteratorId_;
420439
BaseWorker *pendingCloseWorker_;
421440
std::map< uint32_t, Iterator * > iterators_;
441+
442+
private:
443+
uint32_t priorityWork_;
422444
};
423445

424446
/**
@@ -430,6 +452,28 @@ static void FinalizeDatabase (napi_env env, void* data, void* hint) {
430452
}
431453
}
432454

455+
/**
456+
* Base worker class for doing async work that defers closing the database.
457+
* @TODO Use this for Get-, Del-, Batch-, BatchWrite-, ApproximateSize- and CompactRangeWorker too.
458+
*/
459+
struct PriorityWorker : public BaseWorker {
460+
PriorityWorker (napi_env env, Database* database, napi_value callback, const char* resourceName)
461+
: BaseWorker(env, database, callback, resourceName) {
462+
}
463+
464+
virtual ~PriorityWorker () {}
465+
466+
void Queue () final {
467+
database_->IncrementPriorityWork();
468+
BaseWorker::Queue();
469+
}
470+
471+
void DoComplete () final {
472+
database_->DecrementPriorityWork();
473+
BaseWorker::DoComplete();
474+
}
475+
};
476+
433477
/**
434478
* Owns a leveldb iterator.
435479
*/
@@ -472,13 +516,15 @@ struct Iterator {
472516
landed_(false),
473517
nexting_(false),
474518
ended_(false),
475-
endWorker_(NULL) {
519+
endWorker_(NULL),
520+
ref_(NULL) {
476521
options_ = new leveldb::ReadOptions();
477522
options_->fill_cache = fillCache;
478523
options_->snapshot = database->NewSnapshot();
479524
}
480525

481526
~Iterator () {
527+
assert(ended_);
482528
ReleaseTarget();
483529
if (start_ != NULL) {
484530
// Special case for `start` option: it won't be
@@ -506,6 +552,7 @@ struct Iterator {
506552
if (gte_ != NULL) {
507553
delete gte_;
508554
}
555+
delete options_;
509556
}
510557

511558
void ReleaseTarget () {
@@ -518,8 +565,14 @@ struct Iterator {
518565
}
519566
}
520567

521-
void Release () {
522-
database_->ReleaseIterator(id_);
568+
void Attach (napi_ref ref) {
569+
ref_ = ref;
570+
database_->AttachIterator(id_, this);
571+
}
572+
573+
napi_ref Detach () {
574+
database_->DetachIterator(id_);
575+
return ref_;
523576
}
524577

525578
leveldb::Status IteratorStatus () {
@@ -681,6 +734,9 @@ struct Iterator {
681734

682735
leveldb::ReadOptions* options_;
683736
EndWorker* endWorker_;
737+
738+
private:
739+
napi_ref ref_;
684740
};
685741

686742
/**
@@ -803,7 +859,7 @@ NAPI_METHOD(db_close) {
803859
napi_value callback = argv[1];
804860
CloseWorker* worker = new CloseWorker(env, database, callback);
805861

806-
if (database->iterators_.empty()) {
862+
if (!database->HasPriorityWork()) {
807863
worker->Queue();
808864
NAPI_RETURN_UNDEFINED();
809865
}
@@ -813,13 +869,11 @@ NAPI_METHOD(db_close) {
813869
napi_value noop;
814870
napi_create_function(env, NULL, 0, noop_callback, NULL, &noop);
815871

816-
std::map< uint32_t, Iterator * >::iterator it;
817-
for (it = database->iterators_.begin();
818-
it != database->iterators_.end(); ++it) {
819-
Iterator *iterator = it->second;
820-
if (!iterator->ended_) {
821-
iterator_end_do(env, iterator, noop);
822-
}
872+
std::map<uint32_t, Iterator*> iterators = database->iterators_;
873+
std::map<uint32_t, Iterator*>::iterator it;
874+
875+
for (it = iterators.begin(); it != iterators.end(); ++it) {
876+
iterator_end_do(env, it->second, noop);
823877
}
824878

825879
NAPI_RETURN_UNDEFINED();
@@ -828,14 +882,14 @@ NAPI_METHOD(db_close) {
828882
/**
829883
* Worker class for putting key/value to the database
830884
*/
831-
struct PutWorker : public BaseWorker {
885+
struct PutWorker : public PriorityWorker {
832886
PutWorker (napi_env env,
833887
Database* database,
834888
napi_value callback,
835889
leveldb::Slice key,
836890
leveldb::Slice value,
837891
bool sync)
838-
: BaseWorker(env, database, callback, "leveldown.db.put"),
892+
: PriorityWorker(env, database, callback, "leveldown.db.put"),
839893
key_(key), value_(value) {
840894
options_.sync = sync;
841895
}
@@ -1292,12 +1346,18 @@ NAPI_METHOD(iterator_init) {
12921346
Iterator* iterator = new Iterator(database, id, start, end, reverse, keys,
12931347
values, limit, lt, lte, gt, gte, fillCache,
12941348
keyAsBuffer, valueAsBuffer, highWaterMark);
1295-
database->iterators_[id] = iterator;
1296-
12971349
napi_value result;
1350+
napi_ref ref;
1351+
12981352
NAPI_STATUS_THROWS(napi_create_external(env, iterator,
12991353
FinalizeIterator,
13001354
NULL, &result));
1355+
1356+
// Prevent GC of JS object before the iterator is ended (explicitly or on
1357+
// db close) and keep track of non-ended iterators to end them on db close.
1358+
NAPI_STATUS_THROWS(napi_create_reference(env, result, 1, &ref));
1359+
iterator->Attach(ref);
1360+
13011361
return result;
13021362
}
13031363

@@ -1372,7 +1432,7 @@ struct EndWorker : public BaseWorker {
13721432
}
13731433

13741434
virtual void HandleOKCallback () {
1375-
iterator_->Release();
1435+
napi_delete_reference(env_, iterator_->Detach());
13761436
BaseWorker::HandleOKCallback();
13771437
}
13781438

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"scripts": {
88
"install": "node-gyp-build",
99
"test": "standard && hallmark && nyc tape test/*-test.js",
10+
"test-gc": "npx -n=--expose-gc tape test/{cleanup,iterator-gc}*-test.js",
1011
"coverage": "nyc report --reporter=text-lcov | coveralls",
1112
"rebuild": "node-gyp rebuild",
1213
"prebuild": "prebuildify -t 8.14.0 -t electron@3.0.0 --napi --strip",

test/cleanup-hanging-iterators-test.js

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
const makeTest = require('./make')
2+
const repeats = 200
23

34
makeTest('test ended iterator', function (db, t, done) {
4-
// standard iterator with an end() properly called, easy
5+
// First test normal and proper usage: calling it.end() before db.close()
56
var it = db.iterator({ keyAsBuffer: false, valueAsBuffer: false })
7+
68
it.next(function (err, key, value) {
79
t.ifError(err, 'no error from next()')
810
t.equal(key, 'one', 'correct key')
@@ -14,9 +16,29 @@ makeTest('test ended iterator', function (db, t, done) {
1416
})
1517
})
1618

17-
makeTest('test non-ended iterator', function (db, t, done) {
18-
// no end() call on our iterator, cleanup should crash Node if not handled properly
19+
makeTest('test likely-ended iterator', function (db, t, done) {
20+
// Test improper usage: not calling it.end() before db.close(). Cleanup of the
21+
// database will crash Node if not handled properly.
1922
var it = db.iterator({ keyAsBuffer: false, valueAsBuffer: false })
23+
24+
it.next(function (err, key, value) {
25+
t.ifError(err, 'no error from next()')
26+
t.equal(key, 'one', 'correct key')
27+
t.equal(value, '1', 'correct value')
28+
done()
29+
})
30+
})
31+
32+
makeTest('test non-ended iterator', function (db, t, done) {
33+
// Same as the test above but with a highWaterMark of 0 so that we don't
34+
// preemptively fetch all records, to ensure that the iterator is still
35+
// active when we (attempt to) close the database.
36+
var it = db.iterator({
37+
highWaterMark: 0,
38+
keyAsBuffer: false,
39+
valueAsBuffer: false
40+
})
41+
2042
it.next(function (err, key, value) {
2143
t.ifError(err, 'no error from next()')
2244
t.equal(key, 'one', 'correct key')
@@ -25,17 +47,43 @@ makeTest('test non-ended iterator', function (db, t, done) {
2547
})
2648
})
2749

50+
makeTest('test multiple likely-ended iterators', function (db, t, done) {
51+
// Same as the test above but repeated and with an extra iterator that is not
52+
// nexting, which means its EndWorker will be executed almost immediately.
53+
for (let i = 0; i < repeats; i++) {
54+
db.iterator()
55+
db.iterator().next(function () {})
56+
}
57+
58+
setTimeout(done, Math.floor(Math.random() * 50))
59+
})
60+
2861
makeTest('test multiple non-ended iterators', function (db, t, done) {
29-
// no end() call on our iterator, cleanup should crash Node if not handled properly
30-
db.iterator()
31-
db.iterator().next(function () {})
32-
db.iterator().next(function () {})
33-
db.iterator().next(function () {})
34-
setTimeout(done, 50)
62+
// Same as the test above but with a highWaterMark of 0.
63+
for (let i = 0; i < repeats; i++) {
64+
db.iterator({ highWaterMark: 0 })
65+
db.iterator({ highWaterMark: 0 }).next(function () {})
66+
}
67+
68+
setTimeout(done, Math.floor(Math.random() * 50))
69+
})
70+
71+
global.gc && makeTest('test multiple non-ended iterators with forced gc', function (db, t, done) {
72+
// Same as the test above but with forced GC, to test that the lifespan of an
73+
// iterator is tied to *both* its JS object and whether the iterator was ended.
74+
for (let i = 0; i < repeats; i++) {
75+
db.iterator({ highWaterMark: 0 })
76+
db.iterator({ highWaterMark: 0 }).next(function () {})
77+
}
78+
79+
setTimeout(function () {
80+
global.gc()
81+
done()
82+
}, Math.floor(Math.random() * 50))
3583
})
3684

3785
makeTest('test ending iterators', function (db, t, done) {
38-
// at least one end() should be in progress when we try to close the db
86+
// At least one end() should be in progress when we try to close the db.
3987
var it1 = db.iterator().next(function () {
4088
it1.end(function () {})
4189
})

test/leak-tester-iterator.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
const db = require('./common').factory()
2+
3+
let count = 0
4+
let rssBase
5+
6+
if (!global.gc) {
7+
console.error('To force GC, run with "node --expose-gc"')
8+
}
9+
10+
function run () {
11+
var it = db.iterator()
12+
13+
it.next(function (err) {
14+
if (err) throw err
15+
16+
it.end(function (err) {
17+
if (err) throw err
18+
19+
if (!rssBase) {
20+
rssBase = process.memoryUsage().rss
21+
}
22+
23+
if (++count % 1000 === 0) {
24+
if (global.gc) global.gc()
25+
26+
const rss = process.memoryUsage().rss
27+
const percent = Math.round((rss / rssBase) * 100)
28+
const mb = Math.round(rss / 1024 / 1024)
29+
30+
console.log('count = %d, rss = %d% %dM', count, percent, mb)
31+
}
32+
33+
run()
34+
})
35+
})
36+
}
37+
38+
db.open(function (err) {
39+
if (err) throw err
40+
41+
db.put('key', 'value', function (err) {
42+
if (err) throw err
43+
run()
44+
})
45+
})

0 commit comments

Comments
 (0)