autobee 2.7.1 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -285,9 +285,9 @@ Pass one of `head` or `legacy`, not both.
285
285
 
286
286
  #### `fastForward.conservative`
287
287
 
288
- Defaults to `true`: only fast-forward onto a head a connected peer can serve whole.
288
+ Defaults to `true`: only fast-forward once the system and view lengths the head stamps are held whole, either locally or by a connected peer.
289
289
 
290
- The check covers the oplog head only, not the system and view cores the fast-forward then reads.
290
+ The check runs at the end of the fast-forward, after the system has booted and the view head has been fetched, so a sparse view nobody can serve is skipped instead of landed on.
291
291
 
292
292
  #### `await db.moveTo(head, [options])`
293
293
 
package/index.js CHANGED
@@ -98,8 +98,8 @@ module.exports = class Autobee extends ReadyResource {
98
98
  // oplog head to boot from: migrates or fast-forwards depending on its version
99
99
  this.bootFrom = (fastForward && fastForward.boot) || null
100
100
 
101
- // conservative (default on): only fast-forward onto a head someone can serve whole
102
- this._conservativeFF = !fastForward || !!fastForward.conservative
101
+ // conservative (default on): only fast-forward onto a system and view held whole
102
+ this._conservativeFF = fastForward === null || fastForward.conservative !== false
103
103
 
104
104
  this.trusted = new TrustedPeers(this, handlers)
105
105
 
@@ -828,7 +828,7 @@ module.exports = class Autobee extends ReadyResource {
828
828
 
829
829
  for (const { key, length } of hints) {
830
830
  if (length === 0) continue
831
- promises.push(this._resolveOplogHint(key, length, timeout ? { timeout } : null))
831
+ promises.push(this._resolveOplogHint(key, length, { timeout }))
832
832
  }
833
833
 
834
834
  const ops = await Promise.all(promises)
@@ -867,7 +867,7 @@ module.exports = class Autobee extends ReadyResource {
867
867
  }
868
868
  }
869
869
 
870
- async _getOplog(key, length, opts = null) {
870
+ async _getOplog(key, length, opts) {
871
871
  const core = this.openCore(key)
872
872
 
873
873
  try {
@@ -878,17 +878,12 @@ module.exports = class Autobee extends ReadyResource {
878
878
  }
879
879
 
880
880
  // reads on a core the caller owns, so a walk does not churn a session per step
881
- async readOplog(core, length, opts = null) {
881
+ async readOplog(core, length, { timeout = 0 } = {}) {
882
882
  await core.ready()
883
883
 
884
- const { conservative = false, timeout } = opts === null ? {} : opts
885
-
886
884
  const target = length >= 0 ? length : core.length
887
885
  if (target === 0) return null
888
886
 
889
- // conservative: only proceed when a connected peer can serve the head whole
890
- if (conservative && core.remoteContiguousLength < target) return null
891
-
892
887
  const buf = await core.get(target - 1, { timeout })
893
888
  if (buf === null) return null
894
889
 
@@ -1218,16 +1213,17 @@ module.exports = class Autobee extends ReadyResource {
1218
1213
  return t
1219
1214
  }
1220
1215
 
1221
- async applyBacklog(batches) {
1222
- const queue = batches.slice()
1216
+ async _processBatch(batch) {
1217
+ // a stack: the tip is pushed in reverse so it pops in order
1218
+ const stack = [batch]
1223
1219
 
1224
- while (queue.length) {
1225
- const batch = queue.shift()
1220
+ while (stack.length) {
1221
+ const batch = stack.pop()
1226
1222
  const t = await this.prepareBatch(batch)
1227
1223
 
1228
1224
  if (t.view) {
1229
1225
  this._workingBee.move(t.view)
1230
- queue.unshift(...t.tip)
1226
+ for (let i = t.tip.length - 1; i >= 0; i--) stack.push(t.tip[i])
1231
1227
  continue
1232
1228
  }
1233
1229
 
@@ -1237,15 +1233,12 @@ module.exports = class Autobee extends ReadyResource {
1237
1233
 
1238
1234
  await this._applyBatch(batch, batch[0].optimistic)
1239
1235
 
1240
- this.writers.triggers.trigger(
1241
- b4a.toString(batch[0].key, 'hex'),
1242
- batch[batch.length - 1].length
1243
- )
1244
- }
1245
- }
1236
+ // trigger any upstream writer waiting for this batch early
1237
+ const id = b4a.toString(batch[0].key, 'hex')
1238
+ const length = batch[batch.length - 1].length
1246
1239
 
1247
- async _processBatch(batch) {
1248
- await this.applyBacklog([batch])
1240
+ this.writers.triggers.trigger(id, length)
1241
+ }
1249
1242
  }
1250
1243
 
1251
1244
  async _applyBatch(batch, optimistic) {
@@ -10,9 +10,12 @@ const DEFAULT_OP_TIMEOUT = 5_000
10
10
  const MIN_FF_GAP = 32
11
11
 
12
12
  class FastForward {
13
- constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
13
+ constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT, conservative = false } = {}) {
14
14
  this.auto = auto
15
15
 
16
+ // conservative: only land when the system and view we booted are held whole
17
+ this.conservative = conservative
18
+
16
19
  this.activeRequests = []
17
20
 
18
21
  // 'reboot' is a storage namespace: renaming it would derive a different
@@ -38,15 +41,11 @@ class FastForward {
38
41
 
39
42
  static async fromHead(auto, head, trusted, { force = false, timeout = 0 } = {}) {
40
43
  const conservative = !force && auto._conservativeFF
41
- const timeoutOpts = timeout ? { timeout } : null
42
44
 
43
- const oplog = await FastForward.flushHead(auto, head, {
44
- conservative,
45
- ...timeoutOpts
46
- })
45
+ const oplog = await FastForward.flushHead(auto, head, { timeout })
47
46
  if (!oplog) return null
48
47
 
49
- const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
48
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, { timeout }) : oplog
50
49
  if (!verified) return null
51
50
 
52
51
  // legacy nodes from non-indexers have no system info to fast-forward from
@@ -57,7 +56,8 @@ class FastForward {
57
56
  }
58
57
 
59
58
  return new FastForward(auto, batchToHead(verified.op.views.system), {
60
- timeout: timeout || DEFAULT_OP_TIMEOUT
59
+ timeout: timeout || DEFAULT_OP_TIMEOUT,
60
+ conservative
61
61
  })
62
62
  }
63
63
 
@@ -68,7 +68,7 @@ class FastForward {
68
68
 
69
69
  for (const head of heads) {
70
70
  if (head.length === 0) continue
71
- promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
71
+ promises.push(FastForward.flushHead(auto, head, { timeout }))
72
72
  }
73
73
 
74
74
  const ops = await Promise.all(promises)
@@ -180,8 +180,10 @@ class FastForward {
180
180
  promises.push(this.system.get(this.auto.local.key, { timeout: this.timeout }))
181
181
 
182
182
  // a legacy system without the designated view migrates onto an empty view
183
+ let view = null
184
+
183
185
  if (this.system.view.length) {
184
- const view = this.auto.store.get({ key: this.system.view.key, active: true })
186
+ view = this.auto.store.get({ key: this.system.view.key, active: true })
185
187
  this.cores.push(view)
186
188
 
187
189
  promises.push(
@@ -199,6 +201,13 @@ class FastForward {
199
201
  await Promise.all(promises)
200
202
  if (this.destroyed) return null
201
203
 
204
+ // conservative: the lengths we land on must be held whole, either by us or
205
+ // by a connected peer, so the view does not stall on a block nobody has
206
+ if (this.conservative) {
207
+ if (!isContiguous(this.system.bee.core, this.head.length)) return null
208
+ if (view !== null && !isContiguous(view, this.system.view.length)) return null
209
+ }
210
+
202
211
  return {
203
212
  head: this.head,
204
213
  migrate: null
@@ -360,6 +369,10 @@ async function findNewestSystem(ff, info) {
360
369
  return { legacy, autobee }
361
370
  }
362
371
 
372
+ function isContiguous(core, length) {
373
+ return core.contiguousLength >= length || core.remoteContiguousLength >= length
374
+ }
375
+
363
376
  function batchToHead(b) {
364
377
  return {
365
378
  key: b.key,
package/lib/triggers.js CHANGED
@@ -1,9 +1,14 @@
1
+ // writers waiting on a node of another writer, keyed by that writer's id.
2
+ // a writer waits on at most one entry at a time, tracked as writer.waiting
1
3
  module.exports = class Triggers {
2
4
  constructor() {
3
5
  this.waiting = new Map()
4
6
  }
5
7
 
6
8
  add(id, length, writer) {
9
+ // a writer only ever waits on one link: drop whatever it waited on before
10
+ if (writer.waiting !== null && writer.waiting.id !== id) this.remove(writer.waiting)
11
+
7
12
  let w = this.waiting.get(id)
8
13
 
9
14
  if (!w) {
@@ -24,6 +29,7 @@ module.exports = class Triggers {
24
29
  }
25
30
 
26
31
  remove(entry) {
32
+ // writer is nulled on removal, so a removed entry is a noop here
27
33
  if (!entry.writer) return
28
34
 
29
35
  const w = this.waiting.get(entry.id)
@@ -42,14 +48,18 @@ module.exports = class Triggers {
42
48
  const w = this.waiting.get(id)
43
49
  if (!w) return
44
50
 
51
+ // collect first: the walk swap-removes, so nothing may touch the array mid-loop
52
+ const bump = []
53
+
45
54
  for (let i = w.length - 1; i >= 0; i--) {
46
55
  const entry = w[i]
47
56
  if (entry.length > length) continue
48
57
 
49
- const writer = entry.writer
58
+ bump.push(entry.writer)
50
59
  this._remove(w, entry)
51
- writer.bump()
52
60
  }
61
+
62
+ for (const writer of bump) writer.bump()
53
63
  }
54
64
 
55
65
  _remove(w, entry) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",