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

Commit 66454c8

Browse files
committed
Close database on environment exit
Closes #667, closes #755.
1 parent ef1c321 commit 66454c8

5 files changed

Lines changed: 135 additions & 8 deletions

File tree

binding.cc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,33 @@ NAPI_METHOD(db_init) {
726726
return result;
727727
}
728728

729+
// Hook for when the environment exits.
730+
static void env_cleanup_hook (void* arg) {
731+
Database* database = (Database*)arg;
732+
733+
// Do everything that db_close() does but synchronously. We're expecting that
734+
// GC did not yet collect the database because that would be a user mistake (not
735+
// closing their db) made during the lifetime of (and within) the environment.
736+
// That's different from an environment being torn down (like the main process
737+
// or a worker thread) where it's our responsibility to clean up.
738+
if (database && database->db_ != NULL) {
739+
std::map<uint32_t, Iterator*> iterators = database->iterators_;
740+
std::map<uint32_t, Iterator*>::iterator it;
741+
742+
for (it = iterators.begin(); it != iterators.end(); ++it) {
743+
Iterator* iterator = it->second;
744+
745+
if (!iterator->ended_) {
746+
iterator->ended_ = true;
747+
iterator->IteratorEnd();
748+
}
749+
}
750+
751+
// Having ended the iterators (and released snapshots) we can safely close.
752+
database->CloseDatabase();
753+
}
754+
}
755+
729756
/**
730757
* Worker class for opening a database.
731758
*/
@@ -791,6 +818,11 @@ NAPI_METHOD(db_open) {
791818

792819
database->blockCache_ = leveldb::NewLRUCache(cacheSize);
793820

821+
// Register a hook for when the environment exits. Will be called after
822+
// already-scheduled napi_async_work items have finished, which gives us
823+
// the guarantee that no db operations will be in-flight at that time.
824+
napi_add_env_cleanup_hook(env, env_cleanup_hook, database);
825+
794826
napi_value callback = argv[3];
795827
OpenWorker* worker = new OpenWorker(env, database, callback, location,
796828
createIfMissing, errorIfExists,
@@ -833,6 +865,12 @@ NAPI_METHOD(db_close) {
833865
napi_value callback = argv[1];
834866
CloseWorker* worker = new CloseWorker(env, database, callback);
835867

868+
// Remove hook for when the environment exits. Don't need it anymore
869+
// because we're closing here and although that is asynchronous, the
870+
// environment will wait for already-scheduled napi_async_work items
871+
// to finish before exiting.
872+
napi_remove_env_cleanup_hook(env, env_cleanup_hook, database);
873+
836874
if (!database->HasPriorityWork()) {
837875
worker->Queue();
838876
NAPI_RETURN_UNDEFINED();

test/env-cleanup-hook-test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict'
2+
3+
const test = require('tape')
4+
const fork = require('child_process').fork
5+
const path = require('path')
6+
7+
// Test env_cleanup_hook at several stages of a db lifetime
8+
addTest(['create'])
9+
addTest(['create', 'open'])
10+
addTest(['create', 'open', 'create-iterator'])
11+
addTest(['create', 'open', 'create-iterator', 'close'])
12+
addTest(['create', 'open', 'create-iterator', 'nexting'])
13+
addTest(['create', 'open', 'create-iterator', 'nexting', 'close'])
14+
addTest(['create', 'open', 'close'])
15+
addTest(['create', 'open-error'])
16+
17+
function addTest (steps) {
18+
test(`cleanup on environment exit (${steps.join(', ')})`, function (t) {
19+
t.plan(3)
20+
21+
const child = fork(path.join(__dirname, 'env-cleanup-hook.js'), steps)
22+
23+
child.on('message', function (m) {
24+
t.is(m, steps[steps.length - 1], `got to step: ${m}`)
25+
child.disconnect()
26+
})
27+
28+
child.on('exit', function (code, sig) {
29+
t.is(code, 0, 'child exited normally')
30+
t.is(sig, null, 'not terminated due to signal')
31+
})
32+
})
33+
}

test/env-cleanup-hook.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
3+
const testCommon = require('./common')
4+
5+
function test (steps) {
6+
let step
7+
8+
function nextStep () {
9+
step = steps.shift() || step
10+
return step
11+
}
12+
13+
if (nextStep() !== 'create') {
14+
// Send a message indicating at which step we stopped, and
15+
// triggering an environment exit.
16+
return process.send(step)
17+
}
18+
19+
// Create a db, expected only to be garbage-collected because
20+
// the environment cleanup hook is not added until open().
21+
const db = testCommon.factory()
22+
23+
if (nextStep() !== 'open') {
24+
if (nextStep() === 'open-error') {
25+
// If opening fails we'll still have the cleanup hook but it will be a noop.
26+
db.open({ createIfMissing: false, errorIfExists: true }, function (err) {
27+
if (!err) throw new Error('Expected an open() error')
28+
})
29+
}
30+
31+
return process.send(step)
32+
}
33+
34+
// Open the db, expected to be closed by the cleanup hook.
35+
db.open(function (err) {
36+
if (err) throw err
37+
38+
if (nextStep() === 'create-iterator') {
39+
// Create an iterator, expected to be ended by the cleanup hook.
40+
const it = db.iterator()
41+
42+
if (nextStep() === 'nexting') {
43+
// This async work should finish before the cleanup hook is called.
44+
it.next(function (err) {
45+
if (err) throw err
46+
})
47+
}
48+
}
49+
50+
if (nextStep() === 'close') {
51+
// Close the db, expected only to be garbage-collected because we
52+
// synchronously remove the cleanup hook in close().
53+
db.close(function (err) {
54+
if (err) throw err
55+
})
56+
}
57+
58+
process.send(step)
59+
})
60+
}
61+
62+
test(process.argv.slice(2))

test/iterator-recursion-test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ test('setUp common', testCommon.setUp)
2727
// call in our Iterator to segfault. This was fixed in 2014 (commit 85e6a38).
2828
//
2929
// Today (2020), we see occasional failures in CI again. We no longer call
30-
// node::FatalException() so there's a new reason. Possibly related to
31-
// https://github.com/Level/leveldown/issues/667.
30+
// node::FatalException() so there's a new reason.
3231
test.skip('try to create an iterator with a blown stack', function (t) {
3332
for (let i = 0; i < 100; i++) {
3433
t.test(`try to create an iterator with a blown stack (${i})`, function (t) {

test/stack-blower.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ if (process.argv[2] === 'run') {
2121
try {
2222
recurse()
2323
} catch (e) {
24-
// Closing before process exit is normally not needed. This is a
25-
// temporary workaround for Level/leveldown#667.
26-
db.close(function (err) {
27-
if (err) throw err
28-
process.send('Catchable error at depth ' + depth)
29-
})
24+
process.send('Catchable error at depth ' + depth)
3025
}
3126
})
3227
}

0 commit comments

Comments
 (0)