@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.
- package/docs/data-export.md +20 -4
- package/docs/swingstore.md +52 -0
- package/package.json +8 -5
- package/src/assertComplete.js +22 -0
- package/src/bundleStore.js +136 -47
- package/src/exporter.js +175 -0
- package/src/importer.js +121 -0
- package/src/index.js +11 -0
- package/src/internal.js +14 -0
- package/src/kvStore.js +172 -0
- package/src/repairMetadata.js +65 -0
- package/src/snapStore.js +160 -33
- package/src/snapStoreIO.js +8 -0
- package/src/swingStore.js +52 -587
- package/src/transcriptStore.js +170 -37
- package/src/types.d.ts +14 -0
- package/src/types.js +6 -0
- package/src/util.js +7 -1
- package/test/test-bundles.js +9 -7
- package/test/test-export.js +308 -0
- package/test/test-exportImport.js +36 -54
- package/test/test-import.js +476 -0
- package/test/test-repair-metadata.js +131 -0
- package/test/util.js +26 -0
package/src/importer.js
ADDED
|
@@ -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';
|
package/src/internal.js
ADDED
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
485
|
+
for (const rec of sqlGetAvailableSnapshots.iterate(1)) {
|
|
452
486
|
yield snapshotArtifactName(rec);
|
|
453
487
|
}
|
|
454
488
|
if (includeHistorical) {
|
|
455
|
-
for (const rec of
|
|
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 {
|
|
464
|
-
* @param {object}
|
|
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
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
Fail`
|
|
579
|
+
const metadata =
|
|
580
|
+
sqlGetSnapshotHashFor.get(vatID, snapPos) ||
|
|
581
|
+
Fail`no metadata for snapshot ${name}`;
|
|
480
582
|
|
|
481
|
-
|
|
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
|
|
484
|
-
inStream.on('data', chunk => (
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
692
|
+
|
|
693
|
+
importSnapshotRecord,
|
|
694
|
+
populateSnapshot,
|
|
695
|
+
assertComplete,
|
|
696
|
+
repairSnapshotRecord,
|
|
570
697
|
|
|
571
698
|
hasHash,
|
|
572
699
|
listAllSnapshots,
|