Skip to content

Commit 7dca766

Browse files
committed
Implement explicit snapshots
See Level/community#118. Depends on Level/supports#32. Category: addition
1 parent 2268eaa commit 7dca766

17 files changed

+637
-45
lines changed

Diff for: README.md

+34-24
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Get a value from the database by `key`. The optional `options` object may contai
127127

128128
- `keyEncoding`: custom key encoding for this operation, used to encode the `key`.
129129
- `valueEncoding`: custom value encoding for this operation, used to decode the value.
130-
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshot) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
130+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
131131

132132
Returns a promise for the value. If the `key` was not found then the value will be `undefined`.
133133

@@ -137,7 +137,7 @@ Get multiple values from the database by an array of `keys`. The optional `optio
137137

138138
- `keyEncoding`: custom key encoding for this operation, used to encode the `keys`.
139139
- `valueEncoding`: custom value encoding for this operation, used to decode values.
140-
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshot) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
140+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot for this operation.
141141

142142
Returns a promise for an array of values with the same order as `keys`. If a key was not found, the relevant value will be `undefined`.
143143

@@ -233,7 +233,7 @@ The `gte` and `lte` range options take precedence over `gt` and `lt` respectivel
233233
- `keyEncoding`: custom key encoding for this iterator, used to encode range options, to encode `seek()` targets and to decode keys.
234234
- `valueEncoding`: custom value encoding for this iterator, used to decode values.
235235
- `signal`: an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) to [abort read operations on the iterator](#aborting-iterators).
236-
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshot) for the iterator to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot before returning an iterator.
236+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) for the iterator to read from. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database will create its own internal snapshot before returning an iterator.
237237

238238
Lastly, an implementation is free to add its own options.
239239

@@ -276,7 +276,7 @@ Delete all entries or a range. Not guaranteed to be atomic. Returns a promise. A
276276
- `reverse` (boolean, default: `false`): delete entries in reverse order. Only effective in combination with `limit`, to delete the last N entries.
277277
- `limit` (number, default: `Infinity`): limit the number of entries to be deleted. This number represents a _maximum_ number of entries and will not be reached if the end of the range is reached first. A value of `Infinity` or `-1` means there is no limit. When `reverse` is true the entries with the highest keys will be deleted instead of the lowest keys.
278278
- `keyEncoding`: custom key encoding for this operation, used to encode range options.
279-
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshot) to read from, such that entries not present in the snapshot will not be deleted. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database may create its own internal snapshot but (unlike on other methods) this is currently not a hard requirement for implementations.
279+
- `snapshot`: explicit [snapshot](#snapshot--dbsnapshotoptions) to read from, such that entries not present in the snapshot will not be deleted. If no `snapshot` is provided and `db.supports.implicitSnapshots` is true, the database may create its own internal snapshot but (unlike on other methods) this is currently not a hard requirement for implementations.
280280

281281
The `gte` and `lte` range options take precedence over `gt` and `lt` respectively. If no options are provided, all entries will be deleted.
282282

@@ -383,11 +383,15 @@ console.log(nested.prefixKey('a', 'utf8')) // '!example!!nested!a'
383383
console.log(nested.prefixKey('a', 'utf8', true)) // '!nested!a'
384384
```
385385

386-
### `snapshot = db.snapshot()`
386+
### `snapshot = db.snapshot(options)`
387387

