Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yield LEVEL_LOCKED error when lock is held #8

Merged
merged 3 commits into from
Mar 25, 2022
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
8 changes: 8 additions & 0 deletions binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Comment on lines +448 to +451
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This platform difference will be gone once we upgrade LevelDB - but we'll still have to compare the error message. I don't see a cleaner way atm, short of patching upstream to return a more specific leveldb::Status.

} else {
argv = CreateCodeError(env, "LEVEL_IO_ERROR", errMsg_);
}
} else {
argv = CreateError(env, errMsg_);
}
Expand Down
52 changes: 52 additions & 0 deletions test/lock-test.js
Original file line number Diff line number Diff line change
@@ -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))
})
})
})
10 changes: 10 additions & 0 deletions test/lock.js
Original file line number Diff line number Diff line change
@@ -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)
})