autobee 2.0.0-rc.8 → 2.0.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 +48 -1
- package/encoding/compat.js +1 -1
- package/encoding/legacy.js +33 -8
- package/encoding/spec/autobee/index.js +312 -25
- package/encoding/spec/autobee/schema.json +228 -2
- package/index.js +555 -310
- package/lib/apply-calls.js +5 -6
- package/lib/apply-view.js +25 -0
- package/lib/boot.js +14 -182
- package/lib/constants.js +3 -6
- package/lib/encoding.js +72 -3
- package/lib/fast-forward.js +346 -0
- package/lib/migrations.js +457 -0
- package/lib/promotions.js +164 -0
- package/lib/system.js +97 -179
- package/lib/topo.js +110 -52
- package/lib/trusted.js +44 -0
- package/lib/updates.js +1 -38
- package/lib/witness.js +34 -65
- package/lib/writers.js +180 -116
- package/package.json +5 -4
- package/lib/reboot.js +0 -238
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
const safetyCatch = require('safety-catch')
|
|
2
|
+
const b4a = require('b4a')
|
|
3
|
+
const Hypercore = require('hypercore')
|
|
4
|
+
|
|
5
|
+
const System = require('./system.js')
|
|
6
|
+
const migrations = require('./migrations.js')
|
|
7
|
+
const { LEGACY_AUTOBASE_VERSION } = require('./constants.js')
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OP_TIMEOUT = 5_000
|
|
10
|
+
const MIN_FF_GAP = 32
|
|
11
|
+
|
|
12
|
+
module.exports = class FastForward {
|
|
13
|
+
constructor(auto, head, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
|
|
14
|
+
this.auto = auto
|
|
15
|
+
|
|
16
|
+
this.activeRequests = []
|
|
17
|
+
|
|
18
|
+
// 'reboot' is a storage namespace: renaming it would derive a different
|
|
19
|
+
// scratch core on disk, so it stays as is
|
|
20
|
+
this.system = new System(auto.store.namespace('reboot'), null, {
|
|
21
|
+
getEncryptionProvider: this.auto.getSystemEncryption,
|
|
22
|
+
encrypted: this.encrypted,
|
|
23
|
+
activeRequests: this.activeRequests
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// length -1 marks an uninitialised head: we know the key but not the length
|
|
27
|
+
this.head = { key: head.key, length: head.length ?? 0 }
|
|
28
|
+
this.timeout = timeout
|
|
29
|
+
this.destroyed = false
|
|
30
|
+
this.running = null
|
|
31
|
+
this.failed = false
|
|
32
|
+
this.cores = []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
|
|
36
|
+
|
|
37
|
+
static async fromHead(
|
|
38
|
+
auto,
|
|
39
|
+
head,
|
|
40
|
+
trusted,
|
|
41
|
+
{ force = false, timeout = 0, condition = null, reference = null } = {}
|
|
42
|
+
) {
|
|
43
|
+
const conservative = !force && auto._conservativeFF
|
|
44
|
+
const timeoutOpts = timeout ? { timeout } : null
|
|
45
|
+
|
|
46
|
+
const oplog = await FastForward.flushHead(auto, head, {
|
|
47
|
+
conservative,
|
|
48
|
+
...timeoutOpts
|
|
49
|
+
})
|
|
50
|
+
if (!oplog) return null
|
|
51
|
+
|
|
52
|
+
const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
|
|
53
|
+
if (!verified) return null
|
|
54
|
+
|
|
55
|
+
// legacy nodes from non-indexers have no system info to fast-forward from
|
|
56
|
+
if (!oplog.op.views || !verified.op.views) return null
|
|
57
|
+
|
|
58
|
+
if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return new FastForward(auto, batchToHead(verified.op.views.system))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async fromHeads(
|
|
70
|
+
auto,
|
|
71
|
+
heads,
|
|
72
|
+
{ force = false, timeout = 0, condition = null, reference = null } = {}
|
|
73
|
+
) {
|
|
74
|
+
if (reference === null) reference = auto._workingView.view
|
|
75
|
+
|
|
76
|
+
const promises = []
|
|
77
|
+
|
|
78
|
+
for (const head of heads) {
|
|
79
|
+
if (head.length === 0) continue
|
|
80
|
+
promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ops = await Promise.all(promises)
|
|
84
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
85
|
+
|
|
86
|
+
const trust = await Promise.all(
|
|
87
|
+
ops.map((res) => {
|
|
88
|
+
return res === null ? false : auto.trusted.isTrusted(res.key, reference)
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
92
|
+
|
|
93
|
+
const candidates = []
|
|
94
|
+
|
|
95
|
+
let bestTrusted = null
|
|
96
|
+
let bestTrustedFlushes = -1
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < ops.length; i++) {
|
|
99
|
+
const res = ops[i]
|
|
100
|
+
if (res === null || !res.op.views) continue
|
|
101
|
+
if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
|
|
102
|
+
|
|
103
|
+
if (!trust[i]) {
|
|
104
|
+
candidates.push(res)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (res.op.views.flushes > bestTrustedFlushes) {
|
|
109
|
+
bestTrustedFlushes = res.op.views.flushes
|
|
110
|
+
bestTrusted = res
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (bestTrusted !== null) {
|
|
115
|
+
return FastForward.fromHead(auto, bestTrusted, null, {
|
|
116
|
+
force,
|
|
117
|
+
timeout,
|
|
118
|
+
condition,
|
|
119
|
+
reference
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
|
|
124
|
+
|
|
125
|
+
for (const res of candidates) {
|
|
126
|
+
const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
|
|
127
|
+
if (trusted === null) continue
|
|
128
|
+
if (auto.fastForwarding || auto.fastForwardTo) return null
|
|
129
|
+
|
|
130
|
+
const ff = await FastForward.fromHead(auto, res, trusted, {
|
|
131
|
+
force,
|
|
132
|
+
timeout,
|
|
133
|
+
condition,
|
|
134
|
+
reference
|
|
135
|
+
})
|
|
136
|
+
if (ff !== null) return ff
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static async flushHead(auto, head, opts) {
|
|
143
|
+
const core = auto.openCore(head.key)
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const oplog = await auto.readOplog(core, head.length, opts)
|
|
147
|
+
if (oplog === null) return null
|
|
148
|
+
return oplog.op.views ? oplog : null
|
|
149
|
+
} finally {
|
|
150
|
+
await core.close()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static async accepts(auto, oplog, condition, reference) {
|
|
155
|
+
const opened = auto.openViewAt(oplog)
|
|
156
|
+
if (opened === null) return false
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
return !!(await condition(opened.view, reference))
|
|
160
|
+
} finally {
|
|
161
|
+
await opened.close()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static async mostRecentTrusted(auto, head, reference) {
|
|
166
|
+
const opened = auto.openViewAt(head)
|
|
167
|
+
if (opened === null) return null
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
|
|
171
|
+
} finally {
|
|
172
|
+
await opened.close()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async run() {
|
|
177
|
+
try {
|
|
178
|
+
if (!this.running) this.running = this._run()
|
|
179
|
+
|
|
180
|
+
return await this.running
|
|
181
|
+
} catch (err) {
|
|
182
|
+
safetyCatch(err)
|
|
183
|
+
this.failed = true
|
|
184
|
+
return null
|
|
185
|
+
} finally {
|
|
186
|
+
await this.close()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async _run() {
|
|
191
|
+
// an uninitialised head has an unknown length: wait for the core to catch up
|
|
192
|
+
if (!this.head.length) {
|
|
193
|
+
this.head.length = await this._resolveLength()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// zero-length boot is unsupported
|
|
197
|
+
if (!this.head.length) return null
|
|
198
|
+
|
|
199
|
+
await this.system.boot(this.head, { timeout: this.timeout })
|
|
200
|
+
|
|
201
|
+
// a legacy system needs migrating rather than a plain fast-forward
|
|
202
|
+
if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
|
|
203
|
+
const migrated = this.auto._migratedHead && this.auto._migratedHead.system
|
|
204
|
+
if (
|
|
205
|
+
migrated &&
|
|
206
|
+
b4a.equals(migrated.key, this.head.key) &&
|
|
207
|
+
migrated.length >= this.head.length
|
|
208
|
+
) {
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const result = await this._migrate()
|
|
213
|
+
if (result === null || result.autobee === null) return result
|
|
214
|
+
|
|
215
|
+
// an indexer of the newest legacy generation already moved to autobee -
|
|
216
|
+
// fast-forward straight onto its announced head instead of migrating,
|
|
217
|
+
// falling back to the legacy migration if the head cannot be booted
|
|
218
|
+
try {
|
|
219
|
+
this.head = result.autobee
|
|
220
|
+
await this.system.boot(this.head, { timeout: this.timeout })
|
|
221
|
+
if (this.system.version <= LEGACY_AUTOBASE_VERSION) {
|
|
222
|
+
throw new Error('Expected an autobee system')
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
safetyCatch(err)
|
|
226
|
+
this.head = result.head
|
|
227
|
+
return { head: result.head, migrate: result.migrate }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// fall through to the plain fast-forward below
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const promises = []
|
|
234
|
+
|
|
235
|
+
// ensure local key is locally available always
|
|
236
|
+
promises.push(this.system.get(this.auto.local.key, { timeout: this.timeout }))
|
|
237
|
+
|
|
238
|
+
const view = this.auto.store.get({ key: this.system.view.key, active: true })
|
|
239
|
+
this.cores.push(view)
|
|
240
|
+
|
|
241
|
+
promises.push(
|
|
242
|
+
view.get(this.system.view.length - 1, {
|
|
243
|
+
timeout: this.timeout,
|
|
244
|
+
activeRequests: this.activeRequests
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
for (const head of this.system.heads) {
|
|
249
|
+
promises.push(this.system.get(head.key, { timeout: this.timeout }))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await Promise.all(promises)
|
|
253
|
+
if (this.destroyed) return null
|
|
254
|
+
|
|
255
|
+
if (this.destroyed) return null
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
head: this.head,
|
|
259
|
+
migrate: null
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Upgrade a legacy (pre-AUTOBEE_VERSION) system in place. The system already
|
|
264
|
+
// booted above (coerced to the current struct), but the original v1/v2 struct
|
|
265
|
+
// is needed to recover the indexers/views/entropy, so decode the raw block.
|
|
266
|
+
//
|
|
267
|
+
// A head may be arbitrarily old: an indexer rotation freezes the system core
|
|
268
|
+
// and moves the indexers to a successor. The frozen core cannot reveal its
|
|
269
|
+
// successor (the promotion only lands in its unsigned tail), but the
|
|
270
|
+
// indexers' newest oplog digests stamp the newest system they know - chase
|
|
271
|
+
// those so the migration lands on the newest generation instead of the head
|
|
272
|
+
// we were given.
|
|
273
|
+
async _migrate() {
|
|
274
|
+
let head = { key: this.head.key, length: this.head.length }
|
|
275
|
+
const seen = new Set()
|
|
276
|
+
|
|
277
|
+
for (let hops = 0; hops < migrations.MAX_MIGRATE_HOPS; hops++) {
|
|
278
|
+
seen.add(b4a.toString(head.key, 'hex'))
|
|
279
|
+
|
|
280
|
+
const core = this.auto.store.get({ key: head.key, encryption: null })
|
|
281
|
+
this.cores.push(core)
|
|
282
|
+
await core.ready()
|
|
283
|
+
|
|
284
|
+
// setup encryption
|
|
285
|
+
await core.setEncryption(this.auto.getSystemEncryption())
|
|
286
|
+
|
|
287
|
+
// successors are resolved by key only - wait for the replicated length
|
|
288
|
+
if (!head.length) head.length = await migrations.coreLength(core, this.timeout)
|
|
289
|
+
if (!head.length || this.destroyed) return null
|
|
290
|
+
|
|
291
|
+
const info = await migrations.readLegacySystemInfo(core, head.length, {
|
|
292
|
+
timeout: this.timeout,
|
|
293
|
+
activeRequests: this.activeRequests
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
const { legacy, autobee } = await migrations.findNewestSystem(this, info)
|
|
297
|
+
if (this.destroyed) return null
|
|
298
|
+
|
|
299
|
+
if (legacy !== null && !seen.has(b4a.toString(legacy, 'hex'))) {
|
|
300
|
+
head = { key: legacy, length: 0 }
|
|
301
|
+
continue
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.head = head
|
|
305
|
+
|
|
306
|
+
const views = await migrations.resolveLegacyViews(this, info)
|
|
307
|
+
if (views === null || this.destroyed) return null
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
head,
|
|
311
|
+
migrate: views,
|
|
312
|
+
autobee: autobee === null ? null : batchToHead(autobee.op.views.system)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async _resolveLength() {
|
|
320
|
+
const core = this.auto.store.get({ key: this.head.key })
|
|
321
|
+
this.cores.push(core)
|
|
322
|
+
|
|
323
|
+
await core.ready()
|
|
324
|
+
|
|
325
|
+
return migrations.coreLength(core, this.timeout)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// cancels every read in flight and fails any that come after
|
|
329
|
+
clearRequests(err = null) {
|
|
330
|
+
Hypercore.destroyRequests(this.activeRequests, err)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async close() {
|
|
334
|
+
this.destroyed = true
|
|
335
|
+
this.clearRequests()
|
|
336
|
+
if (this.system) await this.system.close()
|
|
337
|
+
for (const core of this.cores) await core.close()
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function batchToHead(b) {
|
|
342
|
+
return {
|
|
343
|
+
key: b.key,
|
|
344
|
+
length: b.start + b.length
|
|
345
|
+
}
|
|
346
|
+
}
|