@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/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,22 @@
1
+ import { Fail, q } from '@agoric/assert';
2
+
3
+ /**
4
+ * @typedef { import('./snapStore.js').SnapStoreInternal } SnapStoreInternal
5
+ * @typedef { import('./transcriptStore.js').TranscriptStoreInternal } TranscriptStoreInternal
6
+ * @typedef { import('./bundleStore.js').BundleStoreInternal } BundleStoreInternal
7
+ *
8
+ * @typedef {{
9
+ * transcriptStore: TranscriptStoreInternal,
10
+ * snapStore: SnapStoreInternal,
11
+ * bundleStore: BundleStoreInternal,
12
+ * }} SwingStoreInternal
13
+ *
14
+ * @typedef {'operational' | 'replay' | 'archival' | 'debug'} ArtifactMode
15
+ */
16
+
17
+ export const artifactModes = ['operational', 'replay', 'archival', 'debug'];
18
+ export function validateArtifactMode(artifactMode) {
19
+ if (!artifactModes.includes(artifactMode)) {
20
+ Fail`invalid artifactMode ${q(artifactMode)}`;
21
+ }
22
+ }
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,67 @@
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.js').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
+ /** @type { import('./internal.js').ArtifactMode } */
64
+ const artifactMode = 'operational';
65
+ assertComplete(internal, artifactMode);
66
+ await exporter.close();
67
+ }
package/src/snapStore.js CHANGED
@@ -4,7 +4,10 @@ import { finished as finishedCallback, PassThrough, Readable } from 'stream';
4
4
  import { promisify } from 'util';
5
5
  import { createGzip, createGunzip } from 'zlib';
6
6
  import { Fail, q } from '@agoric/assert';
7
- import { aggregateTryFinally, PromiseAllOrErrors } from '@agoric/internal';
7
+ import {
8
+ aggregateTryFinally,
9
+ PromiseAllOrErrors,
10
+ } from '@agoric/internal/src/node/utils.js';
8
11
  import { buffer } from './util.js';
9
12
 
10
13
  /**
@@ -25,7 +28,12 @@ import { buffer } from './util.js';
25
28
  */
26
29
 
27
30
  /**
28
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
31
+ * @import {AnyIterableIterator} from './exporter.js'
32
+ */
33
+
34
+ /**
35
+ * @typedef { import('./exporter.js').SwingStoreExporter } SwingStoreExporter
36
+ * @typedef { import('./internal.js').ArtifactMode } ArtifactMode
29
37
  *
30
38
  * @typedef {{
31
39
  * loadSnapshot: (vatID: string) => AsyncIterableIterator<Uint8Array>,
@@ -37,10 +45,13 @@ import { buffer } from './util.js';
37
45
  * }} SnapStore
38
46
  *
39
47
  * @typedef {{
40
- * exportSnapshot: (name: string, includeHistorical: boolean) => AsyncIterableIterator<Uint8Array>,
41
- * importSnapshot: (artifactName: string, exporter: SwingStoreExporter, artifactMetadata: Map) => void,
48
+ * exportSnapshot: (name: string) => AsyncIterableIterator<Uint8Array>,
42
49
  * getExportRecords: (includeHistorical: boolean) => IterableIterator<readonly [key: string, value: string]>,
43
- * getArtifactNames: (includeHistorical: boolean) => AsyncIterableIterator<string>,
50
+ * getArtifactNames: (artifactMode: ArtifactMode) => AsyncIterableIterator<string>,
51
+ * importSnapshotRecord: (key: string, value: string) => void,
52
+ * populateSnapshot: (name: string, makeChunkIterator: () => AnyIterableIterator<Uint8Array>, options: { artifactMode: ArtifactMode }) => Promise<void>,
53
+ * assertComplete: (checkMode: Omit<ArtifactMode, 'debug'>) => void,
54
+ * repairSnapshotRecord: (key: string, value: string) => void,
44
55
  * }} SnapStoreInternal
45
56
  *
46
57
  * @typedef {{
@@ -81,11 +92,26 @@ export function makeSnapStore(
81
92
  compressedSize INTEGER,
82
93
  compressedSnapshot BLOB,
83
94
  PRIMARY KEY (vatID, snapPos),
84
- UNIQUE (vatID, inUse),
85
- CHECK(compressedSnapshot is not null or inUse is null)
95
+ UNIQUE (vatID, inUse)
86
96
  )
87
97
  `);
88
98
 
99
+ // NOTE: there are two versions of this schema. The original, which
100
+ // we'll call "version 1A", has a:
101
+ // CHECK(compressedSnapshot is not null or inUse is null)
102
+ // in the table. Version 1B is missing that constraint. Any DB
103
+ // created by the original code will use 1A. Any DB created by the
104
+ // new version will use 1B. The import process needs to temporarily
105
+ // violate that check, but any DB created by `importSwingStore` is
106
+ // (by definition) new, so it will use 1B, which doesn't enforce the
107
+ // check. We expect to implement schema migration
108
+ // (https://github.com/Agoric/agoric-sdk/issues/8089) soon, which
109
+ // will upgrade both 1A and 1B to "version 2", which will omit the
110
+ // check (in addition to any other changes we need at that point)
111
+
112
+ // pruned snapshots will have compressedSnapshot of NULL, and might
113
+ // also have NULL for uncompressedSize and compressedSize
114
+
89
115
  const sqlDeleteAllUnusedSnapshots = db.prepare(`
90
116
  DELETE FROM snapshots
91
117
  WHERE inUse is null
@@ -98,6 +124,12 @@ export function makeSnapStore(
98
124
  function deleteAllUnusedSnapshots() {
99
125
  ensureTxn();
100
126
  sqlDeleteAllUnusedSnapshots.run();
127
+
128
+ // NOTE: this is more than pruning the snapshot data, it deletes
129
+ // the metadata/hash as well, making it impossible to safely
130
+ // repopulate the snapshot data from an untrusted source. We need
131
+ // to replace this with a method that merely nulls out the
132
+ // 'compressedSnapshot' field.
101
133
  }
102
134
 
103
135
  function snapshotArtifactName(rec) {
@@ -255,10 +287,9 @@ export function makeSnapStore(
255
287
  * `snapshot.${vatID}.${startPos}`
256
288
  *
257
289
  * @param {string} name
258
- * @param {boolean} includeHistorical
259
290
  * @returns {AsyncIterableIterator<Uint8Array>}
260
291
  */
