Skip to content

Commit 1b83f6c

Browse files
committed
feat(lock): allow for custom lock
fix: failing tests test: add a lock test suite doc: update repo docs for custom locking
1 parent d187e7d commit 1b83f6c

File tree

5 files changed

+205
-7
lines changed

5 files changed

+205
-7
lines changed

README.md

+33-2
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Arguments:
142142

143143
* `path` (string, mandatory): the path for this repo
144144
* `options` (object, optional): may contain the following values
145-
* `lock` (string, defaults to `"fs"` in Node.js, `"memory"` in the browser): what type of lock to use. Lock has to be acquired when opening.
145+
* `lock` (string *Deprecated* or [Lock](#lock)), string can be `"fs"` or `"memory"`: what type of lock to use. Lock has to be acquired when opening.
146146
* `storageBackends` (object, optional): may contain the following values, which should each be a class implementing the [datastore interface](https://github.com/ipfs/interface-datastore#readme):
147147
* `root` (defaults to [`datastore-fs`](https://github.com/ipfs/js-datastore-fs#readme) in Node.js and [`datastore-level`](https://github.com/ipfs/js-datastore-level#readme) in the browser). Defines the back-end type used for gets and puts of values at the root (`repo.set()`, `repo.get()`)
148148
* `blocks` (defaults to [`datastore-fs`](https://github.com/ipfs/js-datastore-fs#readme) in Node.js and [`datastore-level`](https://github.com/ipfs/js-datastore-level#readme) in the browser). Defines the back-end type used for gets and puts of values at `repo.blocks`.
@@ -284,7 +284,7 @@ Sets the API address.
284284

285285
### `repo.stat ([options], callback)`
286286

287-
Gets the repo status.
287+
Gets the repo status.
288288

289289
`options` is an object which might contain the key `human`, which is a boolean indicating whether or not the `repoSize` should be displayed in MiB or not.
290290

@@ -296,6 +296,37 @@ Gets the repo status.
296296
- `version`
297297
- `storageMax`
298298

299+
### Lock
300+
301+
IPFS Repo comes with two built in locks: memory and fs. These can be imported via the following:
302+
303+
```js
304+
const fsLock = require('ipfs-repo/lock') // Default in Node.js
305+
const memLock = require('ipfs-repo/lock-memory') // Default in browser
306+
```
307+
308+
#### `lock.open (dir, callback)`
309+
310+
Sets the lock if one does not already exist.
311+
312+
`dir` is a string to the directory the lock should be created at. The repo typically creates the lock at its root.
313+
314+
`callback` is a function with the signature `function (err, closer)`, where `closer` has a `close` method for removing the lock.
315+
316+
##### `closer.close (callback)`
317+
318+
Closes the lock created by `lock.open`
319+
320+
`callback` is a function with the signature `function (err)`. If no error was returned, the lock was successfully removed.
321+
322+
#### `lock.locked (dir, callback)`
323+
324+
Checks the existence of the lock.
325+
326+
`dir` is a string to the directory to check for the lock. The repo typically checks for the lock at its root.
327+
328+
`callback` is a function with the signature `function (err, boolean)`, where `boolean` indicates the existence of the lock.
329+
299330
## Notes
300331

301332
- [Explanation of how repo is structured](https://github.com/ipfs/js-ipfs-repo/pull/111#issuecomment-279948247)

src/index.js

+59-5
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ class IpfsRepo {
4444
this.closed = true
4545
this.path = repoPath
4646

47-
this._locker = lockers[this.options.lock]
48-
assert(this._locker, 'Unknown lock type: ' + this.options.lock)
47+
this._locker = this._getLocker()
4948

5049
this.root = backends.create('root', this.path, this.options)
5150
this.version = version(this.root)
@@ -88,7 +87,7 @@ class IpfsRepo {
8887
waterfall([
8988
(cb) => this.root.open(ignoringAlreadyOpened(cb)),
9089
(cb) => this._isInitialized(cb),
91-
(cb) => this._locker.lock(this.path, cb),
90+
(cb) => this._openLock(this.path, cb),
9291
(lck, cb) => {
9392
log('aquired repo.lock')
9493
this.lockfile = lck
@@ -121,7 +120,7 @@ class IpfsRepo {
121120
}
122121
], (err) => {
123122
if (err && this.lockfile) {
124-
this.lockfile.close((err2) => {
123+
this._closeLock((err2) => {
125124
if (!err2) {
126125
this.lockfile = null
127126
} else {
@@ -135,6 +134,61 @@ class IpfsRepo {
135134
})
136135
}
137136

137+
/**
138+
* Returns the repo locker to be used. Null will be returned if no locker is requested
139+
*
140+
* @private
141+
* @returns {Locker}
142+
*/
143+
_getLocker () {
144+
if (typeof this.options.lock === 'string') {
145+
assert(lockers[this.options.lock], 'Unknown lock type: ' + this.options.lock)
146+
return lockers[this.options.lock]
147+
}
148+
149+
assert(this.options.lock, 'No lock provided')
150+
return this.options.lock
151+
}
152+
153+
/**
154+
* Creates a lock on the repo if a locker is specified. The lockfile object will
155+
* be returned in the callback if one has been created.
156+
*
157+
* @param {string} path
158+
* @param {function(Error, lockfile)} callback
159+
* @returns {void}
160+
*/
161+
_openLock (path, callback) {
162+
this._locker.lock(path, callback)
163+
}
164+
165+
/**
166+
* Closes the lock on the repo
167+
*
168+
* @param {function(Error)} callback
169+
* @returns {void}
170+
*/
171+
_closeLock (callback) {
172+
if (this.lockfile) {
173+
return this.lockfile.close(callback)
174+
}
175+
callback()
176+
}
177+
178+
/**
179+
* Gets the status of the lock on the repo
180+
*
181+
* @param {string} path
182+
* @param {function(Error, boolean)} callback
183+
* @returns {void}
184+
*/
185+
_isLocked (path, callback) {
186+
if (this._locker) {
187+
return this._locker.locked(path, callback)
188+
}
189+
callback(null, false)
190+
}
191+
138192
/**
139193
* Check if the repo is already initialized.
140194
*
@@ -186,7 +240,7 @@ class IpfsRepo {
186240
(cb) => {
187241
log('unlocking')
188242
this.closed = true
189-
this.lockfile.close(cb)
243+
this._closeLock(cb)
190244
},
191245
(cb) => {
192246
this.lockfile = null

test/lock-test.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* eslint-env mocha */
2+
'use strict'
3+
4+
const chai = require('chai')
5+
chai.use(require('dirty-chai'))
6+
const expect = chai.expect
7+
const series = require('async/series')
8+
const IPFSRepo = require('../')
9+
10+
module.exports = (repo) => {
11+
describe('Repo lock tests', () => {
12+
it('should handle locking for a repo lifecycle', (done) => {
13+
expect(repo.lockfile).to.not.equal(null)
14+
series([
15+
(cb) => {
16+
repo.close(cb)
17+
},
18+
(cb) => {
19+
expect(repo.lockfile).to.equal(null)
20+
cb()
21+
},
22+
(cb) => {
23+
repo.open(cb)
24+
}
25+
], done)
26+
})
27+
28+
it('should prevent multiple repos from using the same path', (done) => {
29+
const repoClone = new IPFSRepo(repo.path, repo.options)
30+
31+
// Levelup throws an uncaughtException when a lock already exists, catch it
32+
const mochaExceptionHandler = process.listeners('uncaughtException').pop()
33+
process.removeListener('uncaughtException', mochaExceptionHandler)
34+
process.once('uncaughtException', function (err) {
35+
expect(err.message).to.match(/already held/)
36+
})
37+
38+
series([
39+
(cb) => {
40+
try {
41+
repoClone.init({}, cb)
42+
} catch (err) {
43+
cb(err)
44+
}
45+
},
46+
(cb) => {
47+
repoClone.open(cb)
48+
}
49+
], function (err) {
50+
// There will be no listeners if the uncaughtException was triggered
51+
if (process.listeners('uncaughtException').length > 0) {
52+
expect(err.message).to.match(/already locked|already held|ENOENT/)
53+
}
54+
55+
// Reset listeners to maintain test integrity
56+
process.removeAllListeners('uncaughtException')
57+
process.addListener('uncaughtException', mochaExceptionHandler)
58+
59+
done()
60+
})
61+
})
62+
})
63+
}

test/node.js

+37
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
const ncp = require('ncp').ncp
55
const rimraf = require('rimraf')
6+
const fs = require('fs')
67
const path = require('path')
78
const series = require('async/series')
89
const chai = require('chai')
@@ -13,6 +14,35 @@ const IPFSRepo = require('../src')
1314
describe('IPFS Repo Tests onNode.js', () => {
1415
require('./options-test')
1516

17+
const customLock = {
18+
lockName: 'test.lock',
19+
lock: (dir, callback) => {
20+
customLock.locked(dir, (err, isLocked) => {
21+
if (err || isLocked) {
22+
return callback(new Error('already locked'))
23+
}
24+
25+
let lockPath = path.join(dir, customLock.lockName)
26+
fs.writeFileSync(lockPath, '')
27+
28+
callback(null, {
29+
close: (cb) => {
30+
rimraf(lockPath, cb)
31+
}
32+
})
33+
})
34+
},
35+
locked: (dir, callback) => {
36+
fs.stat(path.join(dir, customLock.lockName), (err, stats) => {
37+
if (err) {
38+
callback(null, false)
39+
} else {
40+
callback(null, true)
41+
}
42+
})
43+
}
44+
}
45+
1646
const repos = [{
1747
name: 'default inited',
1848
opts: undefined,
@@ -25,6 +55,12 @@ describe('IPFS Repo Tests onNode.js', () => {
2555
lock: 'memory'
2656
},
2757
init: true
58+
}, {
59+
name: 'custom locker',
60+
opts: {
61+
lock: customLock
62+
},
63+
init: true
2864
}, {
2965
name: 'default existing',
3066
opts: undefined,
@@ -62,6 +98,7 @@ describe('IPFS Repo Tests onNode.js', () => {
6298
require('./datastore-test')(repo)
6399
require('./keystore-test')(repo)
64100
require('./stat-test')(repo)
101+
require('./lock-test')(repo)
65102
if (!r.init) {
66103
require('./interop-test')(repo)
67104
}

test/options-test.js

+13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ describe('custom options tests', () => {
2828
const repo = new Repo(repoPath)
2929
expect(repo.options).to.deep.equal(expectedRepoOptions())
3030
})
31+
32+
it('allows for a custom locker', () => {
33+
const lock = {
34+
lock: (path, callback) => { },
35+
locked: (path, callback) => { }
36+
}
37+
38+
const repo = new Repo(repoPath, {
39+
lock
40+
})
41+
42+
expect(repo._getLocker()).to.deep.equal(lock)
43+
})
3144
})
3245

3346
function noop () {}

0 commit comments

Comments
 (0)