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

Commit 8fdcaaa

Browse files
authored
Close database on environment exit (#783)
Closes #667, closes #755.
1 parent ef1c321 commit 8fdcaaa

5 files changed

+138
-17
lines changed

binding.cc

+44-9
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,6 @@ struct Database {
468468
uint32_t priorityWork_;
469469
};
470470

471-
/**
472-
* Runs when a Database is garbage collected.
473-
*/
474-
static void FinalizeDatabase (napi_env env, void* data, void* hint) {
475-
if (data) {
476-
delete (Database*)data;
477-
}
478-
}
479-
480471
/**
481472
* Base worker class for doing async work that defers closing the database.
482473
*/
@@ -713,11 +704,55 @@ struct Iterator {
713704
napi_ref ref_;
714705
};
715706

707+
/**
708+
* Hook for when the environment exits. This hook will be called after
709+
* already-scheduled napi_async_work items have finished, which gives us
710+
* the guarantee that no db operations will be in-flight at this time.
711+
*/
712+
static void env_cleanup_hook (void* arg) {
713+
Database* database = (Database*)arg;
714+
715+
// Do everything that db_close() does but synchronously. We're expecting that GC
716+
// did not (yet) collect the database because that would be a user mistake (not
717+
// closing their db) made during the lifetime of the environment. That's different
718+
// from an environment being torn down (like the main process or a worker thread)
719+
// where it's our responsibility to clean up. Note also, the following code must
720+
// be a safe noop if called before db_open() or after db_close().
721+
if (database && database->db_ != NULL) {
722+
std::map<uint32_t, Iterator*> iterators = database->iterators_;
723+
std::map<uint32_t, Iterator*>::iterator it;
724+
725+
for (it = iterators.begin(); it != iterators.end(); ++it) {
726+
Iterator* iterator = it->second;
727+
728+
if (!iterator->ended_) {
729+
iterator->ended_ = true;
730+
iterator->IteratorEnd();
731+
}
732+
}
733+
734+
// Having ended the iterators (and released snapshots) we can safely close.
735+
database->CloseDatabase();
736+
}
737+
}
738+
739+
/**
740+
* Runs when a Database is garbage collected.
741+
*/
742+
static void FinalizeDatabase (napi_env env, void* data, void* hint) {
743+
if (data) {
744+
Database* database = (Database*)data;
745+
napi_remove_env_cleanup_hook(env, env_cleanup_hook, database);
746+
delete database;
747+
}
748+
}
749+
716750
/**
717751
* Returns a context object for a database.
718752
*/
719753
NAPI_METHOD(db_init) {
720754
Database* database = new Database(env);
755+
napi_add_env_cleanup_hook(env, env_cleanup_hook, database);
721756

722757
napi_value result;
723758
NAPI_STATUS_THROWS(napi_create_external(env, database,

test/env-cleanup-hook-test.js

+33
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

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 triggering an environment exit
15+
// and indicating at which step we stopped.
16+
return process.send(step)
17+
}
18+
19+
const db = testCommon.factory()
20+
21+
if (nextStep() !== 'open') {
22+
if (nextStep() === 'open-error') {
23+
// If opening fails the cleanup hook should be a noop.
24+
db.open({ createIfMissing: false, errorIfExists: true }, function (err) {
25+
if (!err) throw new Error('Expected an open() error')
26+
})
27+
}
28+
29+
return process.send(step)
30+
}
31+
32+
// Open the db, expected to be closed by the cleanup hook.
33+
db.open(function (err) {
34+
if (err) throw err
35+
36+
if (nextStep() === 'create-iterator') {
37+
// Create an iterator, expected to be ended by the cleanup hook.
38+
const it = db.iterator()
39+
40+
if (nextStep() === 'nexting') {
41+
// This async work should finish before the cleanup hook is called.
42+
it.next(function (err) {
43+
if (err) throw err
44+
})
45+
}
46+
}
47+
48+
if (nextStep() === 'close') {
49+
// Close the db, after which the cleanup hook is a noop.
50+
db.close(function (err) {
51+
if (err) throw err
52+
})
53+
}
54+
55+
process.send(step)
56+
})
57+
}
58+
59+
test(process.argv.slice(2))

test/iterator-recursion-test.js

+1-2
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

+1-6
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)