261
- function exportSnapshot(name, includeHistorical) {
292
+ function exportSnapshot(name) {
262
293
  typeof name === 'string' || Fail`artifact name must be a string`;
263
294
  const parts = name.split('.');
264
295
  const [type, vatID, pos] = parts;
@@ -268,9 +299,8 @@ export function makeSnapStore(
268
299
  const snapPos = Number(pos);
269
300
  const snapshotInfo = sqlGetSnapshot.get(vatID, snapPos);
270
301
  snapshotInfo || Fail`snapshot ${q(name)} not available`;
271
- const { inUse, compressedSnapshot } = snapshotInfo;
302
+ const { compressedSnapshot } = snapshotInfo;
272
303
  compressedSnapshot || Fail`artifact ${q(name)} is not available`;
273
- inUse || includeHistorical || Fail`artifact ${q(name)} is not available`;
274
304
  // weird construct here is because we need to be able to throw before the generator starts
275
305
  async function* exporter() {
276
306
  const gzReader = Readable.from(compressedSnapshot);
@@ -278,6 +308,7 @@ export function makeSnapStore(
278
308
  const snapshotReader = gzReader.pipe(unzipper);
279
309
  yield* snapshotReader;
280
310
  }
311
+ harden(exporter);
281
312
  return exporter();
282
313
  }
283
314
 
@@ -305,17 +336,18 @@ export function makeSnapStore(
305
336
  snapReader.pipe(hashStream);
306
337
  snapReader.pipe(output);
307
338
 
339
+ await null;
308
340
  try {
309
341
  yield* output;
310
342
  } finally {
311
343
  gzReader.destroy();
312
- // eslint-disable-next-line @jessie.js/no-nested-await
313
344
  await finished(gzReader);
314
345
  const hash = hashStream.digest('hex');
315
346
  hash === snapshotID ||
316
347
  Fail`actual hash ${q(hash)} !== expected ${q(snapshotID)}`;
317
348
  }
318
349
  }
350
+ harden(loadSnapshot);
319
351
 
320
352
  const sqlDeleteVatSnapshots = db.prepare(`
321
353
  DELETE FROM snapshots
@@ -412,6 +444,13 @@ export function makeSnapStore(
412
444
  ORDER BY vatID, snapPos
413
445
  `);
414
446
 
447
+ const sqlGetAvailableSnapshots = db.prepare(`
448
+ SELECT vatID, snapPos, hash, uncompressedSize, compressedSize, inUse
449
+ FROM snapshots
450
+ WHERE inUse IS ? AND compressedSnapshot is not NULL
451
+ ORDER BY vatID, snapPos
452
+ `);
453
+
415
454
  /**
416
455
  * Obtain artifact metadata records for spanshots contained in this store.
417
456
  *
@@ -446,42 +485,116 @@ export function makeSnapStore(
446
485
  }
447
486
  }
448
487
  }
488
+ harden(getExportRecords);
449
489
 
450
- async function* getArtifactNames(includeHistorical) {
451
- for (const rec of sqlGetSnapshotMetadata.iterate(1)) {
490
+ async function* getArtifactNames(artifactMode) {
491
+ for (const rec of sqlGetAvailableSnapshots.iterate(1)) {
452
492
  yield snapshotArtifactName(rec);
453
493
  }
454
- if (includeHistorical) {
455
- for (const rec of sqlGetSnapshotMetadata.iterate(null)) {
494
+ if (artifactMode === 'debug') {
495
+ for (const rec of sqlGetAvailableSnapshots.iterate(null)) {
456
496
  yield snapshotArtifactName(rec);
457
497
  }
458
498
  }
459
499
  }
500
+ harden(getArtifactNames);
501
+
502
+ const sqlAddSnapshotRecord = db.prepare(`
503
+ INSERT INTO snapshots (vatID, snapPos, hash, inUse)
504
+ VALUES (?, ?, ?, ?)
505
+ `);
506
+
507
+ function importSnapshotRecord(key, value) {
508
+ ensureTxn();
509
+ const [tag, ...pieces] = key.split('.');
510
+ assert.equal(tag, 'snapshot');
511
+ const [_vatID, endPos] = pieces;
512
+ if (endPos === 'current') {
513
+ // metadata['snapshot.v1.current'] = 'snapshot.v1.5' , i.e. it
514
+ // points to the name of the current artifact. We could
515
+ // conceivably remember this and compare it against the .inUse
516
+ // property of that record, but it's not worth the effort (we
517
+ // might encounter the records in either order).
518
+ return;
519
+ }
520
+ const metadata = JSON.parse(value);
521
+ const { vatID, snapPos, hash, inUse } = metadata;
522
+ vatID || Fail`snapshot metadata missing vatID: ${metadata}`;
523
+ snapPos !== undefined ||
524
+ Fail`snapshot metadata missing snapPos: ${metadata}`;
525
+ hash || Fail`snapshot metadata missing hash: ${metadata}`;
526
+ inUse !== undefined || Fail`snapshot metadata missing inUse: ${metadata}`;
527
+
528
+ sqlAddSnapshotRecord.run(vatID, snapPos, hash, inUse ? 1 : null);
529
+ }
530
+
531
+ const sqlGetSnapshotHashFor = db.prepare(`
532
+ SELECT hash, inUse
533
+ FROM snapshots
534
+ WHERE vatID = ? AND snapPos = ?
535
+ `);
536
+
537
+ function repairSnapshotRecord(key, value) {
538
+ ensureTxn();
539
+ const [tag, keyVatID, keySnapPos] = key.split('.');
540
+ assert.equal(tag, 'snapshot');
541
+ if (keySnapPos === 'current') {
542
+ // "snapshot.${vatID}.current" entries are meta-metadata: they
543
+ // point to the metadata key of the current snapshot, to avoid
544
+ // the need for an expensive search
545
+ return;
546
+ }
547
+ const metadata = JSON.parse(value);
548
+ const { vatID, snapPos, hash, inUse } = metadata;
549
+ assert.equal(keyVatID, vatID);
550
+ assert.equal(Number(keySnapPos), snapPos);
551
+ const existing = sqlGetSnapshotHashFor.get(vatID, snapPos);
552
+ if (existing) {
553
+ if (
554
+ Boolean(existing.inUse) !== Boolean(inUse) ||
555
+ existing.hash !== hash
556
+ ) {
557
+ throw Fail`repairSnapshotRecord metadata mismatch: ${existing} vs ${metadata}`;
558
+ }
559
+ } else {
560
+ sqlAddSnapshotRecord.run(vatID, snapPos, hash, inUse ? 1 : null);
561
+ }
562
+ }
563
+
564
+ const sqlPopulateSnapshot = db.prepare(`
565
+ UPDATE snapshots SET
566
+ uncompressedSize = ?, compressedSize = ?, compressedSnapshot = ?
567
+ WHERE vatID = ? AND snapPos = ?
568
+ `);
460
569
 
461
570
  /**
462
571
  * @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
572
+ * @param {() => AnyIterableIterator<Uint8Array>} makeChunkIterator get an iterator of snapshot byte chunks
573
+ * @param {object} options
574
+ * @param {ArtifactMode} options.artifactMode
465
575
  * @returns {Promise<void>}
466
576
  */
467
- async function importSnapshot(name, exporter, info) {
577
+ async function populateSnapshot(name, makeChunkIterator, options) {
578
+ ensureTxn();
579
+ const { artifactMode } = options;
468
580
  const parts = name.split('.');
469
581
  const [type, vatID, rawEndPos] = parts;
470
582
  // prettier-ignore
471
583
  parts.length === 3 && type === 'snapshot' ||
472
584
  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
585
  const snapPos = Number(rawEndPos);
477
- // prettier-ignore
478
- info.snapPos === snapPos ||
479
- Fail`snapshot name says snapPos ${q(snapPos)}, metadata says ${q(info.snapPos)}`;
586
+ const metadata =
587
+ sqlGetSnapshotHashFor.get(vatID, snapPos) ||
588
+ Fail`no metadata for snapshot ${name}`;
480
589
 
481
- const artifactChunks = exporter.getArtifact(name);
590
+ if (!metadata.inUse && artifactMode !== 'debug') {
591
+ return; // ignore old snapshots
592
+ }
593
+
594
+ const artifactChunks = makeChunkIterator();
482
595
  const inStream = Readable.from(artifactChunks);
483
- let size = 0;
484
- inStream.on('data', chunk => (size += chunk.length));
596
+ let uncompressedSize = 0;
597
+ inStream.on('data', chunk => (uncompressedSize += chunk.length));
485
598
  const hashStream = createHash('sha256');
486
599
  const gzip = createGzip();
487
600
  inStream.pipe(hashStream);
@@ -489,21 +602,37 @@ export function makeSnapStore(
489
602
  const compressedArtifact = await buffer(gzip);
490
603
  await finished(inStream);
491
604
  const hash = hashStream.digest('hex');
605
+
606
+ // validate against the previously-established metadata
492
607
  // 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,
608
+ metadata.hash === hash ||
609
+ Fail`snapshot ${q(name)} hash is ${q(hash)}, metadata says ${q(metadata.hash)}`;
610
+
611
+ sqlPopulateSnapshot.run(
612
+ uncompressedSize,
502
613
  compressedArtifact.length,
503
614
  compressedArtifact,
615
+ vatID,
616
+ snapPos,
504
617
  );
505
618
  }
506
619
 
620
+ const sqlListPrunedCurrentSnapshots = db.prepare(`
621
+ SELECT vatID FROM snapshots
622
+ WHERE inUse = 1 AND compressedSnapshot IS NULL
623
+ ORDER BY vatID
624
+ `);
625
+ sqlListPrunedCurrentSnapshots.pluck();
626
+
627
+ function assertComplete(checkMode) {
628
+ assert(checkMode !== 'debug', checkMode);
629
+ // every 'inUse' snapshot must be populated
630
+ const vatIDs = sqlListPrunedCurrentSnapshots.all();
631
+ if (vatIDs.length) {
632
+ throw Fail`current snapshots are pruned for vats ${vatIDs.join(',')}`;
633
+ }
634
+ }
635
+
507
636
  const sqlListAllSnapshots = db.prepare(`
508
637
  SELECT vatID, snapPos, inUse, hash, uncompressedSize, compressedSize
509
638
  FROM snapshots
@@ -517,6 +646,7 @@ export function makeSnapStore(
517
646
  function* listAllSnapshots() {
518
647
  yield* sqlListAllSnapshots.iterate();
519
648
  }
649
+ harden(listAllSnapshots);
520
650
 
521
651
  const sqlDumpCurrentSnapshots = db.prepare(`
522
652
  SELECT vatID, snapPos, hash, compressedSnapshot, inUse
@@ -563,10 +693,15 @@ export function makeSnapStore(
563
693
  deleteVatSnapshots,
564
694
  stopUsingLastSnapshot,
565
695
  getSnapshotInfo,
696
+
566
697
  getExportRecords,
567
698
  getArtifactNames,
568
699
  exportSnapshot,
569
- importSnapshot,
700
+
701
+ importSnapshotRecord,
702
+ populateSnapshot,
703
+ assertComplete,
704
+ repairSnapshotRecord,
570
705
 
571
706
  hasHash,
572
707
  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.bind(performance)),
7
+ };
8
+ }