diff --git a/src/EventStore.js b/src/EventStore.js index 5a2ad87..379eecc 100644 --- a/src/EventStore.js +++ b/src/EventStore.js @@ -221,6 +221,17 @@ class EventStore extends events.EventEmitter { this.storage.close(); } + /** + * Returns true if the storage is currently locked by a writer process. + * Useful when this store is opened read-only to check whether a writer holds an exclusive lock. + * + * @api + * @returns {boolean} + */ + isLocked() { + return this.storage.isLocked(); + } + /** * Override EventEmitter.on() to delegate 'preCommit' and 'preRead' event registrations * to the underlying storage, so that `eventstore.on('preCommit', handler)` works naturally. diff --git a/src/Storage/ReadableStorage.js b/src/Storage/ReadableStorage.js index f0a0c2a..24a1feb 100644 --- a/src/Storage/ReadableStorage.js +++ b/src/Storage/ReadableStorage.js @@ -93,6 +93,7 @@ class ReadableStorage extends events.EventEmitter { this.hmac = createHmac(config.hmacSecret); this.dataDirectory = path.resolve(config.dataDirectory); + this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock'); const partitionDefaults = { readBufferSize: DEFAULT_READ_BUFFER_SIZE }; this.partitionConfig = Object.assign(partitionDefaults, config); @@ -157,6 +158,17 @@ class ReadableStorage extends events.EventEmitter { return this.index.length; } + /** + * Returns true if the storage is currently locked by a writer process. + * Useful for read-only clients to check whether a writer holds an exclusive lock. + * + * @api + * @returns {boolean} + */ + isLocked() { + return fs.existsSync(this.lockFile); + } + /** * Scan partitions and secondary index files; emit 'index-created' for each found index. * @param {function} done Called when both scans finish. diff --git a/src/Storage/WritableStorage.js b/src/Storage/WritableStorage.js index 4030c5e..857ae7a 100644 --- a/src/Storage/WritableStorage.js +++ b/src/Storage/WritableStorage.js @@ -61,7 +61,6 @@ class WritableStorage extends ReadableStorage { ensureDirectory(config.dataDirectory); super(storageName, config); - this.lockFile = path.resolve(this.dataDirectory, this.storageFile + '.lock'); this._lockMode = config.lock; this.partitioner = config.partitioner; } @@ -226,14 +225,18 @@ class WritableStorage extends ReadableStorage { if (this.locked) { return false; } + if (this.isLocked()) { + throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`); + } try { fs.mkdirSync(this.lockFile); this.locked = true; } catch (e) { - /* istanbul ignore if */ + /* istanbul ignore next */ if (e.code !== 'EEXIST') { throw new Error(`Error creating lock for storage ${this.storageFile}: ` + e.message); } + /* istanbul ignore next */ throw new StorageLockedError(`Storage ${this.storageFile} is locked by another process`); } return true; diff --git a/test/EventStore.spec.js b/test/EventStore.spec.js index d70fec3..940d4d4 100644 --- a/test/EventStore.spec.js +++ b/test/EventStore.spec.js @@ -277,6 +277,44 @@ describe('EventStore', function() { }); }); + it('isLocked() returns true when a writer is open', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + + eventstore.once('ready', () => { + const readstore = new EventStore({ + storageDirectory, + readOnly: true + }); + readstore.once('ready', () => { + expect(readstore.isLocked()).to.be(true); + readstore.close(); + done(); + }); + }); + }); + + it('isLocked() returns false when no writer is open', function(done) { + eventstore = new EventStore({ + storageDirectory + }); + + eventstore.once('ready', () => { + eventstore.close(); + const readstore = new EventStore({ + storageDirectory, + readOnly: true + }); + readstore.once('ready', () => { + expect(readstore.isLocked()).to.be(false); + readstore.close(); + eventstore = null; + done(); + }); + }); + }); + describe('commit', function() { it('throws when no stream name specified', function() { diff --git a/test/Storage.spec.js b/test/Storage.spec.js index bee3dfb..9a56c41 100644 --- a/test/Storage.spec.js +++ b/test/Storage.spec.js @@ -1225,6 +1225,43 @@ describe('Storage', function() { }).to.not.throwError(); }); + it('isLocked() returns true when a writer holds the lock', function(){ + storage = createStorage(); + storage.open(); + const reader = createReader(); + reader.open(); + expect(reader.isLocked()).to.be(true); + reader.close(); + }); + + it('isLocked() returns false when no writer holds the lock', function(){ + storage = createStorage(); + storage.open(); + storage.close(); + const reader = createReader(); + reader.open(); + expect(reader.isLocked()).to.be(false); + reader.close(); + }); + + it('isLocked() returns false on a writer before open', function(){ + storage = createStorage(); + expect(storage.isLocked()).to.be(false); + }); + + it('isLocked() returns true on a writer after open', function(){ + storage = createStorage(); + storage.open(); + expect(storage.isLocked()).to.be(true); + }); + + it('isLocked() returns false on a writer after close', function(){ + storage = createStorage(); + storage.open(); + storage.close(); + expect(storage.isLocked()).to.be(false); + }); + it('allows multiple readers for one storage', function () { storage = createStorage(); storage.open();