Skip to content
This repository has been archived by the owner on Mar 23, 2023. It is now read-only.

fix: leveldb iterator memory leak #26

Merged
merged 2 commits into from
Jan 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,12 @@ function levelIteratorToIterator (li) {
next: () => new Promise((resolve, reject) => {
li.next((err, key, value) => {
if (err) return reject(err)
if (key == null) return resolve({ done: true })
if (key == null) {
Copy link
Member

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?

return li.end(err => {
if (err) return reject(err)
resolve({ done: true })
})
}
resolve({ done: false, value: { key, value } })
})
}),
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/test-level-iterator-destroy.js
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()
30 changes: 30 additions & 0 deletions test/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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_)')
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

@alanshaw alanshaw Jan 4, 2020

Choose a reason for hiding this comment

The 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!

Copy link

Choose a reason for hiding this comment

The 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 it._ended.

done()
})
})
})