@agoric/swing-store 0.9.2-dev-2f092c3.0 → 0.9.2-dev-aa10ecd.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.
@@ -0,0 +1,121 @@
1
+ import { Fail, q } from '@agoric/assert';
2
+
3
+ import { makeSwingStore } from './swingStore.js';
4
+ import { buffer } from './util.js';
5
+ import { assertComplete } from './assertComplete.js';
6
+
7
+ /**
8
+ * @typedef { object } ImportSwingStoreOptions
9
+ * @property { boolean } [includeHistorical] Should the importer pay attention to historical artifacts?
10
+ */
11
+
12
+ /**
13
+ * Function used to create a new swingStore from an object implementing the
14
+ * exporter API. The exporter API may be provided by a swingStore instance, or
15
+ * implemented by a host to restore data that was previously exported.
16
+ *
17
+ * @param {import('./exporter').SwingStoreExporter} exporter
18
+ * @param {string | null} [dirPath]
19
+ * @param {ImportSwingStoreOptions} options
20
+ * @returns {Promise<import('./swingStore').SwingStore>}
21
+ */
22
+ export async function importSwingStore(exporter, dirPath = null, options = {}) {
23
+ if (dirPath && typeof dirPath !== 'string') {
24
+ Fail`dirPath must be a string`;
25
+ }
26
+ const { includeHistorical = false } = options;
27
+ const store = makeSwingStore(dirPath, true, options);
28
+ const { kernelStorage, internal } = store;
29
+
30
+ // For every exportData entry, we add a DB record. 'kv' entries are
31
+ // the "kvStore shadow table", and are not associated with any
32
+ // artifacts. All other entries are associated with an artifact,
33
+ // however the import may or may not contain that artifact (the
34
+ // dataset can be incomplete: either the original DB was pruned at
35
+ // some point, or the exporter did not choose to include
36
+ // everything). The DB records we add are marked as incomplete (as
37
+ // if they had been pruned locally), and can be populated later when
38
+ // the artifact is retrieved.
39
+
40
+ // While unlikely, the getExportData() protocol *is* allowed to
41
+ // deliver multiple values for the same key (last one wins), or use
42
+ // 'null' to delete a previously-defined key. So our first pass both
43
+ // installs the kvStore shadow records, and de-dups/deletes the
44
+ // metadata records into this Map.
45
+
46
+ const allMetadata = new Map();
47
+
48
+ for await (const [key, value] of exporter.getExportData()) {
49
+ const [tag] = key.split('.', 1);
50
+ if (tag === 'kv') {
51
+ // 'kv' keys contain individual kvStore entries
52
+ const subKey = key.substring(tag.length + 1);
53
+ if (value == null) {
54
+ // Note '==' rather than '===': any nullish value implies deletion
55
+ kernelStorage.kvStore.delete(subKey);
56
+ } else {
57
+ kernelStorage.kvStore.set(subKey, value);
58
+ }
59
+ } else if (value == null) {
60
+ allMetadata.delete(key);
61
+ } else {
62
+ allMetadata.set(key, value);
63
+ }
64
+ }
65
+
66
+ // Now take each metadata record and install the stub/pruned entry
67
+ // into the DB.
68
+
69
+ for (const [key, value] of allMetadata.entries()) {
70
+ const [tag] = key.split('.', 1);
71
+ if (tag === 'bundle') {
72
+ internal.bundleStore.importBundleRecord(key, value);
73
+ } else if (tag === 'snapshot') {
74
+ internal.snapStore.importSnapshotRecord(key, value);
75
+ } else if (tag === 'transcript') {
76
+ internal.transcriptStore.importTranscriptSpanRecord(key, value);
77
+ } else {
78
+ Fail`unknown export-data type ${q(tag)} on import`;
79
+ }
80
+ }
81
+
82
+ // All the metadata is now installed, and we're prepared for
83
+ // artifacts. We walk `getArtifactNames()` and offer each one to the
84
+ // submodule, which ignores historical ones (unless
85
+ // 'includeHistorical' is true), and validates+accepts the
86
+ // rest. This is an initial import, so we don't need to check if we
87
+ // already have the data, but the submodule function is free to do
88
+ // that check if they want.
89
+
90
+ for await (const name of exporter.getArtifactNames()) {
91
+ const makeChunkIterator = () => exporter.getArtifact(name);
92
+ const dataProvider = async () => buffer(makeChunkIterator());
93
+ const [tag] = name.split('.', 1);
94
+ // TODO: pass the same args to all artifact importers, and let
95
+ // stores register their functions by
96
+ // 'type'. https://github.com/Agoric/agoric-sdk/pull/8075#discussion_r1285265453
97
+ if (tag === 'bundle') {
98
+ await internal.bundleStore.importBundle(name, dataProvider);
99
+ } else if (tag === 'snapshot') {
100
+ await internal.snapStore.populateSnapshot(name, makeChunkIterator, {
101
+ includeHistorical,
102
+ });
103
+ } else if (tag === 'transcript') {
104
+ await internal.transcriptStore.populateTranscriptSpan(
105
+ name,
106
+ makeChunkIterator,
107
+ { includeHistorical },
108
+ );
109
+ } else {
110
+ Fail`unknown artifact type ${q(tag)} on import`;
111
+ }
112
+ }
113
+
114
+ // We've installed all the artifacts that we could, now do a
115
+ // completeness check.
116
+
117
+ assertComplete(internal, 'operational');
118
+
119
+ await exporter.close();
120
+ return store;
121
+ }
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { initSwingStore, openSwingStore, isSwingStore } from './swingStore.js';
2
+ export { makeSwingStoreExporter } from './exporter.js';
3
+ export { importSwingStore } from './importer.js';
4
+
5
+ // temporary, for the benefit of SwingSet/misc-tools/replay-transcript.js
6
+ export { makeSnapStore } from './snapStore.js';
7
+ // and less temporary, for SwingSet/test/vat-warehouse/test-reload-snapshot.js
8
+ export { makeSnapStoreIO } from './snapStoreIO.js';
9
+
10
+ // eslint-disable-next-line import/export
11
+ export * from './types.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @typedef { import('./snapStore').SnapStoreInternal } SnapStoreInternal
3
+ * @typedef { import('./transcriptStore').TranscriptStoreInternal } TranscriptStoreInternal
4
+ * @typedef { import('./bundleStore').BundleStoreInternal } BundleStoreInternal
5
+ *
6
+ * @typedef {{
7
+ * transcriptStore: TranscriptStoreInternal,
8
+ * snapStore: SnapStoreInternal,
9
+ * bundleStore: BundleStoreInternal,
10
+ * }} SwingStoreInternal
11
+ */
12
+
13
+ // Ensure this is a module.
14
+ export {};
package/src/kvStore.js ADDED
@@ -0,0 +1,172 @@
1
+ // @ts-check
2
+ import { Fail } from '@agoric/assert';
3
+
4
+ /**
5
+ * @typedef {{
6
+ * has: (key: string) => boolean,
7
+ * get: (key: string) => string | undefined,
8
+ * getNextKey: (previousKey: string) => string | undefined,
9
+ * set: (key: string, value: string, bypassHash?: boolean ) => void,
10
+ * delete: (key: string) => void,
11
+ * }} KVStore
12
+ */
13
+
14
+ /**
15
+ * @param {string} key
16
+ */
17
+ export function getKeyType(key) {
18
+ if (key.startsWith('local.')) {
19
+ return 'local';
20
+ } else if (key.startsWith('host.')) {
21
+ return 'host';
22
+ }
23
+ return 'consensus';
24
+ }
25
+
26
+ /**
27
+ * @param {object} db The SQLite database connection.
28
+ * @param {() => void} ensureTxn Called before mutating methods to establish a DB transaction
29
+ * @param {(...args: string[]) => void} trace Called after sets/gets to record a debug log
30
+ * @returns { KVStore }
31
+ */
32
+
33
+ export function makeKVStore(db, ensureTxn, trace) {
34
+ db.exec(`
35
+ CREATE TABLE IF NOT EXISTS kvStore (
36
+ key TEXT,
37
+ value TEXT,
38
+ PRIMARY KEY (key)
39
+ )
40
+ `);
41
+
42
+ const sqlKVGet = db.prepare(`
43
+ SELECT value
44
+ FROM kvStore
45
+ WHERE key = ?
46
+ `);
47
+ sqlKVGet.pluck(true);
48
+
49
+ /**
50
+ * Obtain the value stored for a given key.
51
+ *
52
+ * @param {string} key The key whose value is sought.
53
+ *
54
+ * @returns {string | undefined} the (string) value for the given key, or
55
+ * undefined if there is no such value.
56
+ *
57
+ * @throws if key is not a string.
58
+ */
59
+ function get(key) {
60
+ typeof key === 'string' || Fail`key must be a string`;
61
+ return sqlKVGet.get(key);
62
+ }
63
+
64
+ const sqlKVGetNextKey = db.prepare(`
65
+ SELECT key
66
+ FROM kvStore
67
+ WHERE key > ?
68
+ LIMIT 1
69
+ `);
70
+ sqlKVGetNextKey.pluck(true);
71
+
72
+ /**
73
+ * getNextKey enables callers to iterate over all keys within a
74
+ * given range. To build an iterator of all keys from start
75
+ * (inclusive) to end (exclusive), do:
76
+ *
77
+ * function* iterate(start, end) {
78
+ * if (kvStore.has(start)) {
79
+ * yield start;
80
+ * }
81
+ * let prev = start;
82
+ * while (true) {
83
+ * let next = kvStore.getNextKey(prev);
84
+ * if (!next || next >= end) {
85
+ * break;
86
+ * }
87
+ * yield next;
88
+ * prev = next;
89
+ * }
90
+ * }
91
+ *
92
+ * @param {string} previousKey The key returned will always be later than this one.
93
+ *
94
+ * @returns {string | undefined} a key string, or undefined if we reach the end of the store
95
+ *
96
+ * @throws if previousKey is not a string
97
+ */
98
+
99
+ function getNextKey(previousKey) {
100
+ typeof previousKey === 'string' || Fail`previousKey must be a string`;
101
+ return sqlKVGetNextKey.get(previousKey);
102
+ }
103
+
104
+ /**
105
+ * Test if the state contains a value for a given key.
106
+ *
107
+ * @param {string} key The key that is of interest.
108
+ *
109
+ * @returns {boolean} true if a value is stored for the key, false if not.
110
+ *
111
+ * @throws if key is not a string.
112
+ */
113
+ function has(key) {
114
+ typeof key === 'string' || Fail`key must be a string`;
115
+ return get(key) !== undefined;
116
+ }
117
+
118
+ const sqlKVSet = db.prepare(`
119
+ INSERT INTO kvStore (key, value)
120
+ VALUES (?, ?)
121
+ ON CONFLICT DO UPDATE SET value = excluded.value
122
+ `);
123
+
124
+ /**
125
+ * Store a value for a given key. The value will replace any prior value if
126
+ * there was one.
127
+ *
128
+ * @param {string} key The key whose value is being set.
129
+ * @param {string} value The value to set the key to.
130
+ *
131
+ * @throws if either parameter is not a string.
132
+ */
133
+ function set(key, value) {
134
+ typeof key === 'string' || Fail`key must be a string`;
135
+ typeof value === 'string' || Fail`value must be a string`;
136
+ // synchronous read after write within a transaction is safe
137
+ // The transaction's overall success will be awaited during commit
138
+ ensureTxn();
139
+ sqlKVSet.run(key, value);
140
+ trace('set', key, value);
141
+ }
142
+
143
+ const sqlKVDel = db.prepare(`
144
+ DELETE FROM kvStore
145
+ WHERE key = ?
146
+ `);
147
+
148
+ /**
149
+ * Remove any stored value for a given key. It is permissible for there to
150
+ * be no existing stored value for the key.
151
+ *
152
+ * @param {string} key The key whose value is to be deleted
153
+ *
154
+ * @throws if key is not a string.
155
+ */
156
+ function del(key) {
157
+ typeof key === 'string' || Fail`key must be a string`;
158
+ ensureTxn();
159
+ sqlKVDel.run(key);
160
+ trace('del', key);
161
+ }
162
+
163
+ const kvStore = {
164
+ has,
165
+ get,
166
+ getNextKey,
167
+ set,
168
+ delete: del,
169
+ };
170
+
171
+ return kvStore;
172
+ }
@@ -0,0 +1,65 @@
1
+ import { Fail, q } from '@agoric/assert';
2
+ import { assertComplete } from './assertComplete.js';
3
+
4
+ /**
5
+ * Given a pre-existing swingstore and a SwingStoreExporter, read in
6
+ * all the metadata from the exporter and use it to regenerate any
7
+ * missing metadata records. This can be used to fix the damage caused
8
+ * by #8025.
9
+ *
10
+ * The repair method will call `exporter.getExportData` and examine
11
+ * all entries to do one of three things:
12
+ *
13
+ * 1: kvStore records are ignored (they are not metadata)
14
+ * 2: bundle/snapshot/transcript records whose keys already exist will
15
+ * be compared against the existing data, and an error thrown if
16
+ * they do not match
17
+ * 3: new snapshot/transcript records will be silently added to
18
+ * the swingstore (new bundle records are an error, since we do not
19
+ * tolerate pruned bundles)
20
+ *
21
+ * It will not call `exporter.getArtifactNames` or `getArtifacts`.
22
+ *
23
+ * At the end of the process, the DB will contain pending changes in
24
+ * an open transaction. The caller is responsible for calling
25
+ * `hostStorage.commit()` when they are ready.
26
+ *
27
+ * @param {import('./internal.js').SwingStoreInternal} internal
28
+ * @param {import('./exporter').SwingStoreExporter} exporter
29
+ * @returns {Promise<void>}
30
+ */
31
+ export async function doRepairMetadata(internal, exporter) {
32
+ // first we strip kvStore entries and deduplicate the rest
33
+
34
+ const allMetadata = new Map();
35
+
36
+ for await (const [key, value] of exporter.getExportData()) {
37
+ const [tag] = key.split('.', 1);
38
+ if (tag === 'kv') {
39
+ continue;
40
+ } else if (value == null) {
41
+ allMetadata.delete(key);
42
+ } else {
43
+ allMetadata.set(key, value);
44
+ }
45
+ }
46
+
47
+ // then process the metadata records
48
+
49
+ for (const [key, value] of allMetadata.entries()) {
50
+ const [tag] = key.split('.', 1);
51
+ if (tag === 'bundle') {
52
+ internal.bundleStore.repairBundleRecord(key, value);
53
+ } else if (tag === 'snapshot') {
54
+ internal.snapStore.repairSnapshotRecord(key, value);
55
+ } else if (tag === 'transcript') {
56
+ internal.transcriptStore.repairTranscriptSpanRecord(key, value);
57
+ } else {
58
+ Fail`unknown export-data type in key ${q(key)} on repairMetadata`;
59
+ }
60
+ }
61
+
62
+ // and do a completeness check
63
+ assertComplete(internal, 'operational');
64
+ await exporter.close();
65
+ }
package/src/snapStore.js CHANGED
@@ -25,7 +25,12 @@ import { buffer } from './util.js';
25
25
  */
