@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 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
@@ -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, which wants to start from the contents of the export, rather than initializing a brand new (empty) kernel state.
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
- When starting a brand new instance, host applications would normally call `openSwingStore(dirPath)` to create a new (empty) SwingStore, then call SwingSet's `initializeSwingset(config, .., kernelStorage)` to let the kernel initialize the DB with a config-dependent starting state:
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 initialization step is replaced with `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:
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.commit();
165
- // now the swingstore is fully populated
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 use `makeSwingsetController()` to build a kernel that will start from the exported state.
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()` returns a swingstore, which means its options bag also contains the same options as `openSwingStore()`, including the `keepTranscripts` option. This defaults to `true`, but if it were overridden to `false`, then the new swingstore will delete transcript spans as soon as they are no longer needed for operational purposes (e.g. when `transcriptStore.rolloverSpan()` is called).
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
- * the `importSwingStore` call (and all subsequent `openSwingStore` calls) must not use `keepTranscripts: false`, otherwise the new swingstore will prune historical spans as new ones are created (during `rolloverSpan`).
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-u11wf.0",
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.3.3-u11wf.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": "faf9ba6ab8b2b69bf25f435f262f0b5bd2f2bc97"
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 create a new swingStore from an object implementing the
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 store = makeSwingStore(dirPath, true, makeSwingStoreOptions);
31
- const { kernelStorage, internal } = store;
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 store;
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
- db.exec(`PRAGMA journal_mode=WAL`);
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({ snapStore, transcriptStore, bundleStore });
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);
@@ -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 { kernelStorage } = await importSwingStore(exporter);
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
  },
@@ -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();
@@ -328,6 +328,7 @@ async function testExportImport(
328
328
  }
329
329
  t.is(failureMode, 'none');
330
330
  const ssIn = await doImport();
331
+ t.teardown(ssIn.hostStorage.close);
331
332
  await ssIn.hostStorage.commit();
332
333
  let dumpsShouldMatch = true;
333
334
  if (runMode === 'operational') {
@@ -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 ss = await importSwingStore(exporter, dbDir);
25
- await ss.hostStorage.commit();
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 ss = await importSwingStore(exporter, dbDir);
82
- await ss.hostStorage.commit();
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 ss = await importSwingStore(exporter, dbDir);
101
- await ss.hostStorage.commit();
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 ss = await importSwingStore(exporter, dbDir);
121
- await ss.hostStorage.commit();
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
  });
@@ -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
+ });