diff --git a/README.md b/README.md index 8636406..252aad0 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,7 @@ Read-only getter that returns a string reflecting the current state of the datab ### `db.open([options][, callback])` -Open the database. The `callback` function will be called with no arguments when successfully opened, or with a single error argument if opening failed. If no callback is provided, a promise is returned. Options passed to `open()` take precedence over options passed to the database constructor. +Open the database. The `callback` function will be called with no arguments when successfully opened, or with a single error argument if opening failed. The database has an exclusive lock (on disk): if another process or instance has already opened the underlying LevelDB store at the given `location` then opening will fail with error code [`LEVEL_LOCKED`](https://github.com/Level/abstract-level#errors). If no callback is provided, a promise is returned. Options passed to `open()` take precedence over options passed to the database constructor. The optional `options` object may contain: diff --git a/binding.cc b/binding.cc index d4cff53..01507c4 100644 --- a/binding.cc +++ b/binding.cc @@ -444,6 +444,14 @@ struct BaseWorker { argv = CreateCodeError(env, "LEVEL_NOT_FOUND", errMsg_); } else if (status_.IsCorruption()) { argv = CreateCodeError(env, "LEVEL_CORRUPTION", errMsg_); + } else if (status_.IsIOError()) { + if (strlen(errMsg_) > 15 && strncmp("IO error: lock ", errMsg_, 15) == 0) { // env_posix.cc + argv = CreateCodeError(env, "LEVEL_LOCKED", errMsg_); + } else if (strlen(errMsg_) > 19 && strncmp("IO error: LockFile ", errMsg_, 19) == 0) { // env_win.cc + argv = CreateCodeError(env, "LEVEL_LOCKED", errMsg_); + } else { + argv = CreateCodeError(env, "LEVEL_IO_ERROR", errMsg_); + } } else { argv = CreateError(env, errMsg_); } diff --git a/test/lock-test.js b/test/lock-test.js new file mode 100644 index 0000000..cbc0aeb --- /dev/null +++ b/test/lock-test.js @@ -0,0 +1,52 @@ +'use strict' + +const test = require('tape') +const tempy = require('tempy') +const fork = require('child_process').fork +const path = require('path') +const { ClassicLevel } = require('..') + +test('lock held by same process', async function (t) { + t.plan(2) + + const location = tempy.directory() + const db1 = new ClassicLevel(location) + await db1.open() + const db2 = new ClassicLevel(location) + + try { + await db2.open() + } catch (err) { + t.is(err.code, 'LEVEL_DATABASE_NOT_OPEN', 'second instance failed to open') + t.is(err.cause.code, 'LEVEL_LOCKED', 'second instance got lock error') + } + + return db1.close() +}) + +test('lock held by other process', function (t) { + t.plan(6) + + const location = tempy.directory() + const db = new ClassicLevel(location) + + db.open(function (err) { + t.ifError(err, 'no open error') + + const child = fork(path.join(__dirname, 'lock.js'), [location]) + + child.on('message', function (err) { + t.is(err.code, 'LEVEL_DATABASE_NOT_OPEN', 'second process failed to open') + t.is(err.cause.code, 'LEVEL_LOCKED', 'second process got lock error') + + child.disconnect() + }) + + child.on('exit', function (code, sig) { + t.is(code, 0, 'child exited normally') + t.is(sig, null, 'not terminated due to signal') + + db.close(t.ifError.bind(t)) + }) + }) +}) diff --git a/test/lock.js b/test/lock.js new file mode 100644 index 0000000..d74f6ac --- /dev/null +++ b/test/lock.js @@ -0,0 +1,10 @@ +'use strict' + +const { ClassicLevel } = require('..') + +const location = process.argv[2] +const db = new ClassicLevel(location) + +db.open(function (err) { + process.send(err) +})