26
26
 
27
27
  /**
28
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
28
+ * @template T
29
+ * @typedef { import('./exporter').AnyIterableIterator<T> } AnyIterableIterator<T>
30
+ */
31
+
32
+ /**
33
+ * @typedef { import('./exporter').SwingStoreExporter } SwingStoreExporter
29
34
  *
30
35
  * @typedef {{
31
36
  * loadSnapshot: (vatID: string) => AsyncIterableIterator<Uint8Array>,
@@ -37,10 +42,13 @@ import { buffer } from './util.js';
37
42
  * }} SnapStore
38
43
  *
39
44
  * @typedef {{
40
- * exportSnapshot: (name: string, includeHistorical: boolean) => AsyncIterableIterator<Uint8Array>,
41
- * importSnapshot: (artifactName: string, exporter: SwingStoreExporter, artifactMetadata: Map) => void,
45
+ * exportSnapshot: (name: string) => AsyncIterableIterator<Uint8Array>,
42
46
  * getExportRecords: (includeHistorical: boolean) => IterableIterator<readonly [key: string, value: string]>,
43
47
  * getArtifactNames: (includeHistorical: boolean) => AsyncIterableIterator<string>,
48
+ * importSnapshotRecord: (key: string, value: string) => void,
49
+ * populateSnapshot: (name: string, makeChunkIterator: () => AnyIterableIterator<Uint8Array>, options: { includeHistorical: boolean }) => Promise<void>,
50
+ * assertComplete: (level: 'operational') => void,
51
+ * repairSnapshotRecord: (key: string, value: string) => void,
44
52
  * }} SnapStoreInternal
45
53
  *
46
54
  * @typedef {{
@@ -81,11 +89,26 @@ export function makeSnapStore(
81
89
  compressedSize INTEGER,
82
90
  compressedSnapshot BLOB,
83
91
  PRIMARY KEY (vatID, snapPos),
84
- UNIQUE (vatID, inUse),
85
- CHECK(compressedSnapshot is not null or inUse is null)
92
+ UNIQUE (vatID, inUse)
86
93
  )
87
94
  `);
88
95
 
96
+ // NOTE: there are two versions of this schema. The original, which
97
+ // we'll call "version 1A", has a:
98
+ // CHECK(compressedSnapshot is not null or inUse is null)
99
+ // in the table. Version 1B is missing that constraint. Any DB
100
+ // created by the original code will use 1A. Any DB created by the
101
+ // new version will use 1B. The import process needs to temporarily
102
+ // violate that check, but any DB created by `importSwingStore` is
103
+ // (by definition) new, so it will use 1B, which doesn't enforce the
104
+ // check. We expect to implement schema migration
105
+ // (https://github.com/Agoric/agoric-sdk/issues/8089) soon, which
106
+ // will upgrade both 1A and 1B to "version 2", which will omit the
107
+ // check (in addition to any other changes we need at that point)
108
+
109
+ // pruned snapshots will have compressedSnapshot of NULL, and might
110
+ // also have NULL for uncompressedSize and compressedSize
111
+
89
112
  const sqlDeleteAllUnusedSnapshots = db.prepare(`
90
113
  DELETE FROM snapshots
91
114
  WHERE inUse is null
@@ -98,6 +121,12 @@ export function makeSnapStore(
98
121
  function deleteAllUnusedSnapshots() {
99
122
  ensureTxn();
100
123
  sqlDeleteAllUnusedSnapshots.run();
124
+
125
+ // NOTE: this is more than pruning the snapshot data, it deletes
126
+ // the metadata/hash as well, making it impossible to safely
127
+ // repopulate the snapshot data from an untrusted source. We need
128
+ // to replace this with a method that merely nulls out the
129
+ // 'compressedSnapshot' field.
101
130
  }
102
131
 
103
132
  function snapshotArtifactName(rec) {
@@ -255,10 +284,9 @@ export function makeSnapStore(
255
284
  * `snapshot.${vatID}.${startPos}`
256
285
  *
257
286
  * @param {string} name
258
- * @param {boolean} includeHistorical
259
287
  * @returns {AsyncIterableIterator<Uint8Array>}
260
288
  */
