@agoric/swing-store 0.9.2-u11wf.0 → 0.9.2-u13.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/CHANGELOG.md +24 -0
- package/docs/data-export.md +10 -20
- package/package.json +3 -3
- package/src/exporter.js +33 -0
- package/src/importer.js +14 -6
- package/src/swingStore.js +31 -3
- package/test/test-bundles.js +11 -1
- package/test/test-export.js +9 -0
- package/test/test-exportImport.js +1 -0
- package/test/test-import.js +5 -0
- package/test/test-repair-metadata.js +23 -9
- package/test/test-state.js +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
### [0.9.2-u13.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.2-u12.0...@agoric/swing-store@0.9.2-u13.0) (2023-12-07)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add exporter.getHostKV() API ([16435d2](https://github.com/Agoric/agoric-sdk/commit/16435d20e9ede86916a54c7bae54ecfc59e4c950)), closes [#8523](https://github.com/Agoric/agoric-sdk/issues/8523)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### [0.9.2-u12.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.2-u11wf.0...@agoric/swing-store@0.9.2-u12.0) (2023-11-10)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **swing-store:** faster import of swing-store ([35aef87](https://github.com/Agoric/agoric-sdk/commit/35aef87ec0f10b7f0cdce462ac0509296e8bd752))
|
|
21
|
+
* **swing-store:** prevent SwingSet usage of imported swing-store ([03f642d](https://github.com/Agoric/agoric-sdk/commit/03f642d39f90ef9465a439723c3a69beef73bd61))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **swing-store:** ensure crank savepoint is wrapped in transaction ([8d738c6](https://github.com/Agoric/agoric-sdk/commit/8d738c65ed37b9159e94fbcf291ed7fe8478ee5a))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
### [0.9.2-u11wf.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.2-u11.0...@agoric/swing-store@0.9.2-u11wf.0) (2023-09-23)
|
|
7
31
|
|
|
8
32
|
**Note:** Version bump only for package @agoric/swing-store
|
package/docs/data-export.md
CHANGED
|
@@ -125,21 +125,9 @@ Then, on the few occasions when the application needs to build a full state-sync
|
|
|
125
125
|
|
|
126
126
|
## Import
|
|
127
127
|
|
|
128
|
-
On other end of the export process is an importer. This is a new host application
|
|
128
|
+
On the other end of the export process is an importer. This is used to restore kernel state, so that a new host application can simply continue mostly as if it had been previously executing. The expectation is that the import and the execution are 2 independent events, and the execution doesn't need to be aware it was imported.
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
```js
|
|
133
|
-
// this is done only the first time an instance is created:
|
|
134
|
-
|
|
135
|
-
import { openSwingStore } from '@agoric/swing-store';
|
|
136
|
-
import { initializeSwingset } from '@agoric/swingset-vat';
|
|
137
|
-
const dirPath = './swing-store';
|
|
138
|
-
const { hostStorage, kernelStorage } = openSwingStore(dirPath);
|
|
139
|
-
await initializeSwingset(config, argv, kernelStorage);
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Once the initial state is created, each time the application is launched, it will build a controller around the existing state:
|
|
130
|
+
For reference, after the initial state is created, each time the application is launched, it builds a controller around the existing state:
|
|
143
131
|
|
|
144
132
|
```js
|
|
145
133
|
import { openSwingStore } from '@agoric/swing-store';
|
|
@@ -150,7 +138,7 @@ const controller = await makeSwingsetController(kernelStorage);
|
|
|
150
138
|
// ... now do things like controller.run(), etc
|
|
151
139
|
```
|
|
152
140
|
|
|
153
|
-
When cloning an existing kernel, the
|
|
141
|
+
When cloning an existing kernel, the host application first imports and commits the restored state using `importSwingStore`. The host application should feed the importer with the export data and artifacts, by passing an object that has the same API as the SwingStore's exporter:
|
|
154
142
|
|
|
155
143
|
```js
|
|
156
144
|
import { importSwingStore } from '@agoric/swing-store';
|
|
@@ -161,11 +149,13 @@ const exporter = {
|
|
|
161
149
|
getArtifact(name) { // return blob of artifact data },
|
|
162
150
|
};
|
|
163
151
|
const { hostStorage } = importSwingStore(exporter, dirPath);
|
|
164
|
-
hostStorage
|
|
165
|
-
|
|
152
|
+
// Update any hostStorage as needed
|
|
153
|
+
await hostStorage.commit();
|
|
154
|
+
await hostStorage.close();
|
|
155
|
+
// now the populated swingstore can be re-opened using `openSwingStore``
|
|
166
156
|
```
|
|
167
157
|
|
|
168
|
-
Once the new SwingStore is fully populated with the previously-exported data, the host application can
|
|
158
|
+
Once the new SwingStore is fully populated with the previously-exported data, the host application can update any host specific state before committing and closing the SwingStore. `importSwingStore` returns only the host facet of the SwingStore instance, as it is not suitable for immediate execution.
|
|
169
159
|
|
|
170
160
|
## Optional / Historical Data
|
|
171
161
|
|
|
@@ -196,14 +186,14 @@ Also note that when a vat is terminated, we delete all information about it, inc
|
|
|
196
186
|
|
|
197
187
|
When importing, the `importSwingStore()` function's options bag takes a property named `artifactMode`, with the same meanings as for export. Importing with the `operational` mode will ignore any artifacts other than those needed for current operations, and will fail unless all such artifacts were available. Importing with `replay` will ignore spans from old incarnations, but will fail unless all spans from current incarnations are present. Importing with `archival` will fail unless all spans from all incarnations are present. There is no `debug` option during import.
|
|
198
188
|
|
|
199
|
-
`importSwingStore()`
|
|
189
|
+
While `importSwingStore()`'s options bag accepts the same options as `openSwingStore()`, since it returns only the host facet of a SwingStore, some of these options might not be meaningful, such as `keepTranscripts`.
|
|
200
190
|
|
|
201
191
|
So, to avoid pruning current-incarnation historical transcript spans when exporting from one swingstore to another, you must set (or avoid overriding) the following options along the way:
|
|
202
192
|
|
|
203
193
|
* the original swingstore must not be opened with `{ keepTranscripts: false }`, otherwise the old spans will be pruned immediately
|
|
204
194
|
* the export must use `makeSwingStoreExporter(dirpath, { artifactMode: 'replay'})`, otherwise the export will omit the old spans
|
|
205
195
|
* the import must use `importSwingStore(exporter, dirPath, { artifactMode: 'replay'})`, otherwise the import will ignore the old spans
|
|
206
|
-
*
|
|
196
|
+
* subsequent `openSwingStore` calls must not use `keepTranscripts: false`, otherwise the new swingstore will prune historical spans as new ones are created (during `rolloverSpan`).
|
|
207
197
|
|
|
208
198
|
## Implementation Details
|
|
209
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/swing-store",
|
|
3
|
-
"version": "0.9.2-
|
|
3
|
+
"version": "0.9.2-u13.0",
|
|
4
4
|
"description": "Persistent storage for SwingSet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@agoric/assert": "^0.6.1-u11wf.0",
|
|
25
|
-
"@agoric/internal": "^0.
|
|
25
|
+
"@agoric/internal": "^0.4.0-u13.0",
|
|
26
26
|
"@endo/base64": "0.2.31",
|
|
27
27
|
"@endo/bundle-source": "2.5.2-upstream-rollup",
|
|
28
28
|
"@endo/check-bundle": "0.2.18",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
],
|
|
46
46
|
"timeout": "2m"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "5a6cdeb0c18ae9700d706445acf402f8d1e873c3"
|
|
49
49
|
}
|
package/src/exporter.js
CHANGED
|
@@ -36,6 +36,11 @@ import { validateArtifactMode } from './internal.js';
|
|
|
36
36
|
* the concurrent activity of other swingStore instances, the data representing
|
|
37
37
|
* the commit point will stay consistent and available.
|
|
38
38
|
*
|
|
39
|
+
* @property {(key: string) => string | undefined} getHostKV
|
|
40
|
+
*
|
|
41
|
+
* Retrieve a value from the "host" portion of the kvStore, just like
|
|
42
|
+
* hostStorage.hostKVStore.get() would do.
|
|
43
|
+
*
|
|
39
44
|
* @property {() => AnyIterableIterator<KVPair>} getExportData
|
|
40
45
|
*
|
|
41
46
|
* Get a full copy of the first-stage export data (key-value pairs) from the
|
|
@@ -112,6 +117,33 @@ export function makeSwingStoreExporter(dirPath, options = {}) {
|
|
|
112
117
|
assertComplete(internal, artifactMode);
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
const sqlKVGet = db.prepare(`
|
|
121
|
+
SELECT value
|
|
122
|
+
FROM kvStore
|
|
123
|
+
WHERE key = ?
|
|
124
|
+
`);
|
|
125
|
+
sqlKVGet.pluck(true);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Obtain the value stored for a given host key. This is for the
|
|
129
|
+
* benefit of clients who need to briefly query the DB to ensure
|
|
130
|
+
* they are exporting the right thing, and need to avoid modifying
|
|
131
|
+
* anything (or creating a read-write DB lock) in the process.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} key The key whose value is sought.
|
|
134
|
+
*
|
|
135
|
+
* @returns {string | undefined} the (string) value for the given key, or
|
|
136
|
+
* undefined if there is no such value.
|
|
137
|
+
*
|
|
138
|
+
* @throws if key is not a string, or the key is not in the host
|
|
139
|
+
* section
|
|
140
|
+
*/
|
|
141
|
+
function getHostKV(key) {
|
|
142
|
+
typeof key === 'string' || Fail`key must be a string`;
|
|
143
|
+
getKeyType(key) === 'host' || Fail`getHostKV requires host keys`;
|
|
144
|
+
return sqlKVGet.get(key);
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
const sqlGetAllKVData = db.prepare(`
|
|
116
148
|
SELECT key, value
|
|
117
149
|
FROM kvStore
|
|
@@ -173,6 +205,7 @@ export function makeSwingStoreExporter(dirPath, options = {}) {
|
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
return harden({
|
|
208
|
+
getHostKV,
|
|
176
209
|
getExportData,
|
|
177
210
|
getArtifactNames,
|
|
178
211
|
getArtifact,
|
package/src/importer.js
CHANGED
|
@@ -11,14 +11,16 @@ import { assertComplete } from './assertComplete.js';
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Function used to
|
|
14
|
+
* Function used to populate a swingStore from an object implementing the
|
|
15
15
|
* exporter API. The exporter API may be provided by a swingStore instance, or
|
|
16
|
-
* implemented by a host to restore data that was previously exported.
|
|
16
|
+
* implemented by a host to restore data that was previously exported. The
|
|
17
|
+
* returned swingStore is not suitable for execution, and thus only contains
|
|
18
|
+
* the host facet for committing the populated swingStore.
|
|
17
19
|
*
|
|
18
20
|
* @param {import('./exporter').SwingStoreExporter} exporter
|
|
19
21
|
* @param {string | null} [dirPath]
|
|
20
22
|
* @param {ImportSwingStoreOptions} [options]
|
|
21
|
-
* @returns {Promise<import('./swingStore').SwingStore
|
|
23
|
+
* @returns {Promise<Pick<import('./swingStore').SwingStore, 'hostStorage' | 'debug'>>}
|
|
22
24
|
*/
|
|
23
25
|
export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
24
26
|
if (dirPath && typeof dirPath !== 'string') {
|
|
@@ -27,8 +29,14 @@ export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
|
27
29
|
const { artifactMode = 'operational', ...makeSwingStoreOptions } = options;
|
|
28
30
|
validateArtifactMode(artifactMode);
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
const { hostStorage, kernelStorage, internal, debug } = makeSwingStore(
|
|
33
|
+
dirPath,
|
|
34
|
+
true,
|
|
35
|
+
{
|
|
36
|
+
unsafeFastMode: true,
|
|
37
|
+
...makeSwingStoreOptions,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
32
40
|
|
|
33
41
|
// For every exportData entry, we add a DB record. 'kv' entries are
|
|
34
42
|
// the "kvStore shadow table", and are not associated with any
|
|
@@ -121,5 +129,5 @@ export async function importSwingStore(exporter, dirPath = null, options = {}) {
|
|
|
121
129
|
assertComplete(internal, checkMode);
|
|
122
130
|
|
|
123
131
|
await exporter.close();
|
|
124
|
-
return
|
|
132
|
+
return { hostStorage, debug };
|
|
125
133
|
}
|
package/src/swingStore.js
CHANGED
|
@@ -203,9 +203,29 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
203
203
|
// mode that defers merge work for a later attempt rather than block any
|
|
204
204
|
// potential readers or writers. See https://sqlite.org/wal.html for details.
|
|
205
205
|
|
|
206
|
+
// However we also allow opening the DB with journaling off, which is unsafe
|
|
207
|
+
// and doesn't support rollback, but avoids any overhead for large
|
|
208
|
+
// transactions like for during an import.
|
|
209
|
+
|
|
210
|
+
function setUnsafeFastMode(enabled) {
|
|
211
|
+
const journalMode = enabled ? 'off' : 'wal';
|
|
212
|
+
const synchronousMode = enabled ? 'normal' : 'full';
|
|
213
|
+
!db.inTransaction || Fail`must not be in a transaction`;
|
|
214
|
+
|
|
215
|
+
db.unsafeMode(!!enabled);
|
|
216
|
+
// The WAL mode is persistent so it's not possible to switch to a different
|
|
217
|
+
// mode for an existing DB.
|
|
218
|
+
const actualMode = db.pragma(`journal_mode=${journalMode}`, {
|
|
219
|
+
simple: true,
|
|
220
|
+
});
|
|
221
|
+
actualMode === journalMode ||
|
|
222
|
+
filePath === ':memory:' ||
|
|
223
|
+
Fail`Couldn't set swing-store DB to ${journalMode} mode (is ${actualMode})`;
|
|
224
|
+
db.pragma(`synchronous=${synchronousMode}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
206
227
|
// PRAGMAs have to happen outside a transaction
|
|
207
|
-
|
|
208
|
-
db.exec(`PRAGMA synchronous=FULL`);
|
|
228
|
+
setUnsafeFastMode(options.unsafeFastMode);
|
|
209
229
|
|
|
210
230
|
// We use IMMEDIATE because the kernel is supposed to be the sole writer of
|
|
211
231
|
// the DB, and if some other process is holding a write lock, we want to find
|
|
@@ -370,6 +390,10 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
370
390
|
inCrank || Fail`establishCrankSavepoint outside of crank`;
|
|
371
391
|
const savepointOrdinal = savepoints.length;
|
|
372
392
|
savepoints.push(savepoint);
|
|
393
|
+
// We must be in a transaction when creating the savepoint or releasing it
|
|
394
|
+
// later will cause an autocommit.
|
|
395
|
+
// See https://github.com/Agoric/agoric-sdk/issues/8423
|
|
396
|
+
ensureTxn();
|
|
373
397
|
const sql = db.prepare(`SAVEPOINT t${savepointOrdinal}`);
|
|
374
398
|
sql.run();
|
|
375
399
|
}
|
|
@@ -477,7 +501,11 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
477
501
|
}
|
|
478
502
|
|
|
479
503
|
/** @type {import('./internal.js').SwingStoreInternal} */
|
|
480
|
-
const internal = harden({
|
|
504
|
+
const internal = harden({
|
|
505
|
+
snapStore,
|
|
506
|
+
transcriptStore,
|
|
507
|
+
bundleStore,
|
|
508
|
+
});
|
|
481
509
|
|
|
482
510
|
async function repairMetadata(exporter) {
|
|
483
511
|
return doRepairMetadata(internal, exporter);
|
package/test/test-bundles.js
CHANGED
|
@@ -107,6 +107,9 @@ test('b0 import', async t => {
|
|
|
107
107
|
const idA = makeB0ID(b0A);
|
|
108
108
|
const nameA = `bundle.${idA}`;
|
|
109
109
|
const exporter = {
|
|
110
|
+
getHostKV(_key) {
|
|
111
|
+
return undefined;
|
|
112
|
+
},
|
|
110
113
|
async *getExportData() {
|
|
111
114
|
yield /** @type {const} */ ([nameA, idA]);
|
|
112
115
|
},
|
|
@@ -119,7 +122,11 @@ test('b0 import', async t => {
|
|
|
119
122
|
},
|
|
120
123
|
close: async () => undefined,
|
|
121
124
|
};
|
|
122
|
-
const
|
|
125
|
+
const ss = await importSwingStore(exporter);
|
|
126
|
+
t.teardown(ss.hostStorage.close);
|
|
127
|
+
await ss.hostStorage.commit();
|
|
128
|
+
const serialized = ss.debug.serialize();
|
|
129
|
+
const { kernelStorage } = initSwingStore(null, { serialized });
|
|
123
130
|
const { bundleStore } = kernelStorage;
|
|
124
131
|
t.truthy(bundleStore.hasBundle(idA));
|
|
125
132
|
t.deepEqual(bundleStore.getBundle(idA), b0A);
|
|
@@ -131,6 +138,9 @@ test('b0 bad import', async t => {
|
|
|
131
138
|
const idA = makeB0ID(b0A);
|
|
132
139
|
const nameA = `bundle.${idA}`;
|
|
133
140
|
const exporter = {
|
|
141
|
+
getHostKV(_key) {
|
|
142
|
+
return undefined;
|
|
143
|
+
},
|
|
134
144
|
async *getExportData() {
|
|
135
145
|
yield /** @type {const} */ ([nameA, idA]);
|
|
136
146
|
},
|
package/test/test-export.js
CHANGED
|
@@ -36,6 +36,8 @@ const exportTest = test.macro(async (t, mode) => {
|
|
|
36
36
|
const ss1 = initSwingStore(dbDir, options);
|
|
37
37
|
const ks = ss1.kernelStorage;
|
|
38
38
|
|
|
39
|
+
ss1.hostStorage.kvStore.set('host.h1', 'hostvalue1');
|
|
40
|
+
|
|
39
41
|
// build a DB with four spans (one in an old incarnation, two
|
|
40
42
|
// historical but current incarnation, only one inUse) and two
|
|
41
43
|
// snapshots (only one inUSe)
|
|
@@ -88,6 +90,13 @@ const exportTest = test.macro(async (t, mode) => {
|
|
|
88
90
|
}
|
|
89
91
|
const exporter = makeSwingStoreExporter(dbDir, { artifactMode });
|
|
90
92
|
|
|
93
|
+
// hostKV
|
|
94
|
+
t.is(exporter.getHostKV('host.h1'), 'hostvalue1');
|
|
95
|
+
t.is(exporter.getHostKV('host.hmissing'), undefined);
|
|
96
|
+
t.throws(() => exporter.getHostKV('nonhost'), {
|
|
97
|
+
message: 'getHostKV requires host keys',
|
|
98
|
+
});
|
|
99
|
+
|
|
91
100
|
// exportData
|
|
92
101
|
{
|
|
93
102
|
const exportData = new Map();
|
package/test/test-import.js
CHANGED
|
@@ -54,6 +54,9 @@ function convert(orig) {
|
|
|
54
54
|
*/
|
|
55
55
|
export function makeExporter(exportData, artifacts) {
|
|
56
56
|
return {
|
|
57
|
+
getHostKV(_key) {
|
|
58
|
+
return undefined;
|
|
59
|
+
},
|
|
57
60
|
async *getExportData() {
|
|
58
61
|
for (const [key, value] of exportData.entries()) {
|
|
59
62
|
/** @type { KVPair } */
|
|
@@ -81,6 +84,7 @@ test('import empty', async t => {
|
|
|
81
84
|
t.teardown(cleanup);
|
|
82
85
|
const exporter = makeExporter(new Map(), new Map());
|
|
83
86
|
const ss = await importSwingStore(exporter, dbDir);
|
|
87
|
+
t.teardown(ss.hostStorage.close);
|
|
84
88
|
await ss.hostStorage.commit();
|
|
85
89
|
const data = convert(ss.debug.dump());
|
|
86
90
|
t.deepEqual(data, {
|
|
@@ -164,6 +168,7 @@ const importTest = test.macro(async (t, mode) => {
|
|
|
164
168
|
|
|
165
169
|
// now import
|
|
166
170
|
const ss = await importSwingStore(exporter, dbDir, { artifactMode });
|
|
171
|
+
t.teardown(ss.hostStorage.close);
|
|
167
172
|
await ss.hostStorage.commit();
|
|
168
173
|
const data = convert(ss.debug.dump());
|
|
169
174
|
|
|
@@ -6,7 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import test from 'ava';
|
|
7
7
|
import sqlite3 from 'better-sqlite3';
|
|
8
8
|
|
|
9
|
-
import { importSwingStore } from '../src/index.js';
|
|
9
|
+
import { importSwingStore, openSwingStore } from '../src/index.js';
|
|
10
10
|
|
|
11
11
|
import { makeExporter, buildData } from './test-import.js';
|
|
12
12
|
import { tmpDir } from './util.js';
|
|
@@ -21,8 +21,9 @@ test('repair metadata', async t => {
|
|
|
21
21
|
// then manually deleting the historical metadata entries from the
|
|
22
22
|
// DB
|
|
23
23
|
const exporter = makeExporter(exportData, artifacts);
|
|
24
|
-
const
|
|
25
|
-
await
|
|
24
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
25
|
+
await ssi.hostStorage.commit();
|
|
26
|
+
await ssi.hostStorage.close();
|
|
26
27
|
|
|
27
28
|
const filePath = path.join(dbDir, 'swingstore.sqlite');
|
|
28
29
|
const db = sqlite3(filePath);
|
|
@@ -53,6 +54,8 @@ test('repair metadata', async t => {
|
|
|
53
54
|
t.deepEqual(ss2, [7]);
|
|
54
55
|
|
|
55
56
|
// now fix it
|
|
57
|
+
const ss = openSwingStore(dbDir);
|
|
58
|
+
t.teardown(ss.hostStorage.close);
|
|
56
59
|
await ss.hostStorage.repairMetadata(exporter);
|
|
57
60
|
await ss.hostStorage.commit();
|
|
58
61
|
|
|
@@ -64,6 +67,7 @@ test('repair metadata', async t => {
|
|
|
64
67
|
|
|
65
68
|
// repair should be idempotent
|
|
66
69
|
await ss.hostStorage.repairMetadata(exporter);
|
|
70
|
+
await ss.hostStorage.commit();
|
|
67
71
|
|
|
68
72
|
const ts4 = getTS.all('v1');
|
|
69
73
|
t.deepEqual(ts4, [0, 2, 5, 8]); // still there
|
|
@@ -78,11 +82,15 @@ test('repair metadata ignores kvStore entries', async t => {
|
|
|
78
82
|
const { exportData, artifacts } = buildData();
|
|
79
83
|
|
|
80
84
|
const exporter = makeExporter(exportData, artifacts);
|
|
81
|
-
const
|
|
82
|
-
await
|
|
85
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
86
|
+
await ssi.hostStorage.commit();
|
|
87
|
+
await ssi.hostStorage.close();
|
|
83
88
|
|
|
84
89
|
// perform the repair with spurious kv entries
|
|
85
90
|
exportData.set('kv.key2', 'value2');
|
|
91
|
+
|
|
92
|
+
const ss = openSwingStore(dbDir);
|
|
93
|
+
t.teardown(ss.hostStorage.close);
|
|
86
94
|
await ss.hostStorage.repairMetadata(exporter);
|
|
87
95
|
await ss.hostStorage.commit();
|
|
88
96
|
|
|
@@ -97,14 +105,17 @@ test('repair metadata rejects mismatched snapshot entries', async t => {
|
|
|
97
105
|
const { exportData, artifacts } = buildData();
|
|
98
106
|
|
|
99
107
|
const exporter = makeExporter(exportData, artifacts);
|
|
100
|
-
const
|
|
101
|
-
await
|
|
108
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
109
|
+
await ssi.hostStorage.commit();
|
|
110
|
+
await ssi.hostStorage.close();
|
|
102
111
|
|
|
103
112
|
// perform the repair with mismatched snapshot entry
|
|
104
113
|
const old = JSON.parse(exportData.get('snapshot.v1.4'));
|
|
105
114
|
const wrong = { ...old, hash: 'wrong' };
|
|
106
115
|
exportData.set('snapshot.v1.4', JSON.stringify(wrong));
|
|
107
116
|
|
|
117
|
+
const ss = openSwingStore(dbDir);
|
|
118
|
+
t.teardown(ss.hostStorage.close);
|
|
108
119
|
await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
|
|
109
120
|
message: /repairSnapshotRecord metadata mismatch/,
|
|
110
121
|
});
|
|
@@ -117,14 +128,17 @@ test('repair metadata rejects mismatched transcript span', async t => {
|
|
|
117
128
|
const { exportData, artifacts } = buildData();
|
|
118
129
|
|
|
119
130
|
const exporter = makeExporter(exportData, artifacts);
|
|
120
|
-
const
|
|
121
|
-
await
|
|
131
|
+
const ssi = await importSwingStore(exporter, dbDir);
|
|
132
|
+
await ssi.hostStorage.commit();
|
|
133
|
+
await ssi.hostStorage.close();
|
|
122
134
|
|
|
123
135
|
// perform the repair with mismatched transcript span entry
|
|
124
136
|
const old = JSON.parse(exportData.get('transcript.v1.0'));
|
|
125
137
|
const wrong = { ...old, hash: 'wrong' };
|
|
126
138
|
exportData.set('transcript.v1.0', JSON.stringify(wrong));
|
|
127
139
|
|
|
140
|
+
const ss = openSwingStore(dbDir);
|
|
141
|
+
t.teardown(ss.hostStorage.close);
|
|
128
142
|
await t.throwsAsync(async () => ss.hostStorage.repairMetadata(exporter), {
|
|
129
143
|
message: /repairTranscriptSpanRecord metadata mismatch/,
|
|
130
144
|
});
|
package/test/test-state.js
CHANGED
|
@@ -316,3 +316,37 @@ test('close will abort transaction', async t => {
|
|
|
316
316
|
t.is(kvStore.get('key2'), undefined);
|
|
317
317
|
t.falsy(kvStore.has('key2'));
|
|
318
318
|
});
|
|
319
|
+
|
|
320
|
+
test('savepoints', async t => {
|
|
321
|
+
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
322
|
+
t.teardown(cleanup);
|
|
323
|
+
const ss1 = initSwingStore(dbDir);
|
|
324
|
+
ss1.kernelStorage.startCrank();
|
|
325
|
+
ss1.kernelStorage.kvStore.set('key', 'value1');
|
|
326
|
+
ss1.kernelStorage.establishCrankSavepoint('sp1');
|
|
327
|
+
ss1.kernelStorage.kvStore.set('key', 'value2');
|
|
328
|
+
ss1.kernelStorage.establishCrankSavepoint('sp2');
|
|
329
|
+
ss1.kernelStorage.kvStore.set('key', 'value3');
|
|
330
|
+
ss1.kernelStorage.rollbackCrank('sp1');
|
|
331
|
+
ss1.kernelStorage.endCrank();
|
|
332
|
+
await ss1.hostStorage.commit();
|
|
333
|
+
await ss1.hostStorage.close();
|
|
334
|
+
|
|
335
|
+
const ss2 = openSwingStore(dbDir);
|
|
336
|
+
t.is(ss2.kernelStorage.kvStore.get('key'), 'value1');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('savepoints do not automatically commit', async t => {
|
|
340
|
+
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
341
|
+
t.teardown(cleanup);
|
|
342
|
+
const ss1 = initSwingStore(dbDir);
|
|
343
|
+
ss1.kernelStorage.startCrank();
|
|
344
|
+
ss1.kernelStorage.establishCrankSavepoint('sp1');
|
|
345
|
+
ss1.kernelStorage.kvStore.set('key', 'value1');
|
|
346
|
+
// #8423 meant this .endCrank() accidentally did a commit()
|
|
347
|
+
ss1.kernelStorage.endCrank();
|
|
348
|
+
await ss1.hostStorage.close();
|
|
349
|
+
|
|
350
|
+
const ss2 = openSwingStore(dbDir);
|
|
351
|
+
t.false(ss2.kernelStorage.kvStore.has('key'));
|
|
352
|
+
});
|