@agoric/swing-store 0.9.1 → 0.9.2-calypso-dev-84eb287.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/docs/data-export.md +116 -74
- package/docs/swingstore.md +52 -0
- package/package.json +25 -16
- package/src/assertComplete.js +21 -0
- package/src/bundleStore.js +141 -48
- package/src/exporter.js +221 -0
- package/src/importer.js +133 -0
- package/src/index.js +11 -0
- package/src/internal.js +22 -0
- package/src/kvStore.js +172 -0
- package/src/repairMetadata.js +67 -0
- package/src/snapStore.js +173 -38
- package/src/snapStoreIO.js +8 -0
- package/src/swingStore.js +94 -597
- package/src/transcriptStore.js +265 -61
- package/src/types.d.ts +15 -0
- package/src/types.js +6 -0
- package/src/util.js +7 -1
- package/test/{test-bundles.js → bundles.test.js} +21 -10
- package/test/{test-deletion.js → deletion.test.js} +1 -1
- package/test/export.test.js +350 -0
- package/test/{test-exportImport.js → exportImport.test.js} +87 -67
- package/test/exports.js +102 -0
- package/test/{test-hasher.js → hasher.test.js} +0 -2
- package/test/import.test.js +413 -0
- package/test/repair-metadata.test.js +143 -0
- package/test/{test-snapstore.js → snapstore.test.js} +2 -5
- package/test/{test-state.js → state.test.js} +36 -3
- package/test/util.js +27 -0
- package/{jsconfig.json → tsconfig.json} +1 -0
package/src/bundleStore.js
CHANGED
|
@@ -7,7 +7,6 @@ import { checkBundle } from '@endo/check-bundle/lite.js';
|
|
|
7
7
|
import { Nat } from '@endo/nat';
|
|
8
8
|
import { Fail, q } from '@agoric/assert';
|
|
9
9
|
import { createSHA256 } from './hasher.js';
|
|
10
|
-
import { buffer } from './util.js';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* @typedef { { moduleFormat: 'getExport', source: string, sourceMap?: string } } GetExportBundle
|
|
@@ -16,7 +15,8 @@ import { buffer } from './util.js';
|
|
|
16
15
|
* @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle } Bundle
|
|
17
16
|
*/
|
|
18
17
|
/**
|
|
19
|
-
* @typedef { import('./
|
|
18
|
+
* @typedef { import('./exporter.js').SwingStoreExporter } SwingStoreExporter
|
|
19
|
+
* @typedef { import('./internal.js').ArtifactMode } ArtifactMode
|
|
20
20
|
*
|
|
21
21
|
* @typedef {{
|
|
22
22
|
* addBundle: (bundleID: string, bundle: Bundle) => void;
|
|
@@ -27,7 +27,10 @@ import { buffer } from './util.js';
|
|
|
27
27
|
*
|
|
28
28
|
* @typedef {{
|
|
29
29
|
* exportBundle: (name: string) => AsyncIterableIterator<Uint8Array>,
|
|
30
|
-
*
|
|
30
|
+
* repairBundleRecord: (key: string, value: string) => void,
|
|
31
|
+
* importBundleRecord: (key: string, value: string) => void,
|
|
32
|
+
* importBundle: (name: string, dataProvider: () => Promise<Buffer>) => Promise<void>,
|
|
33
|
+
* assertComplete: (checkMode: Omit<ArtifactMode, 'debug'>) => void,
|
|
31
34
|
* getExportRecords: () => IterableIterator<readonly [key: string, value: string]>,
|
|
32
35
|
* getArtifactNames: () => AsyncIterableIterator<string>,
|
|
33
36
|
* getBundleIDs: () => IterableIterator<string>,
|
|
@@ -39,6 +42,18 @@ import { buffer } from './util.js';
|
|
|
39
42
|
*
|
|
40
43
|
*/
|
|
41
44
|
|
|
45
|
+
function bundleIDFromName(name) {
|
|
46
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
47
|
+
const [tag, ...pieces] = name.split('.');
|
|
48
|
+
if (tag !== 'bundle' || pieces.length !== 1) {
|
|
49
|
+
Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(
|
|
50
|
+
name,
|
|
51
|
+
)}`;
|
|
52
|
+
}
|
|
53
|
+
const bundleID = pieces[0];
|
|
54
|
+
return bundleID;
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
/**
|
|
43
58
|
* @param {*} db
|
|
44
59
|
* @param {() => void} ensureTxn
|
|
@@ -54,6 +69,9 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
54
69
|
)
|
|
55
70
|
`);
|
|
56
71
|
|
|
72
|
+
// A populated record contains both bundleID and bundle, while a
|
|
73
|
+
// pruned record has a bundle of NULL.
|
|
74
|
+
|
|
57
75
|
function bundleArtifactName(bundleID) {
|
|
58
76
|
return `bundle.${bundleID}`;
|
|
59
77
|
}
|
|
@@ -62,20 +80,36 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
62
80
|
return `b${Nat(version)}-${hash}`;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
VALUES (?,
|
|
83
|
+
// the PRIMARY KEY constraint requires the bundleID not already
|
|
84
|
+
// exist
|
|
85
|
+
const sqlAddBundleRecord = db.prepare(`
|
|
86
|
+
INSERT INTO bundles (bundleID, bundle) VALUES (?, NULL)
|
|
69
87
|
`);
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
// this sees both populated and pruned (not-yet-populated) records
|
|
90
|
+
const sqlHasBundleRecord = db.prepare(`
|
|
91
|
+
SELECT count(*)
|
|
92
|
+
FROM bundles
|
|
93
|
+
WHERE bundleID = ?
|
|
94
|
+
`);
|
|
95
|
+
sqlHasBundleRecord.pluck();
|
|
96
|
+
|
|
97
|
+
const sqlPopulateBundleRecord = db.prepare(`
|
|
98
|
+
UPDATE bundles SET bundle = $serialized WHERE bundleID = $bundleID
|
|
99
|
+
`);
|
|
100
|
+
|
|
101
|
+
function addBundleRecord(bundleID) {
|
|
102
|
+
ensureTxn();
|
|
103
|
+
sqlAddBundleRecord.run(bundleID);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function populateBundle(bundleID, serialized) {
|
|
78
107
|
ensureTxn();
|
|
108
|
+
sqlHasBundleRecord.get(bundleID) || Fail`missing ${bundleID}`;
|
|
109
|
+
sqlPopulateBundleRecord.run({ bundleID, serialized });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function serializeBundle(bundleID, bundle) {
|
|
79
113
|
const { moduleFormat } = bundle;
|
|
80
114
|
let serialized;
|
|
81
115
|
if (bundleID.startsWith('b0-')) {
|
|
@@ -98,19 +132,55 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
98
132
|
} else {
|
|
99
133
|
throw Fail`unsupported BundleID ${bundleID}`;
|
|
100
134
|
}
|
|
101
|
-
|
|
135
|
+
return serialized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Store a complete bundle in a single operation, used by runtime
|
|
140
|
+
* (i.e. not an import). We rely upon the caller to provide a
|
|
141
|
+
* correct bundle (e.g. no unexpected properties), but we still
|
|
142
|
+
* check the ID against the contents.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} bundleID
|
|
145
|
+
* @param {Bundle} bundle
|
|
146
|
+
*/
|
|
147
|
+
function addBundle(bundleID, bundle) {
|
|
148
|
+
const serialized = serializeBundle(bundleID, bundle);
|
|
149
|
+
addBundleRecord(bundleID);
|
|
150
|
+
populateBundle(bundleID, serialized);
|
|
102
151
|
noteExport(bundleArtifactName(bundleID), bundleID);
|
|
103
152
|
}
|
|
104
153
|
|
|
105
|
-
const
|
|
154
|
+
const sqlGetPrunedBundles = db.prepare(`
|
|
155
|
+
SELECT bundleID
|
|
156
|
+
FROM bundles
|
|
157
|
+
WHERE bundle IS NULL
|
|
158
|
+
ORDER BY bundleID
|
|
159
|
+
`);
|
|
160
|
+
sqlGetPrunedBundles.pluck();
|
|
161
|
+
|
|
162
|
+
function getPrunedBundles() {
|
|
163
|
+
return sqlGetPrunedBundles.all();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function assertComplete(checkMode) {
|
|
167
|
+
assert(checkMode !== 'debug', checkMode);
|
|
168
|
+
const pruned = getPrunedBundles();
|
|
169
|
+
if (pruned.length) {
|
|
170
|
+
throw Fail`missing bundles for: ${pruned.join(',')}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const sqlHasPopulatedBundle = db.prepare(`
|
|
106
175
|
SELECT count(*)
|
|
107
176
|
FROM bundles
|
|
108
177
|
WHERE bundleID = ?
|
|
178
|
+
AND bundle IS NOT NULL
|
|
109
179
|
`);
|
|
110
|
-
|
|
180
|
+
sqlHasPopulatedBundle.pluck(true);
|
|
111
181
|
|
|
112
182
|
function hasBundle(bundleID) {
|
|
113
|
-
const count =
|
|
183
|
+
const count = sqlHasPopulatedBundle.get(bundleID);
|
|
114
184
|
return count !== 0;
|
|
115
185
|
}
|
|
116
186
|
|
|
@@ -119,15 +189,15 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
119
189
|
FROM bundles
|
|
120
190
|
WHERE bundleID = ?
|
|
121
191
|
`);
|
|
122
|
-
sqlGetBundle.pluck(true);
|
|
123
192
|
|
|
124
193
|
/**
|
|
125
194
|
* @param {string} bundleID
|
|
126
195
|
* @returns {Bundle}
|
|
127
196
|
*/
|
|
128
197
|
function getBundle(bundleID) {
|
|
129
|
-
const
|
|
130
|
-
|
|
198
|
+
const row =
|
|
199
|
+
sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
|
|
200
|
+
const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
|
|
131
201
|
if (bundleID.startsWith('b0-')) {
|
|
132
202
|
return harden(JSON.parse(rawBundle));
|
|
133
203
|
} else if (bundleID.startsWith('b1-')) {
|
|
@@ -153,6 +223,31 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
153
223
|
}
|
|
154
224
|
}
|
|
155
225
|
|
|
226
|
+
// take an export-data record (id/hash but not bundle contents) and
|
|
227
|
+
// insert something in the DB
|
|
228
|
+
function importBundleRecord(key, value) {
|
|
229
|
+
const bundleID = bundleIDFromName(key);
|
|
230
|
+
assert.equal(bundleID, value);
|
|
231
|
+
addBundleRecord(bundleID);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function repairBundleRecord(key, value) {
|
|
235
|
+
// Bundle records have no metadata, and all bundles must be
|
|
236
|
+
// present (there's no notion of "historical bundle"). So there's
|
|
237
|
+
// no "repair", and if the repair process supplies a bundle record
|
|
238
|
+
// that isn't already present, we throw an error. The repair
|
|
239
|
+
// process doesn't get artifacts, so adding a new record here
|
|
240
|
+
// would fail the subsequent completeness check anyways.
|
|
241
|
+
|
|
242
|
+
const bundleID = bundleIDFromName(key);
|
|
243
|
+
assert.equal(bundleID, value);
|
|
244
|
+
if (sqlHasBundleRecord.get(bundleID)) {
|
|
245
|
+
// record is present, there's no metadata to mismatch, so ignore quietly
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
throw Fail`unexpected new bundle record for ${bundleID} during repair`;
|
|
249
|
+
}
|
|
250
|
+
|
|
156
251
|
/**
|
|
157
252
|
* Read a bundle and return it as a stream of data suitable for export to
|
|
158
253
|
* another store.
|
|
@@ -166,16 +261,13 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
166
261
|
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
167
262
|
*/
|
|
168
263
|
async function* exportBundle(name) {
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
(parts.length === 2 && type === 'bundle') ||
|
|
174
|
-
Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(name)}`;
|
|
175
|
-
const rawBundle = sqlGetBundle.get(bundleID);
|
|
176
|
-
rawBundle || Fail`bundle ${q(name)} not available`;
|
|
264
|
+
const bundleID = bundleIDFromName(name);
|
|
265
|
+
const row =
|
|
266
|
+
sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
|
|
267
|
+
const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
|
|
177
268
|
yield* Readable.from(Buffer.from(rawBundle));
|
|
178
269
|
}
|
|
270
|
+
harden(exportBundle);
|
|
179
271
|
|
|
180
272
|
const sqlGetBundleIDs = db.prepare(`
|
|
181
273
|
SELECT bundleID
|
|
@@ -195,12 +287,14 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
195
287
|
yield [bundleArtifactName(bundleID), bundleID];
|
|
196
288
|
}
|
|
197
289
|
}
|
|
290
|
+
harden(getExportRecords);
|
|
198
291
|
|
|
199
292
|
async function* getArtifactNames() {
|
|
200
293
|
for (const bundleID of sqlGetBundleIDs.iterate()) {
|
|
201
294
|
yield bundleArtifactName(bundleID);
|
|
202
295
|
}
|
|
203
296
|
}
|
|
297
|
+
harden(getArtifactNames);
|
|
204
298
|
|
|
205
299
|
function computeSha512(bytes) {
|
|
206
300
|
const hash = createHash('sha512');
|
|
@@ -209,23 +303,17 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
209
303
|
}
|
|
210
304
|
|
|
211
305
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
306
|
+
* Call addBundleRecord() first, then this importBundle() will
|
|
307
|
+
* populate the record.
|
|
308
|
+
*
|
|
309
|
+
* @param {string} name Artifact name, `bundle.${bundleID}`
|
|
310
|
+
* @param {() => Promise<Buffer>} dataProvider Function to get bundle bytes
|
|
215
311
|
* @returns {Promise<void>}
|
|
216
312
|
*/
|
|
217
|
-
async function importBundle(name,
|
|
313
|
+
async function importBundle(name, dataProvider) {
|
|
218
314
|
await 0; // no synchronous prefix
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
// prettier-ignore
|
|
222
|
-
parts.length === 2 && type === 'bundle' ||
|
|
223
|
-
Fail`expected artifact name of the form 'bundle.{bundleID}', saw '${q(name)}'`;
|
|
224
|
-
bundleIDkey === bundleID ||
|
|
225
|
-
Fail`bundle artifact name ${name} doesn't match bundleID ${bundleID}`;
|
|
226
|
-
const artifactChunks = exporter.getArtifact(name);
|
|
227
|
-
const inStream = Readable.from(artifactChunks);
|
|
228
|
-
const data = await buffer(inStream);
|
|
315
|
+
const bundleID = bundleIDFromName(name);
|
|
316
|
+
const data = await dataProvider();
|
|
229
317
|
if (bundleID.startsWith('b0-')) {
|
|
230
318
|
// we dissect and reassemble the bundle, to exclude unexpected properties
|
|
231
319
|
const { moduleFormat, source, sourceMap } = JSON.parse(data.toString());
|
|
@@ -234,7 +322,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
234
322
|
const serialized = JSON.stringify(bundle);
|
|
235
323
|
bundleID === bundleIdFromHash(0, createSHA256(serialized).finish()) ||
|
|
236
324
|
Fail`bundleID ${q(bundleID)} does not match bundle artifact`;
|
|
237
|
-
|
|
325
|
+
populateBundle(bundleID, serialized);
|
|
238
326
|
} else if (bundleID.startsWith('b1-')) {
|
|
239
327
|
/** @type {EndoZipBase64Bundle} */
|
|
240
328
|
const bundle = harden({
|
|
@@ -243,9 +331,8 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
243
331
|
endoZipBase64: encodeBase64(data),
|
|
244
332
|
});
|
|
245
333
|
// Assert that the bundle contents match the ID and hash
|
|
246
|
-
// eslint-disable-next-line @jessie.js/no-nested-await
|
|
247
334
|
await checkBundle(bundle, computeSha512, bundleID);
|
|
248
|
-
|
|
335
|
+
populateBundle(bundleID, serializeBundle(bundleID, bundle));
|
|
249
336
|
} else {
|
|
250
337
|
Fail`unsupported BundleID ${q(bundleID)}`;
|
|
251
338
|
}
|
|
@@ -265,7 +352,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
265
352
|
const dump = {};
|
|
266
353
|
for (const row of sql.iterate()) {
|
|
267
354
|
const { bundleID, bundle } = row;
|
|
268
|
-
dump[bundleID] = encodeBase64(bundle);
|
|
355
|
+
dump[bundleID] = encodeBase64(Buffer.from(bundle, 'utf-8'));
|
|
269
356
|
}
|
|
270
357
|
return dump;
|
|
271
358
|
}
|
|
@@ -280,17 +367,23 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
280
367
|
function* getBundleIDs() {
|
|
281
368
|
yield* sqlListBundleIDs.iterate();
|
|
282
369
|
}
|
|
370
|
+
harden(getBundleIDs);
|
|
283
371
|
|
|
284
372
|
return harden({
|
|
373
|
+
importBundleRecord,
|
|
374
|
+
importBundle,
|
|
375
|
+
assertComplete,
|
|
376
|
+
|
|
285
377
|
addBundle,
|
|
286
378
|
hasBundle,
|
|
287
379
|
getBundle,
|
|
288
380
|
deleteBundle,
|
|
381
|
+
|
|
289
382
|
getExportRecords,
|
|
290
383
|
getArtifactNames,
|
|
291
384
|
exportBundle,
|
|
292
|
-
importBundle,
|
|
293
385
|
getBundleIDs,
|
|
386
|
+
repairBundleRecord,
|
|
294
387
|
|
|
295
388
|
dumpBundles,
|
|
296
389
|
});
|
package/src/exporter.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import sqlite3 from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
import { Fail, q } from '@agoric/assert';
|
|
4
|
+
|
|
5
|
+
import { dbFileInDirectory } from './util.js';
|
|
6
|
+
import { getKeyType } from './kvStore.js';
|
|
7
|
+
import { makeBundleStore } from './bundleStore.js';
|
|
8
|
+
import { makeSnapStore } from './snapStore.js';
|
|
9
|
+
import { makeSnapStoreIO } from './snapStoreIO.js';
|
|
10
|
+
import { makeTranscriptStore } from './transcriptStore.js';
|
|
11
|
+
import { assertComplete } from './assertComplete.js';
|
|
12
|
+
import { validateArtifactMode } from './internal.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @template T
|
|
16
|
+
* @typedef { Iterable<T> | AsyncIterable<T> } AnyIterable
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @template T
|
|
20
|
+
* @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @typedef {readonly [
|
|
26
|
+
* key: string,
|
|
27
|
+
* value?: string | null | undefined,
|
|
28
|
+
* ]} KVPair
|
|
29
|
+
*
|
|
30
|
+
* @typedef {object} SwingStoreExporter
|
|
31
|
+
*
|
|
32
|
+
* Allows export of data from a swingStore as a fixed view onto the content as
|
|
33
|
+
* of the most recent commit point at the time the exporter was created. The
|
|
34
|
+
* exporter may be used while another SwingStore instance is active for the same
|
|
35
|
+
* DB, possibly in another thread or process. It guarantees that regardless of
|
|
36
|
+
* the concurrent activity of other swingStore instances, the data representing
|
|
37
|
+
* the commit point will stay consistent and available.
|
|
38
|
+
*
|
|
39
|
+
* @property {(key: string) => string | undefined} getHostKV
|
|
40
|
+
*
|
|
41
|
+
* Retrieve a value from the "host" portion of the kvStore, just like
|
|
42
|
+
* hostStorage.hostKVStore.get() would do.
|
|
43
|
+
*
|
|
44
|
+
* @property {() => AnyIterableIterator<KVPair>} getExportData
|
|
45
|
+
*
|
|
46
|
+
* Get a full copy of the first-stage export data (key-value pairs) from the
|
|
47
|
+
* swingStore. This represents both the contents of the KVStore (excluding host
|
|
48
|
+
* and local prefixes), as well as any data needed to validate all artifacts,
|
|
49
|
+
* both current and historical. As such it represents the root of trust for the
|
|
50
|
+
* application.
|
|
51
|
+
*
|
|
52
|
+
* Content of validation data (with supporting entries for indexing):
|
|
53
|
+
* - kv.${key} = ${value} // ordinary kvStore data entry
|
|
54
|
+
* - snapshot.${vatID}.${snapPos} = ${{ vatID, snapPos, hash }};
|
|
55
|
+
* - snapshot.${vatID}.current = `snapshot.${vatID}.${snapPos}`
|
|
56
|
+
* - transcript.${vatID}.${startPos} = ${{ vatID, startPos, endPos, hash }}
|
|
57
|
+
* - transcript.${vatID}.current = ${{ vatID, startPos, endPos, hash }}
|
|
58
|
+
*
|
|
59
|
+
* @property {() => AnyIterableIterator<string>} getArtifactNames
|
|
60
|
+
*
|
|
61
|
+
* Get a list of name of artifacts available from the swingStore. A name
|
|
62
|
+
* returned by this method guarantees that a call to `getArtifact` on the same
|
|
63
|
+
* exporter instance will succeed. The `artifactMode` option to
|
|
64
|
+
* `makeSwingStoreExporter` controls the filtering of the artifact names
|
|
65
|
+
* yielded.
|
|
66
|
+
*
|
|
67
|
+
* Artifact names:
|
|
68
|
+
* - transcript.${vatID}.${startPos}.${endPos}
|
|
69
|
+
* - snapshot.${vatID}.${snapPos}
|
|
70
|
+
* - bundle.${bundleID}
|
|
71
|
+
*
|
|
72
|
+
* @property {(name: string) => AnyIterableIterator<Uint8Array>} getArtifact
|
|
73
|
+
*
|
|
74
|
+
* Retrieve an artifact by name as a sequence of binary chunks. May throw if
|
|
75
|
+
* the artifact is not available, which can occur if the artifact is historical
|
|
76
|
+
* and wasn't preserved.
|
|
77
|
+
*
|
|
78
|
+
* @property {() => Promise<void>} close
|
|
79
|
+
*
|
|
80
|
+
* Dispose of all resources held by this exporter. Any further operation on this
|
|
81
|
+
* exporter or its outstanding iterators will fail.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @typedef { object } ExportSwingStoreOptions
|
|
86
|
+
* @property { import('./internal.js').ArtifactMode } [artifactMode] What artifacts should/must the exporter provide?
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} dirPath
|
|
91
|
+
* @param { ExportSwingStoreOptions } [options]
|
|
92
|
+
* @returns {SwingStoreExporter}
|
|
93
|
+
*/
|
|
94
|
+
export function makeSwingStoreExporter(dirPath, options = {}) {
|
|
95
|
+
typeof dirPath === 'string' || Fail`dirPath must be a string`;
|
|
96
|
+
const { artifactMode = 'operational' } = options;
|
|
97
|
+
validateArtifactMode(artifactMode);
|
|
98
|
+
|
|
99
|
+
const filePath = dbFileInDirectory(dirPath);
|
|
100
|
+
const db = sqlite3(filePath);
|
|
101
|
+
|
|
102
|
+
// Execute the data export in a (read) transaction, to ensure that we are
|
|
103
|
+
// capturing the state of the database at a single point in time. Our close()
|
|
104
|
+
// will ROLLBACK the txn just in case some bug tried to change the DB.
|
|
105
|
+
const sqlBeginTransaction = db.prepare('BEGIN TRANSACTION');
|
|
106
|
+
sqlBeginTransaction.run();
|
|
107
|
+
|
|
108
|
+
// ensureTxn can be a dummy, we just started one
|
|
109
|
+
const ensureTxn = () => {};
|
|
110
|
+
const snapStore = makeSnapStore(db, ensureTxn, makeSnapStoreIO());
|
|
111
|
+
const bundleStore = makeBundleStore(db, ensureTxn);
|
|
112
|
+
const transcriptStore = makeTranscriptStore(db, ensureTxn, () => {});
|
|
113
|
+
|
|
114
|
+
const sqlKVGet = db.prepare(`
|
|
115
|
+
SELECT value
|
|
116
|
+
FROM kvStore
|
|
117
|
+
WHERE key = ?
|
|
118
|
+
`);
|
|
119
|
+
sqlKVGet.pluck(true);
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Obtain the value stored for a given host key. This is for the
|
|
123
|
+
* benefit of clients who need to briefly query the DB to ensure
|
|
124
|
+
* they are exporting the right thing, and need to avoid modifying
|
|
125
|
+
* anything (or creating a read-write DB lock) in the process.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} key The key whose value is sought.
|
|
128
|
+
*
|
|
129
|
+
* @returns {string | undefined} the (string) value for the given key, or
|
|
130
|
+
* undefined if there is no such value.
|
|
131
|
+
*
|
|
132
|
+
* @throws if key is not a string, or the key is not in the host
|
|
133
|
+
* section
|
|
134
|
+
*/
|
|
135
|
+
function getHostKV(key) {
|
|
136
|
+
typeof key === 'string' || Fail`key must be a string`;
|
|
137
|
+
getKeyType(key) === 'host' || Fail`getHostKV requires host keys`;
|
|
138
|
+
// @ts-expect-error unknown
|
|
139
|
+
return sqlKVGet.get(key);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @type {any} */
|
|
143
|
+
const sqlGetAllKVData = db.prepare(`
|
|
144
|
+
SELECT key, value
|
|
145
|
+
FROM kvStore
|
|
146
|
+
ORDER BY key
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @returns {AsyncIterableIterator<KVPair>}
|
|
151
|
+
* @yields {KVPair}
|
|
152
|
+
*/
|
|
153
|
+
async function* getExportData() {
|
|
154
|
+
for (const { key, value } of sqlGetAllKVData.iterate()) {
|
|
155
|
+
if (getKeyType(key) === 'consensus') {
|
|
156
|
+
yield [`kv.${key}`, value];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
yield* snapStore.getExportRecords(true);
|
|
160
|
+
yield* transcriptStore.getExportRecords(true);
|
|
161
|
+
yield* bundleStore.getExportRecords();
|
|
162
|
+
}
|
|
163
|
+
harden(getExportData);
|
|
164
|
+
|
|
165
|
+
/** @yields {string} */
|
|
166
|
+
async function* artifactNames() {
|
|
167
|
+
yield* snapStore.getArtifactNames(artifactMode);
|
|
168
|
+
yield* transcriptStore.getArtifactNames(artifactMode);
|
|
169
|
+
yield* bundleStore.getArtifactNames();
|
|
170
|
+
}
|
|
171
|
+
harden(artifactNames);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @returns {AsyncIterableIterator<string>}
|
|
175
|
+
*/
|
|
176
|
+
function getArtifactNames() {
|
|
177
|
+
if (artifactMode !== 'debug') {
|
|
178
|
+
// throw if this DB will not be able to yield all the desired artifacts
|
|
179
|
+
const internal = { snapStore, bundleStore, transcriptStore };
|
|
180
|
+
assertComplete(internal, artifactMode);
|
|
181
|
+
}
|
|
182
|
+
return artifactNames();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {string} name
|
|
187
|
+
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
188
|
+
*/
|
|
189
|
+
function getArtifact(name) {
|
|
190
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
191
|
+
const [type] = name.split('.', 1);
|
|
192
|
+
|
|
193
|
+
if (type === 'snapshot') {
|
|
194
|
+
return snapStore.exportSnapshot(name);
|
|
195
|
+
} else if (type === 'transcript') {
|
|
196
|
+
return transcriptStore.exportSpan(name);
|
|
197
|
+
} else if (type === 'bundle') {
|
|
198
|
+
return bundleStore.exportBundle(name);
|
|
199
|
+
} else {
|
|
200
|
+
throw Fail`invalid type in artifact name ${q(name)}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const sqlAbort = db.prepare('ROLLBACK');
|
|
205
|
+
|
|
206
|
+
async function close() {
|
|
207
|
+
// After all the data has been extracted, always abort the export
|
|
208
|
+
// transaction to ensure that the export was read-only (i.e., that no bugs
|
|
209
|
+
// inadvertantly modified the database).
|
|
210
|
+
sqlAbort.run();
|
|
211
|
+
db.close();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return harden({
|
|
215
|
+
getHostKV,
|
|
216
|
+
getExportData,
|
|
217
|
+
getArtifactNames,
|
|
218
|
+
getArtifact,
|
|
219
|
+
close,
|
|
220
|
+
});
|
|
221
|
+
}
|
package/src/importer.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Fail, q } from '@agoric/assert';
|
|
2
|
+
|
|
3
|
+
import { makeSwingStore } from './swingStore.js';
|
|
4
|
+
import { buffer } from './util.js';
|
|
5
|
+
import { validateArtifactMode } from './internal.js';
|
|
6
|
+
import { assertComplete } from './assertComplete.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef { object } ImportSwingStoreOptions
|
|
10
|
+
* @property { import('./internal.js').ArtifactMode } [artifactMode] What artifacts should the importer use and require?
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Function used to populate a swingStore from an object implementing the
|
|
15
|
+
* exporter API. The exporter API may be provided by a swingStore instance, or
|
|
16
|
+
* implemented by a host to restore data that was previously exported. The
|
|
17
|
+
* returned swingStore is not suitable for execution, and thus only contains
|
|
18
|
+
* the host facet for committing the populated swingStore.
|
|
19
|
+
*
|
|
20
|
+
* @param {import('./exporter.js').SwingStoreExporter} exporter
|
|
21
|
+
* @param {string | null} [dirPath]
|
|
22
|
+
* @param {ImportSwingStoreOptions} [options]
|
|
23
|
+
* @returns {Promise<Pick<import('./swingStore.js').SwingStore, 'hostStorage' | 'debug'>>}
|
|
24
|
+
*/
|
|
25
|
+
export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
26
|
+
if (dirPath && typeof dirPath !== 'string') {
|
|
27
|
+
Fail`dirPath must be a string`;
|
|
28
|
+
}
|
|
29
|
+
const { artifactMode = 'operational', ...makeSwingStoreOptions } = options;
|
|
30
|
+
validateArtifactMode(artifactMode);
|
|
31
|
+
|
|
32
|
+
const { hostStorage, kernelStorage, internal, debug } = makeSwingStore(
|
|
33
|
+
dirPath,
|
|
34
|
+
true,
|
|
35
|
+
{
|
|
36
|
+
unsafeFastMode: true,
|
|
37
|
+
...makeSwingStoreOptions,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// For every exportData entry, we add a DB record. 'kv' entries are
|
|
42
|
+
// the "kvStore shadow table", and are not associated with any
|
|
43
|
+
// artifacts. All other entries are associated with an artifact,
|
|
44
|
+
// however the import may or may not contain that artifact (the
|
|
45
|
+
// dataset can be incomplete: either the original DB was pruned at
|
|
46
|
+
// some point, or the exporter did not choose to include
|
|
47
|
+
// everything). The DB records we add are marked as incomplete (as
|
|
48
|
+
// if they had been pruned locally), and can be populated later when
|
|
49
|
+
// the artifact is retrieved.
|
|
50
|
+
|
|
51
|
+
// While unlikely, the getExportData() protocol *is* allowed to
|
|
52
|
+
// deliver multiple values for the same key (last one wins), or use
|
|
53
|
+
// 'null' to delete a previously-defined key. So our first pass both
|
|
54
|
+
// installs the kvStore shadow records, and de-dups/deletes the
|
|
55
|
+
// metadata records into this Map.
|
|
56
|
+
|
|
57
|
+
const allMetadata = new Map();
|
|
58
|
+
|
|
59
|
+
for await (const [key, value] of exporter.getExportData()) {
|
|
60
|
+
const [tag] = key.split('.', 1);
|
|
61
|
+
if (tag === 'kv') {
|
|
62
|
+
// 'kv' keys contain individual kvStore entries
|
|
63
|
+
const subKey = key.substring(tag.length + 1);
|
|
64
|
+
if (value == null) {
|
|
65
|
+
// Note '==' rather than '===': any nullish value implies deletion
|
|
66
|
+
kernelStorage.kvStore.delete(subKey);
|
|
67
|
+
} else {
|
|
68
|
+
kernelStorage.kvStore.set(subKey, value);
|
|
69
|
+
}
|
|
70
|
+
} else if (value == null) {
|
|
71
|
+
allMetadata.delete(key);
|
|
72
|
+
} else {
|
|
73
|
+
allMetadata.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Now take each metadata record and install the stub/pruned entry
|
|
78
|
+
// into the DB.
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of allMetadata.entries()) {
|
|
81
|
+
const [tag] = key.split('.', 1);
|
|
82
|
+
if (tag === 'bundle') {
|
|
83
|
+
internal.bundleStore.importBundleRecord(key, value);
|
|
84
|
+
} else if (tag === 'snapshot') {
|
|
85
|
+
internal.snapStore.importSnapshotRecord(key, value);
|
|
86
|
+
} else if (tag === 'transcript') {
|
|
87
|
+
internal.transcriptStore.importTranscriptSpanRecord(key, value);
|
|
88
|
+
} else {
|
|
89
|
+
Fail`unknown export-data type ${q(tag)} on import`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// All the metadata is now installed, and we're prepared for
|
|
94
|
+
// artifacts. We walk `getArtifactNames()` and offer each one to the
|
|
95
|
+
// submodule, which may ignore it according to `artifactMode`, but
|
|
96
|
+
// otherwise validates and accepts it. This is an initial import, so
|
|
97
|
+
// we don't need to check if we already have the data, but the
|
|
98
|
+
// submodule function is free to do such checks.
|
|
99
|
+
|
|
100
|
+
for await (const name of exporter.getArtifactNames()) {
|
|
101
|
+
const makeChunkIterator = () => exporter.getArtifact(name);
|
|
102
|
+
const dataProvider = async () => buffer(makeChunkIterator());
|
|
103
|
+
const [tag] = name.split('.', 1);
|
|
104
|
+
// TODO: pass the same args to all artifact importers, and let
|
|
105
|
+
// stores register their functions by
|
|
106
|
+
// 'type'. https://github.com/Agoric/agoric-sdk/pull/8075#discussion_r1285265453
|
|
107
|
+
if (tag === 'bundle') {
|
|
108
|
+
await internal.bundleStore.importBundle(name, dataProvider);
|
|
109
|
+
} else if (tag === 'snapshot') {
|
|
110
|
+
await internal.snapStore.populateSnapshot(name, makeChunkIterator, {
|
|
111
|
+
artifactMode,
|
|
112
|
+
});
|
|
113
|
+
} else if (tag === 'transcript') {
|
|
114
|
+
await internal.transcriptStore.populateTranscriptSpan(
|
|
115
|
+
name,
|
|
116
|
+
makeChunkIterator,
|
|
117
|
+
{ artifactMode },
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
Fail`unknown artifact type ${q(tag)} on import`;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// We've installed all the artifacts that we could, now do a
|
|
125
|
+
// completeness check. Enforce at least 'operational' completeness,
|
|
126
|
+
// even if the given mode was 'debug'.
|
|
127
|
+
|
|
128
|
+
const checkMode = artifactMode === 'debug' ? 'operational' : artifactMode;
|
|
129
|
+
assertComplete(internal, checkMode);
|
|
130
|
+
|
|
131
|
+
await exporter.close();
|
|
132
|
+
return { hostStorage, debug };
|
|
133
|
+
}
|