autobee 2.3.1 → 2.4.0

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
@@ -81,6 +81,7 @@ Options:
81
81
  optimistic: true, // allow optimistic writes from unknown writers
82
82
  isTrusted (key, reference) {}, // do we trust this writer, see Fast-forward
83
83
  mostRecentTrusted (target, reference) {}, // the head we vouch for, see Fast-forward
84
+ ackThreshold: 32, // flushes we may fall behind before acking, see Acking
84
85
  fastForward: {} // see Fast-forward
85
86
  }
86
87
  ```
@@ -162,6 +163,10 @@ Hint that a new writer core is available at `key` with at least `length` entries
162
163
 
163
164
  Rotate the local writer to a different key. The new writer takes over as the active oplog.
164
165
 
166
+ #### `db.setAcking(acking, [options])`
167
+
168
+ Override acking, which is otherwise driven by `isTrusted`, see Acking. `options.threshold` defaults to the current threshold.
169
+
165
170
  #### `views = db.views()`
166
171
 
167
172
  Returns the current system and view core positions. Used for replication coordination.
@@ -270,6 +275,14 @@ Fast-forward onto an oplog head, ignoring the usual distance and conservative ch
270
275
 
271
276
  Pass `{ timeout }` to bound the reads, so a head nobody can serve fails instead of hanging.
272
277
 
278
+ ### Acking
279
+
280
+ Peers fast-forward onto the heads of writers they trust, so a trusted writer that has nothing to say still has to stamp its view progress into its own oplog.
281
+
282
+ Acking does that: an empty node is appended whenever the local writer falls `ackThreshold` (default `64`) flushes behind the system.
283
+
284
+ It is enabled for exactly the writers `isTrusted` accepts. The local key is judged when the db opens, after a fast-forward, and after the local writer rotates, so a writer that becomes trusted starts acking without being told to. `setAcking` overrides it until the next of those points.
285
+
273
286
  ### Encryption
274
287
 
275
288
  Pass an `encryptionKey` to encrypt all writer cores and the view at rest.
package/index.js CHANGED
@@ -25,6 +25,7 @@ const UpdateChanges = require('./lib/updates.js')
25
25
  const migrations = require('./lib/migrations.js')
26
26
 
27
27
  const EMPTY_HEAD = { length: 0, key: null }
28
+ const DEFAULT_ACK_THRESHOLD = 32
28
29
  const INTERRUPT = new Error('Apply interrupted')
29
30
 
30
31
  module.exports = class Autobee extends ReadyResource {
@@ -112,6 +113,9 @@ module.exports = class Autobee extends ReadyResource {
112
113
  this._localSystemStart = 0
113
114
  this._localSystemLength = 0
114
115
  this._localFlushes = 0
116
+ this._acking = false
117
+ this._ackThreshold = handlers.ackThreshold || DEFAULT_ACK_THRESHOLD
118
+ this._ackFlushes = -1
115
119
  this._localViewStart = 0
116
120
  this._localViewLength = 0
117
121
 
@@ -193,6 +197,18 @@ module.exports = class Autobee extends ReadyResource {
193
197
  }
194
198
  }
195
199
 
200
+ setAcking(acking, { threshold = this._ackThreshold } = {}) {
201
+ this._acking = acking !== false && threshold > 0
202
+ this._ackThreshold = threshold
203
+
204
+ if (this._acking) this.bumpSoon()
205
+ }
206
+
207
+ async _updateAcking() {
208
+ if (this._interrupting) return
209
+ this.setAcking(await this.trusted.isTrusted(this.local.key, this.view))
210
+ }
211
+
196
212
  // network free: only a migration needs peers, so ready() awaits the full
197
213
  // state boot unless one is pending and defers it to the background if so -
198
214
  // anything needing the booted system must wait on the boot guard
@@ -468,6 +484,8 @@ module.exports = class Autobee extends ReadyResource {
468
484
 
469
485
  this._bootGuard.exit()
470
486
 
487
+ await this._updateAcking()
488
+
471
489
  this.bumpSoon()
472
490
 
473
491
  return this._bootGuard.ready()
@@ -672,7 +690,12 @@ module.exports = class Autobee extends ReadyResource {
672
690
  break // revaluate conditions...
673
691
  }
674
692
 
675
- if (!(await this._bumpPendingWriters())) break
693
+ if (await this._bumpPendingWriters()) {
694
+ this._needsUpdate = true
695
+ continue
696
+ }
697
+
698
+ if (!(await this._appendAck())) break
676
699
  this._needsUpdate = true
677
700
  }
678
701
 
@@ -922,6 +945,7 @@ module.exports = class Autobee extends ReadyResource {
922
945
  await this.writers.rotateLocalWriter(this.local)
923
946
 
924
947
  this._updateLocalCore = null
948
+ this._ackFlushes = -1
925
949
 
926
950
  this.local.setUserData('referrer', this.key)
927
951
  if (this.encryptionKey) {
@@ -931,6 +955,8 @@ module.exports = class Autobee extends ReadyResource {
931
955
  await this.bootstrap.setUserData('autobee/local', this.local.key)
932
956
  await oldLocal.close()
933
957
 
958
+ await this._updateAcking()
959
+
934
960
  // done, soft restart
935
961
  this.emit('rotate-local-writer')
936
962
  }
@@ -1101,6 +1127,30 @@ module.exports = class Autobee extends ReadyResource {
1101
1127
  }
1102
1128
  }
1103
1129
 
1130
+ async _appendAck() {
1131
+ if (!this._acking) return false
1132
+ if (!this.writers.writable) return false
1133
+
1134
+ if (this.writers.localWriter.pending !== null) return false
1135
+
1136
+ if ((await this._flushesBehind()) < this._ackThreshold) return false
1137
+
1138
+ const links = this.system.getLinks(this.local.key)
1139
+ const t = Math.max(this._now(), this.system.timestamp)
1140
+
1141
+ this.writers.appendLocal(null, t, { start: 0, end: 0 }, links, false, null)
1142
+ return true
1143
+ }
1144
+
1145
+ async _flushesBehind() {
1146
+ if (this._ackFlushes === -1) {
1147
+ const latest = await this.writers.getLatestLocalOplog()
1148
+ this._ackFlushes = latest && latest.views ? latest.views.flushes : this.system.flushes
1149
+ }
1150
+
1151
+ return this.system.flushes - this._ackFlushes
1152
+ }
1153
+
1104
1154
  async _bumpPendingWriters({ local = false } = {}) {
1105
1155
  if (!local && this._catchupMigratedNodes !== null) {
1106
1156
  const updated = await this._bumpMigratedWriters()
@@ -1358,7 +1408,7 @@ module.exports = class Autobee extends ReadyResource {
1358
1408
  this._needsUpdate = true
1359
1409
  }
1360
1410
 
1361
- await this.writers.flushLocal({
1411
+ const flushed = await this.writers.flushLocal({
1362
1412
  flushes: this._localFlushes,
1363
1413
  system: {
1364
1414
  key: this.system.bee.context.local.key,
@@ -1372,6 +1422,8 @@ module.exports = class Autobee extends ReadyResource {
1372
1422
  }
1373
1423
  })
1374
1424
 
1425
+ if (flushed) this._ackFlushes = this._localFlushes
1426
+
1375
1427
  this._localSystemStart = this.system.bee.context.local.length
1376
1428
  this._localSystemLength = 0
1377
1429
  this._localFlushes = this.system.flushes
@@ -1481,6 +1533,8 @@ module.exports = class Autobee extends ReadyResource {
1481
1533
  await this._update(changes, true)
1482
1534
  await this._storeBoot()
1483
1535
 
1536
+ await this._updateAcking()
1537
+
1484
1538
  this.stats.fastForwards++
1485
1539
  this.emit('move-to', to, from)
1486
1540
  this.ff.resolve({ to, from })
@@ -1,13 +1,15 @@
1
1
  const safetyCatch = require('safety-catch')
2
+ const b4a = require('b4a')
2
3
  const Hypercore = require('hypercore')
3
4
 
4
5
  const System = require('./system.js')
5
6
  const migrations = require('./migrations.js')
7
+ const { LEGACY_AUTOBASE_VERSION, LEGACY_OPLOG_VERSION } = require('./constants.js')
6
8
 
7
9
  const DEFAULT_OP_TIMEOUT = 5_000
8
10
  const MIN_FF_GAP = 32
9
11
 
10
- module.exports = class FastForward {
12
+ class FastForward {
11
13
  constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
12
14
  this.auto = auto
13
15
 
@@ -162,6 +164,10 @@ module.exports = class FastForward {
162
164
 
163
165
  await this.system.boot(this.head, { timeout: this.timeout })
164
166
 
167
+ if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
168
+ if (!(await this._runLegacy())) return null
169
+ }
170
+
165
171
  const promises = []
166
172
 
167
173
  // ensure local key is locally available always
@@ -202,6 +208,68 @@ module.exports = class FastForward {
202
208
  return migrations.coreLength(core, this.timeout)
203
209
  }
204
210
 
211
+ async _runLegacy() {
212
+ const resolved = await this._resolveLegacy()
213
+ if (resolved === null) return false
214
+
215
+ const { head, autobee } = resolved
216
+
217
+ if (autobee !== null) {
218
+ try {
219
+ this.head = batchToHead(autobee.op.views.system)
220
+ await this.system.boot(this.head, { timeout: this.timeout })
221
+ if (this.system.version > LEGACY_AUTOBASE_VERSION) return true
222
+ } catch (err) {
223
+ safetyCatch(err)
224
+ }
225
+ }
226
+
227
+ this.head = head
228
+ await this.system.boot(this.head, { timeout: this.timeout })
229
+
230
+ return true
231
+ }
232
+
233
+ async _resolveLegacy() {
234
+ let head = { key: this.head.key, length: this.head.length }
235
+ const seen = new Set()
236
+
237
+ // bound at 5 hops for safety
238
+ let hops = 5
239
+ while (true) {
240
+ seen.add(b4a.toString(head.key, 'hex'))
241
+
242
+ const core = this.auto.store.get({ key: head.key, encryption: null })
243
+ this.cores.push(core)
244
+ await core.ready()
245
+
246
+ await core.setEncryption(this.auto.getSystemEncryption())
247
+
248
+ // successors are resolved by key only - wait for the replicated length
249
+ if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
250
+ if (!head.length || this.destroyed) return null
251
+
252
+ const info = await migrations.readLegacySystemInfo(core, head.length, {
253
+ timeout: this.timeout,
254
+ activeRequests: this.activeRequests
255
+ })
256
+
257
+ const { legacy, autobee } = await findNewestSystem(this, info)
258
+ if (this.destroyed) return null
259
+
260
+ // an autobee candidate ends the crawl: it may be behind, but once we are
261
+ // on autobee the ordinary fast-forward carries us to the newest view
262
+ if (autobee !== null) return { head, autobee }
263
+
264
+ if (hops-- > 0 && legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
265
+ head = { key: legacy, length: 0 }
266
+ continue
267
+ }
268
+
269
+ return { head, autobee: null }
270
+ }
271
+ }
272
+
205
273
  // cancels every read in flight and fails any that come after
206
274
  clearRequests(err = null) {
207
275
  Hypercore.destroyRequests(this.activeRequests, err)
@@ -215,6 +283,50 @@ module.exports = class FastForward {
215
283
  }
216
284
  }
217
285
 
286
+ module.exports = FastForward
287
+
288
+ async function findNewestSystem(ff, info) {
289
+ let autobee = null
290
+ let legacy = null
291
+ let flushes = -1
292
+
293
+ for (const idx of info.indexers) {
294
+ const core = ff.auto.openCore(idx.key)
295
+ ff.cores.push(core)
296
+ await core.ready()
297
+
298
+ const length = await migrations.coreLength(core, ff.timeout)
299
+ if (!length || length <= idx.length || ff.destroyed) continue
300
+
301
+ const oplog = await FastForward.flushHead(
302
+ ff.auto,
303
+ { key: idx.key, length },
304
+ { timeout: ff.timeout }
305
+ )
306
+ if (oplog === null || !oplog.op.views) continue
307
+
308
+ // this indexer has already moved to autobee: follow it rather than
309
+ // digging a legacy stamp out of its history
310
+ if (oplog.op.version > LEGACY_OPLOG_VERSION) {
311
+ if (autobee === null || oplog.op.views.flushes > autobee.op.views.flushes) {
312
+ autobee = oplog
313
+ }
314
+ continue
315
+ }
316
+
317
+ const sys = oplog.op.views.system
318
+ if (!sys || !sys.key) continue
319
+
320
+ // follow whichever indexer has checkpointed furthest
321
+ if (oplog.op.views.flushes > flushes) {
322
+ flushes = oplog.op.views.flushes
323
+ legacy = sys.key
324
+ }
325
+ }
326
+
327
+ return { legacy, autobee }
328
+ }
329
+
218
330
  function batchToHead(b) {
219
331
  return {
220
332
  key: b.key,
package/lib/migrations.js CHANGED
@@ -21,6 +21,7 @@ module.exports = {
21
21
  getCoreManifest,
22
22
  getViewMap,
23
23
  checkAutobaseMigration,
24
+ readLegacySystemInfo,
24
25
  inflateLegacyOplog,
25
26
  coreLength
26
27
  }
package/lib/writers.js CHANGED
@@ -122,11 +122,12 @@ class ActiveWriters {
122
122
  const w = this.localWriter
123
123
 
124
124
  // only consume the queue when a flush will actually write
125
- if (w.pending === null || w.processed === 0) return
125
+ if (w.pending === null || w.processed === 0) return false
126
126
 
127
127
  const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
128
128
 
129
- return w.flush(views, trusted)
129
+ await w.flush(views, trusted)
130
+ return true
130
131
  }
131
132
 
132
133
  async refresh() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.3.1",
3
+ "version": "2.4.0",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",