388-
**This is an experimental API and not widely supported at the time of writing ([Level/community#118](https://github.com/Level/community/issues/118)).**
388+
**This is an experimental API ([Level/community#118](https://github.com/Level/community/issues/118)).**
389389

390-
Create an explicit [snapshot](#snapshot). Throws a [`LEVEL_NOT_SUPPORTED`](#level_not_supported) error if `db.supports.explicitSnapshots` is not true. For details, see [Reading From Snapshots](#reading-from-snapshots).
390+
Create an explicit [snapshot](#snapshot). Throws a [`LEVEL_NOT_SUPPORTED`](#level_not_supported) error if `db.supports.explicitSnapshots` is false. For details, see [Reading From Snapshots](#reading-from-snapshots).
391+
392+
There are currently no options but specific implementations may add their own.
393+
394+
Don't forget to call `snapshot.close()` when done.
391395

392396
### `db.supports`
393397

@@ -705,15 +709,21 @@ console.log(foo.path(true)) // ['example', 'nested', 'foo']
705709

706710
### `snapshot`
707711

708-
#### `snapshot.close()`
712+
#### `snapshot.ref()`
709713

710-
Free up underlying resources. Be sure to call this when the snapshot is no longer needed, because snapshots may cause the database to temporarily pause internal storage optimizations. Returns a promise. Closing the snapshot is an idempotent operation, such that calling `snapshot.close()` more than once is allowed and makes no difference.
714+
Increment reference count, to register work that should delay closing until `snapshot.unref()` is called an equal amount of times. The promise that will be returned by `snapshot.close()` will not resolve until the reference count returns to 0. This prevents prematurely closing underlying resources while the snapshot is in use.
711715

712-
After `snapshot.close()` has been called, no further operations are allowed. For example, `db.get(key, { snapshot })` will yield an error with code [`LEVEL_SNAPSHOT_NOT_OPEN`](#level_snapshot_not_open). Any unclosed iterators (that use this snapshot) will be closed by `snapshot.close()` and can then no longer be used.
716+
It is normally not necessary to call `snapshot.ref()` and `snapshot.unref()` because builtin database methods automatically do.
717+
718+
#### `snapshot.unref()`
719+
720+
Decrement reference count, to indicate that the work has finished.
721+
722+
#### `snapshot.close()`
713723

714-
#### `snapshot.db`
724+
Free up underlying resources. Be sure to call this when the snapshot is no longer needed, because snapshots may cause the database to temporarily pause internal storage optimizations. Returns a promise. Closing the snapshot is an idempotent operation, such that calling `snapshot.close()` more than once is allowed and makes no difference.
715725

716-
A reference to the database that created this snapshot.
726+
After `snapshot.close()` has been called, no further operations are allowed. For example, `db.get(key, { snapshot })` will throw an error with code [`LEVEL_SNAPSHOT_NOT_OPEN`](#level_snapshot_not_open).
717727

718728
### Encodings
719729

@@ -950,10 +960,10 @@ Removing this concern (if necessary) must be done on an application-level. For e
950960

951961
### Reading From Snapshots
952962

953-
A snapshot is a lightweight "token" that represents the version of a database at a particular point in time. This allows for reading data without seeing subsequent writes made on the database. It comes in two forms:
963+
A snapshot is a lightweight "token" that represents a version of a database at a particular point in time. This allows for reading data without seeing subsequent writes made on the database. It comes in two forms:
954964

955965
1. Implicit snapshots: created internally by the database and not visible to the outside world.
956-
2. Explicit snapshots: created with `snapshot = db.snapshot()`. Because it acts as a token, `snapshot` has no methods of its own besides `snapshot.close()`. Instead the snapshot is to be passed to database (or [sublevel](#sublevel)) methods like `db.iterator()`.
966+
2. Explicit snapshots: created with `snapshot = db.snapshot()`. Because it acts as a token, `snapshot` has no read methods of its own. Instead the snapshot is to be passed to database methods like `db.get()` and `db.iterator()`. This also works on sublevels.
957967

958968
Use explicit snapshots wisely, because their lifetime must be managed manually. Implicit snapshots are typically more convenient and possibly more performant because they can handled natively and have their lifetime limited by the surrounding operation. That said, explicit snapshots can be useful to make multiple read operations that require a shared, consistent view of the data.
959969

@@ -1625,19 +1635,19 @@ class ExampleSublevel extends AbstractSublevel {
16251635
}
16261636
```
16271637

1628-
### `snapshot = db._snapshot()`
1638+
### `snapshot = db._snapshot(options)`
1639+
1640+
Create a snapshot. The `options` argument is guaranteed to be an object. There are currently no options but implementations may add their own.
16291641

1630-
The default `_snapshot()` throws a [`LEVEL_NOT_SUPPORTED`](#errors) error. To implement this method, extend `AbstractSnapshot`, return an instance of this class in an overridden `_snapshot()` method and set `manifest.explicitSnapshots` to `true`:
1642+
The default `_snapshot()` throws a [`LEVEL_NOT_SUPPORTED`](#level_not_supported) error. To implement this method, extend `AbstractSnapshot`, return an instance of this class in an overridden `_snapshot()` method and set `manifest.explicitSnapshots` to `true`:
16311643

16321644
```js
16331645
const { AbstractSnapshot } = require('abstract-level')
16341646

16351647
class ExampleSnapshot extends AbstractSnapshot {
1636-
constructor (db) {
1637-
super(db)
1648+
constructor (options) {
1649+
super(options)
16381650
}
1639-
1640-
// ..
16411651
}
16421652

16431653
class ExampleLevel extends AbstractLevel {
@@ -1650,8 +1660,8 @@ class ExampleLevel extends AbstractLevel {
16501660
super(manifest, options)
16511661
}
16521662

1653-
_snapshot () {
1654-
return new ExampleSnapshot(this)
1663+
_snapshot (options) {
1664+
return new ExampleSnapshot(options)
16551665
}
16561666
}
16571667
```
@@ -1762,11 +1772,11 @@ The default `_close()` returns a resolved promise. Overriding is optional.
17621772

17631773
### `snapshot = new AbstractSnapshot(db)`
17641774

1765-
The first argument to this constructor must be an instance of the relevant `AbstractLevel` implementation. The constructor will set `snapshot.db` which ensures that `db` will not be garbage collected in case there are no other references to it.
1775+
The first argument to this constructor must be an instance of the relevant `AbstractLevel` implementation.
17661776

17671777
#### `snapshot._close()`
17681778

1769-
Free up underlying resources. This method is guaranteed to only be called once. Must return a promise.
1779+
Free up underlying resources. This method is guaranteed to only be called once and will not be called while read operations like `db._get()` are inflight. Must return a promise.
17701780

17711781
The default `_close()` returns a resolved promise. Overriding is optional.
17721782

Diff for: abstract-iterator.js

+12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const kValues = Symbol('values')
1818
const kLimit = Symbol('limit')
1919
const kCount = Symbol('count')
2020
const kEnded = Symbol('ended')
21+
const kSnapshot = Symbol('snapshot')
2122

2223
// This class is an internal utility for common functionality between AbstractIterator,
2324
// AbstractKeyIterator and AbstractValueIterator. It's not exported.
@@ -40,6 +41,7 @@ class CommonIterator {
4041
this[kLimit] = Number.isInteger(options.limit) && options.limit >= 0 ? options.limit : Infinity
4142
this[kCount] = 0
4243
this[kSignal] = options.signal != null ? options.signal : null
44+
this[kSnapshot] = options.snapshot != null ? options.snapshot : null
4345

4446
// Ending means reaching the natural end of the data and (unlike closing) that can
4547
// be reset by seek(), unless the limit was reached.
@@ -363,6 +365,11 @@ const startWork = function (iterator) {
363365
}
364366

365367
iterator[kWorking] = true
368+
369+
// Keep snapshot open during operation
370+
if (iterator[kSnapshot] !== null) {
371+
iterator[kSnapshot].ref()
372+
}
366373
}
367374

368375
const endWork = function (iterator) {
@@ -371,6 +378,11 @@ const endWork = function (iterator) {
371378
if (iterator[kPendingClose] !== null) {
372379
iterator[kPendingClose]()
373380
}
381+
382+
// Release snapshot
383+
if (iterator[kSnapshot] !== null) {
384+
iterator[kSnapshot].unref()
385+
}
374386
}
375387

376388
const privateClose = async function (iterator) {

Diff for: abstract-level.js

+76-4
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@ class AbstractLevel extends EventEmitter {
5151
this[kStatusChange] = null
5252
this[kStatusLocked] = false
5353

54+
// Aliased for backwards compatibility
55+
const implicitSnapshots = manifest.snapshots !== false &&
56+
manifest.implicitSnapshots !== false
57+
5458
this.hooks = new DatabaseHooks()
5559
this.supports = supports(manifest, {
5660
deferredOpen: true,
5761

5862
// TODO (next major): add seek
59-
snapshots: manifest.snapshots !== false,
63+
implicitSnapshots,
6064
permanence: manifest.permanence !== false,
6165

6266
encodings: manifest.encodings || {},
@@ -107,6 +111,9 @@ class AbstractLevel extends EventEmitter {
107111
}),
108112
keyFormat: Object.freeze({
109113
keyEncoding: this[kKeyEncoding].format
114+
}),
115+
owner: Object.freeze({
116+
owner: this
110117
})
111118
}
112119

@@ -323,6 +330,7 @@ class AbstractLevel extends EventEmitter {
323330
const err = this._checkKey(key)
324331
if (err) throw err
325332

333+
const snapshot = options.snapshot != null ? options.snapshot : null
326334
const keyEncoding = this.keyEncoding(options.keyEncoding)
327335
const valueEncoding = this.valueEncoding(options.valueEncoding)
328336
const keyFormat = keyEncoding.format
@@ -335,7 +343,23 @@ class AbstractLevel extends EventEmitter {
335343
}
336344

337345
const encodedKey = keyEncoding.encode(key)
338-
const value = await this._get(this.prefixKey(encodedKey, keyFormat, true), options)
346+
const mappedKey = this.prefixKey(encodedKey, keyFormat, true)
347+
348+
// Keep snapshot open during operation
349+
if (snapshot !== null) {
350+
snapshot.ref()
351+
}
352+
353+
let value
354+
355+
try {
356+
value = await this._get(mappedKey, options)
357+
} finally {
358+
// Release snapshot
359+
if (snapshot !== null) {
360+
snapshot.unref()
361+
}
362+
}
339363

340364
try {
341365
return value === undefined ? value : valueEncoding.decode(value)
@@ -368,6 +392,7 @@ class AbstractLevel extends EventEmitter {
368392
return []
369393
}
370394

395+
const snapshot = options.snapshot != null ? options.snapshot : null
371396
const keyEncoding = this.keyEncoding(options.keyEncoding)
372397
const valueEncoding = this.valueEncoding(options.valueEncoding)
373398
const keyFormat = keyEncoding.format
@@ -388,7 +413,21 @@ class AbstractLevel extends EventEmitter {
388413
mappedKeys[i] = this.prefixKey(keyEncoding.encode(key), keyFormat, true)
389414
}
390415

391-
const values = await this._getMany(mappedKeys, options)
416+
// Keep snapshot open during operation
417+
if (snapshot !== null) {
418+
snapshot.ref()
419+
}
420+
421+
let values
422+
423+
try {
424+
values = await this._getMany(mappedKeys, options)
425+
} finally {
426+
// Release snapshot
427+
if (snapshot !== null) {
428+
snapshot.unref()
429+
}
430+
}
392431

393432
try {
394433
for (let i = 0; i < values.length; i++) {
@@ -716,12 +755,26 @@ class AbstractLevel extends EventEmitter {
716755

717756
const original = options
718757
const keyEncoding = this.keyEncoding(options.keyEncoding)
758+
const snapshot = options.snapshot != null ? options.snapshot : null
719759

720760
options = rangeOptions(options, keyEncoding)
721761
options.keyEncoding = keyEncoding.format
722762

723763
if (options.limit !== 0) {
724-
await this._clear(options)
764+
// Keep snapshot open during operation
765+
if (snapshot !== null) {
766+
snapshot.ref()
767+
}
768+
769+
try {
770+
await this._clear(options)
771+
} finally {
772+
// Release snapshot
773+
if (snapshot !== null) {
774+
snapshot.unref()
775+
}
776+
}
777+
725778
this.emit('clear', original)
726779
}
727780
}
@@ -809,6 +862,25 @@ class AbstractLevel extends EventEmitter {
809862
return new DefaultValueIterator(this, options)
810863
}
811864

865+
snapshot (options) {
866+
assertOpen(this)
867+
868+
// Owner is an undocumented option explained in AbstractSnapshot
869+
if (typeof options !== 'object' || options === null) {
870+
options = this[kDefaultOptions].owner
871+
} else if (options.owner == null) {
872+
options = { ...options, owner: this }
873+
}
874+
875+
return this._snapshot(options)
876+
}
877+
878+
_snapshot (options) {
879+
throw new ModuleError('Database does not support explicit snapshots', {
880+
code: 'LEVEL_NOT_SUPPORTED'
881+
})
882+
}
883+
812884
defer (fn, options) {
813885
if (typeof fn !== 'function') {
814886
throw new TypeError('The first argument must be a function')

0 commit comments

Comments
 (0)