@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/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';
|
package/src/internal.js
ADDED
|
@@ -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 {
|
|
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
|
-
* @
|
|
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
|
|
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: (
|
|
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
|
|
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 {
|
|
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(
|
|
451
|
-
for (const rec of
|
|
490
|
+
async function* getArtifactNames(artifactMode) {
|
|
491
|
+
for (const rec of sqlGetAvailableSnapshots.iterate(1)) {
|
|
452
492
|
yield snapshotArtifactName(rec);
|
|
453
493
|
}
|
|
454
|
-
if (
|
|
455
|
-
for (const rec of
|
|
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 {
|
|
464
|
-
* @param {object}
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
Fail`
|
|
586
|
+
const metadata =
|
|
587
|
+
sqlGetSnapshotHashFor.get(vatID, snapPos) ||
|
|
588
|
+
Fail`no metadata for snapshot ${name}`;
|
|
480
589
|
|
|
481
|
-
|
|
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
|
|
484
|
-
inStream.on('data', chunk => (
|
|
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
|
-
|
|
494
|
-
Fail`snapshot ${q(name)} hash is ${q(hash)}, metadata says ${q(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
700
|
+
|
|
701
|
+
importSnapshotRecord,
|
|
702
|
+
populateSnapshot,
|
|
703
|
+
assertComplete,
|
|
704
|
+
repairSnapshotRecord,
|
|
570
705
|
|
|
571
706
|
hasHash,
|
|
572
707
|
listAllSnapshots,
|