261
- function exportSnapshot(name, includeHistorical) {
289
+ function exportSnapshot(name) {
262
290
  typeof name === 'string' || Fail`artifact name must be a string`;
263
291
  const parts = name.split('.');
264
292
  const [type, vatID, pos] = parts;
@@ -268,9 +296,8 @@ export function makeSnapStore(
268
296
  const snapPos = Number(pos);
269
297
  const snapshotInfo = sqlGetSnapshot.get(vatID, snapPos);
270
298
  snapshotInfo || Fail`snapshot ${q(name)} not available`;
271
- const { inUse, compressedSnapshot } = snapshotInfo;
299
+ const { compressedSnapshot } = snapshotInfo;
272
300
  compressedSnapshot || Fail`artifact ${q(name)} is not available`;
273
- inUse || includeHistorical || Fail`artifact ${q(name)} is not available`;
274
301
  // weird construct here is because we need to be able to throw before the generator starts
275
302
  async function* exporter() {
276
303
  const gzReader = Readable.from(compressedSnapshot);
@@ -412,6 +439,13 @@ export function makeSnapStore(
412
439
  ORDER BY vatID, snapPos
413
440
  `);
414
441
 
442
+ const sqlGetAvailableSnapshots = db.prepare(`
443
+ SELECT vatID, snapPos, hash, uncompressedSize, compressedSize, inUse
444
+ FROM snapshots
445
+ WHERE inUse IS ? AND compressedSnapshot is not NULL
446
+ ORDER BY vatID, snapPos
447
+ `);
448
+
415
449
  /**
416
450
  * Obtain artifact metadata records for spanshots contained in this store.
417
451
  *
@@ -448,40 +482,112 @@ export function makeSnapStore(
448
482
  }
449
483
 
450
484
  async function* getArtifactNames(includeHistorical) {
451
- for (const rec of sqlGetSnapshotMetadata.iterate(1)) {
485
+ for (const rec of sqlGetAvailableSnapshots.iterate(1)) {
452
486
  yield snapshotArtifactName(rec);
453
487
  }
454
488
  if (includeHistorical) {
455
- for (const rec of sqlGetSnapshotMetadata.iterate(null)) {
489
+ for (const rec of sqlGetAvailableSnapshots.iterate(null)) {
456
490
  yield snapshotArtifactName(rec);
457
491
  }
458
492
  }
459
493
  }
460
494
 
495
+ const sqlAddSnapshotRecord = db.prepare(`
496
+ INSERT INTO snapshots (vatID, snapPos, hash, inUse)
497
+ VALUES (?, ?, ?, ?)
498
+ `);
499
+
500
+ function importSnapshotRecord(key, value) {
501
+ ensureTxn();
502
+ const [tag, ...pieces] = key.split('.');
503
+ assert.equal(tag, 'snapshot');
504
+ const [_vatID, endPos] = pieces;
505
+ if (endPos === 'current') {
506
+ // metadata['snapshot.v1.current'] = 'snapshot.v1.5' , i.e. it
507
+ // points to the name of the current artifact. We could
508
+ // conceivably remember this and compare it against the .inUse
509
+ // property of that record, but it's not worth the effort (we
510
+ // might encounter the records in either order).
511
+ return;
512
+ }
513
+ const metadata = JSON.parse(value);
514
+ const { vatID, snapPos, hash, inUse } = metadata;
515
+ vatID || Fail`snapshot metadata missing vatID: ${metadata}`;
516
+ snapPos !== undefined ||
517
+ Fail`snapshot metadata missing snapPos: ${metadata}`;
518
+ hash || Fail`snapshot metadata missing hash: ${metadata}`;
519
+ inUse !== undefined || Fail`snapshot metadata missing inUse: ${metadata}`;
520
+
521
+ sqlAddSnapshotRecord.run(vatID, snapPos, hash, inUse ? 1 : null);
522
+ }
523
+
524
+ const sqlGetSnapshotHashFor = db.prepare(`
525
+ SELECT hash, inUse
526
+ FROM snapshots
527
+ WHERE vatID = ? AND snapPos = ?
528
+ `);
529
+
530
+ function repairSnapshotRecord(key, value) {
531
+ ensureTxn();
532
+ const [tag, keyVatID, keySnapPos] = key.split('.');
533
+ assert.equal(tag, 'snapshot');
534
+ if (keySnapPos === 'current') {
535
+ // "snapshot.${vatID}.current" entries are meta-metadata: they
536
+ // point to the metadata key of the current snapshot, to avoid
537
+ // the need for an expensive search
538
+ return;
539
+ }
540
+ const metadata = JSON.parse(value);
541
+ const { vatID, snapPos, hash, inUse } = metadata;
542
+ assert.equal(keyVatID, vatID);
543
+ assert.equal(Number(keySnapPos), snapPos);
544
+ const existing = sqlGetSnapshotHashFor.get(vatID, snapPos);
545
+ if (existing) {
546
+ if (
547
+ Boolean(existing.inUse) !== Boolean(inUse) ||
548
+ existing.hash !== hash
549
+ ) {
550
+ throw Fail`repairSnapshotRecord metadata mismatch: ${existing} vs ${metadata}`;
551
+ }
552
+ } else {
553
+ sqlAddSnapshotRecord.run(vatID, snapPos, hash, inUse ? 1 : null);
554
+ }
555
+ }
556
+
557
+ const sqlPopulateSnapshot = db.prepare(`
558
+ UPDATE snapshots SET
559
+ uncompressedSize = ?, compressedSize = ?, compressedSnapshot = ?
560
+ WHERE vatID = ? AND snapPos = ?
561
+ `);
562
+
461
563
  /**
462
564
  * @param {string} name Artifact name of the snapshot
463
- * @param {SwingStoreExporter} exporter Whence to get the bits
464
- * @param {object} info Metadata describing the artifact
565
+ * @param {() => AnyIterableIterator<Uint8Array>} makeChunkIterator get an iterator of snapshot byte chunks
566
+ * @param {object} options
567
+ * @param {boolean} options.includeHistorical
465
568
  * @returns {Promise<void>}
466
569
  */
467
- async function importSnapshot(name, exporter, info) {
570
+ async function populateSnapshot(name, makeChunkIterator, options) {
571
+ ensureTxn();
572
+ const { includeHistorical } = options;
468
573
  const parts = name.split('.');
469
574
  const [type, vatID, rawEndPos] = parts;
470
575
  // prettier-ignore
471
576
  parts.length === 3 && type === 'snapshot' ||
472
577
  Fail`expected snapshot name of the form 'snapshot.{vatID}.{snapPos}', saw '${q(name)}'`;
473
- // prettier-ignore
474
- info.vatID === vatID ||
475
- Fail`snapshot name says vatID ${q(vatID)}, metadata says ${q(info.vatID)}`;
476
578
  const snapPos = Number(rawEndPos);
477
- // prettier-ignore
478
- info.snapPos === snapPos ||
479
- Fail`snapshot name says snapPos ${q(snapPos)}, metadata says ${q(info.snapPos)}`;
579
+ const metadata =
580
+ sqlGetSnapshotHashFor.get(vatID, snapPos) ||
581
+ Fail`no metadata for snapshot ${name}`;
480
582
 
481
- const artifactChunks = exporter.getArtifact(name);
583
+ if (!metadata.inUse && !includeHistorical) {
584
+ return; // ignore old snapshots
585
+ }
586
+
587
+ const artifactChunks = makeChunkIterator();
482
588
  const inStream = Readable.from(artifactChunks);
483
- let size = 0;
484
- inStream.on('data', chunk => (size += chunk.length));
589
+ let uncompressedSize = 0;
590
+ inStream.on('data', chunk => (uncompressedSize += chunk.length));
485
591
  const hashStream = createHash('sha256');
486
592
  const gzip = createGzip();
487
593
  inStream.pipe(hashStream);
@@ -489,21 +595,37 @@ export function makeSnapStore(
489
595
  const compressedArtifact = await buffer(gzip);
490
596
  await finished(inStream);
491
597
  const hash = hashStream.digest('hex');
598
+
599
+ // validate against the previously-established metadata
492
600
  // prettier-ignore
493
- info.hash === hash ||
494
- Fail`snapshot ${q(name)} hash is ${q(hash)}, metadata says ${q(info.hash)}`;
495
- ensureTxn();
496
- sqlSaveSnapshot.run(
497
- vatID,
498
- snapPos,
499
- info.inUse ? 1 : null,
500
- info.hash,
501
- size,
601
+ metadata.hash === hash ||
602
+ Fail`snapshot ${q(name)} hash is ${q(hash)}, metadata says ${q(metadata.hash)}`;
603
+
604
+ sqlPopulateSnapshot.run(
605
+ uncompressedSize,
502
606
  compressedArtifact.length,
503
607
  compressedArtifact,
608
+ vatID,
609
+ snapPos,
504
610
  );
505
611
  }
506
612
 
613
+ const sqlListPrunedCurrentSnapshots = db.prepare(`
614
+ SELECT vatID FROM snapshots
615
+ WHERE inUse = 1 AND compressedSnapshot IS NULL
616
+ ORDER BY vatID
617
+ `);
618
+ sqlListPrunedCurrentSnapshots.pluck();
619
+
620
+ function assertComplete(level) {
621
+ assert.equal(level, 'operational'); // for now
622
+ // every 'inUse' snapshot must be populated
623
+ const vatIDs = sqlListPrunedCurrentSnapshots.all();
624
+ if (vatIDs.length) {
625
+ throw Fail`current snapshots are pruned for vats ${vatIDs.join(',')}`;
626
+ }
627
+ }
628
+
507
629
  const sqlListAllSnapshots = db.prepare(`
508
630
  SELECT vatID, snapPos, inUse, hash, uncompressedSize, compressedSize
509
631
  FROM snapshots
@@ -563,10 +685,15 @@ export function makeSnapStore(
563
685
  deleteVatSnapshots,
564
686
  stopUsingLastSnapshot,
565
687
  getSnapshotInfo,
688
+
566
689
  getExportRecords,
567
690
  getArtifactNames,
568
691
  exportSnapshot,
569
- importSnapshot,
692
+
693
+ importSnapshotRecord,
694
+ populateSnapshot,
695
+ assertComplete,
696
+ repairSnapshotRecord,
570
697
 
571
698
  hasHash,
572
699
  listAllSnapshots,
@@ -0,0 +1,8 @@
1
+ import { performance } from 'perf_hooks';
2
+ import { makeMeasureSeconds } from '@agoric/internal';
3
+
4
+ export function makeSnapStoreIO() {
5
+ return {
6
+ measureSeconds: makeMeasureSeconds(performance.now),
7
+ };
8
+ }