@agoric/swing-store 0.9.2-u11wf.0 → 0.9.2-u12.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,21 @@
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-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)
7
+
8
+
9
+ ### Features
10
+
11
+ * **swing-store:** faster import of swing-store ([35aef87](https://github.com/Agoric/agoric-sdk/commit/35aef87ec0f10b7f0cdce462ac0509296e8bd752))
12
+ * **swing-store:** prevent SwingSet usage of imported swing-store ([03f642d](https://github.com/Agoric/agoric-sdk/commit/03f642d39f90ef9465a439723c3a69beef73bd61))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **swing-store:** ensure crank savepoint is wrapped in transaction ([8d738c6](https://github.com/Agoric/agoric-sdk/commit/8d738c65ed37b9159e94fbcf291ed7fe8478ee5a))
18
+
19
+
20
+
6
21
  ### [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
22
 
8
23
  **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-u12.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-u12.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": "ee5a5fdad9187a6b1b7b26b772b6564c0db3062e"
49
49
  }
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);
@@ -119,7 +119,11 @@ test('b0 import', async t => {
119
119
  },
120
120
  close: async () => undefined,
121
121
  };
122
- const { kernelStorage } = await importSwingStore(exporter);
122
+ const ss = await importSwingStore(exporter);
123
+ t.teardown(ss.hostStorage.close);
124
+ await ss.hostStorage.commit();
125
+ const serialized = ss.debug.serialize();
126
+ const { kernelStorage } = initSwingStore(null, { serialized });
123
127
  const { bundleStore } = kernelStorage;
124
128
  t.truthy(bundleStore.hasBundle(idA));
125
129
  t.deepEqual(bundleStore.getBundle(idA), b0A);
@@ -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') {
@@ -81,6 +81,7 @@ test('import empty', async t => {
81
81
  t.teardown(cleanup);
82
82
  const exporter = makeExporter(new Map(), new Map());
83
83
  const ss = await importSwingStore(exporter, dbDir);
84
+ t.teardown(ss.hostStorage.close);
84
85
  await ss.hostStorage.commit();
85
86
  const data = convert(ss.debug.dump());
86
87
  t.deepEqual(data, {
@@ -164,6 +165,7 @@ const importTest = test.macro(async (t, mode) => {
164
165
 
165
166
  // now import
166
167
  const ss = await importSwingStore(exporter, dbDir, { artifactMode });
168
+ t.teardown(ss.hostStorage.close);
167
169
  await ss.hostStorage.commit();
168
170
  const data = convert(ss.debug.dump());
169
171
 
@@ -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
+ });