-
Notifications
You must be signed in to change notification settings - Fork 15
fix: leveldb iterator memory leak #26
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
'use strict' | ||
|
||
const { utils } = require('interface-datastore') | ||
const LevelStore = require('../../src') | ||
|
||
async function testLevelIteratorDestroy () { | ||
const store = new LevelStore(utils.tmpdir(), { db: require('level') }) | ||
await store.open() | ||
await store.put(`/test/key${Date.now()}`, Buffer.from(`TESTDATA${Date.now()}`)) | ||
for await (const d of store.query({})) { | ||
console.log(d) // eslint-disable-line no-console | ||
} | ||
} | ||
|
||
// Will exit with: | ||
// > Assertion failed: (ended_), function ~Iterator, file ../binding.cc, line 546. | ||
// If iterator gets destroyed (in c++ land) and .end() was not called on it. | ||
testLevelIteratorDestroy() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ const rimraf = require('rimraf') | |
const { MountDatastore } = require('datastore-core') | ||
const CID = require('cids') | ||
const { promisify } = require('util') | ||
const childProcess = require('child_process') | ||
|
||
const LevelStore = require('../src') | ||
|
||
|
@@ -68,4 +69,33 @@ describe('LevelDatastore', () => { | |
expect(cids[0].version).to.be.eql(0) | ||
expect(cids).to.have.length(4) | ||
}) | ||
|
||
// The `.end()` method MUST be called on LevelDB iterators or they remain open, | ||
// leaking memory. | ||
// | ||
// This test exposes this problem by causing an error to be thrown on process | ||
// exit when an iterator is open AND leveldb is not closed. | ||
// | ||
// Normally when leveldb is closed it'll automatically clean up open iterators | ||
// but if you don't close the store this error will occur: | ||
// | ||
// > Assertion failed: (ended_), function ~Iterator, file ../binding.cc, line 546. | ||
// | ||
// This is thrown by a destructor function for iterator objects that asserts | ||
// the iterator has ended before cleanup. | ||
// | ||
// https://github.com/Level/leveldown/blob/d3453fbde4d2a8aa04d9091101c25c999649069b/binding.cc#L545 | ||
it('should not leave iterators open and leak memory', (done) => { | ||
const cp = childProcess.fork(`${__dirname}/fixtures/test-level-iterator-destroy`, { stdio: 'pipe' }) | ||
|
||
let out = '' | ||
cp.stdout.on('data', d => { out += d }) | ||
cp.stderr.on('data', d => { out += d }) | ||
|
||
cp.on('exit', code => { | ||
expect(code).to.equal(0) | ||
expect(out).to.not.include('Assertion failed: (ended_)') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way to assert on the presence of something rather than the absence of something? Even something we add to the fixture to demonstrate that the iterator has been torn down correctly? Otherwise this test is coupled to the internal implementation of leveldown which may change without us knowing and this test may not expose the problem it is designed to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really, the leveldb iterator is not exposed an any way and the js iterator always ends fine. Given that this doesn’t even work on Node.js 10 I think this is not a good test. That said I’m all out of ideas for how to test it right now! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Still internal but at least it'd assert presence of something) you could check |
||
done() | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness can you also implement the
Generator.prototype.throw()
method with similar logic?