@agoric/swing-store 0.9.1 → 0.9.2-20230923T000433-dev-63b1fb6.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,29 @@
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-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
+
8
+ **Note:** Version bump only for package @agoric/swing-store
9
+
10
+
11
+
12
+
13
+
14
+ ### [0.9.2-u11.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.1...@agoric/swing-store@0.9.2-u11.0) (2023-08-24)
15
+
16
+
17
+ ### Features
18
+
19
+ * **swingstore:** add repairMetadata() ([5b2d19d](https://github.com/Agoric/agoric-sdk/commit/5b2d19d1153a23c118afb14ca4ed80e175640f62)), closes [#8025](https://github.com/Agoric/agoric-sdk/issues/8025) [#8025](https://github.com/Agoric/agoric-sdk/issues/8025)
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **swing-store:** add 'replay' artifactMode, make export more strict ([d46c8e2](https://github.com/Agoric/agoric-sdk/commit/d46c8e2d270999ccba552e0072cb5e0926922f28)), closes [#8105](https://github.com/Agoric/agoric-sdk/issues/8105)
25
+ * rewrite importSwingStore to preserve metadata properly ([03e323a](https://github.com/Agoric/agoric-sdk/commit/03e323a55ffeb98dcc84a57050a5d3fc881899b8)), closes [#8025](https://github.com/Agoric/agoric-sdk/issues/8025)
26
+
27
+
28
+
6
29
  ### [0.9.1](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.0...@agoric/swing-store@0.9.1) (2023-06-02)
7
30
 
8
31
  **Note:** Version bump only for package @agoric/swing-store
@@ -16,7 +16,7 @@ The SwingStore export protocol defines two stages (effectively two datasets). Th
16
16
 
17
17
  Each time a SwingStore API is used to modify the state somehow (e.g. adding/changing/deleting a `kvStore` entry, or pushing a new item on to a transcript), the contents of both datasets may change. New first-stage entries can be created, existing ones may be modified or deleted. And the set of second-stage artifacts may change.
18
18
 
19
- These export data/artifact changes can happen when calling into the kernel (e.g. invoking the external API of a device, causing the device code to change its own state or push messages onto the run-queue), or by normal kernel operations as it runs (any time `controller.run()` is executing). When the kernel is idle (after `controller.run()` has completed), the kernel will not make any changes to the SwingStore, and both datasets will be stable.
19
+ These export data/artifact changes can happen when calling into the kernel (e.g. invoking the external API of a device, causing the device code to change its own state or push messages onto the run-queue), or by normal kernel operations as it runs (any time `controller.run()` is executing). When the kernel is idle (after `controller.run()` has completed), and `hostStorage.commit()` is called, the kernel will not make any changes to the SwingStore, and both datasets will be stable.
20
20
 
21
21
  Among other things, the SwingStore records a transcript of deliveries for each vat. The collection of all deliveries to a particular vat since its last heap snapshot was written is called the "current span". For each vat, the first-stage export data will record a single record that remembers the extent and the hash of the current span. This record then refers to a second-stage export artifact that contains the actual transcript contents.
22
22
 
@@ -83,6 +83,8 @@ So, to include SwingStore data in this state-sync snapshot, we need a way to get
83
83
 
84
84
  To support this, SwingStore has an "incremental export" mode. This is activated when the host application supplies an "export callback" option to the SwingStore instance constructor. Instead of retrieving the entire first-stage export data at the end of the block, the host application will be continuously notified about changes to this data as the kernel executes. The host application can then incorporate those entries into an existing hashed Merkle tree (e.g. the cosmos-sdk IAVL tree), whose root hash is included in the consensus block hash. Every time the callback is given `(key, value)`, the host should add a new (or modify some existing) IAVL entry, using an IAVL key within some range dedicated to the SwingStore first-stage export data. When the callback receives `(key, undefined)` or `(key, null)`, it should delete the entry. In this way, the IAVL tree maintains a "shadow copy" of the first-stage export data at all times, making the contents both covered by the consensus hash, and automatically included in the cosmos-sdk IAVL tree where it will become available to the new validator as it begins to reconstruct the SwingStore.
85
85
 
86
+ The export callback must be established from the very beginning, so it includes all changes made during kernel initialization.
87
+
86
88
  All validator nodes use this export callback, even if they never perform the rest of the export process, to ensure that the consensus state includes the entire first-stage dataset. (Note that the first stage data is generally smaller than the full dataset, making this relatively inexpensive).
87
89
 
88
90
  Then, on the few occasions when the application needs to build a full state-sync snapshot, it can ask the SwingStore (after block commit) for the full set of artifacts that match the most recent commit.
@@ -177,18 +179,41 @@ As a result, for each active vat, the first-stage Export Data contains a record
177
179
 
178
180
  The `openSwingStore()` function has an option named `keepTranscripts` (which defaults to `true`), which causes the transcriptStore to retain the old transcript items. A second option named `keepSnapshots` (which defaults to `false`) causes the snapStore to retain the old heap snapshots. Opening the swingStore with a `false` option does not necessarily delete the old items immediately, but they'll probably get deleted the next time the kernel triggers a heap snapshot or transcript-span rollover. Validators who care about minimizing their disk usage will want to set both to `false`. In the future, we will arrange the SwingStore SQLite tables to provide easy `sqlite3` CLI commands that will delete the old data, so validators can also periodically use the CLI command to prune it.
179
181
 
180
- The `getArtifactNames()` API includes an option named `includeHistorical`. If `true`, all available historical artifacts will be included in the export (limited by what the `openSwingStore` options have deleted). If `false`, none will be included. Note that the "export data" is necessarily unaffected: if we *ever* want to validate this optional data, the hashes are mandatory. But the `getArtifactNames()` list will be smaller if you set `includeHistorical = false`. Also, re-exporting from a pruned copy will lack the old data, even if the re-export uses `includeHistorical = true`, because the second SwingStore cannot magically reconstruct the missing data.
182
+ When exporting, the `makeSwingStoreExporter()` function takes an `artifactMode` option (in an options bag). This serves to both limit, and provide some minimal guarantees about, the set of artifacts that will be provided in the export. The defined values of `artifactMode` each build upon the previous one:
183
+
184
+ * `operational`: include only the current transcript span and current snapshot for each vat: just the minimum set necessary for current operations
185
+ * `replay`: add all transcript spans for the current incarnation
186
+ * `archival`: add all available transcript spans, even for old incarnations
187
+ * `debug`: add all available snapshots, giving you everything. The old snapshots are never necessary for normal operations, nor are they likely to be useful for extreme upgrade scenarios, but they might be useful for some unusual debugging operations or investigations
188
+
189
+ For each mode, the export will fail if the data necessary for those artifacts is not available (e.g. it was previously pruned). For example, an export with `artifactMode: 'replay'` will fail unless every vat has all transcript entries for each one's current incarnation. The `archival` mode will fail to export unless every vat has *every* transcript entry, back to the very first incarnation.
190
+
191
+ However the `debug` export mode will never fail: it merely dumps everything in the swingstore, without limits or completeness checks.
192
+
193
+ Note that `artifactMode` does not affect the Export Data generated by the exporter (because if we *ever* want to validate this optional data, the hashes are mandatory). It only affects the names returned by `getArtifactNames()`: `operational` returns a subset of `replay`, which returns a subset of `archival`. And re-exporting from a previously-pruned copy under `archival` mode will fail, because the second SwingStore cannot magically reconstruct the missing data.
194
+
195
+ Also note that when a vat is terminated, we delete all information about it, including transcript items and snapshots, both current and old. This will remove all the Export Data records, and well as the matching artifacts from `getArtifactNames`.
196
+
197
+ 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
+
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).
200
+
201
+ 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:
181
202
 
182
- Note that when a vat is terminated, we delete all information about it, including transcript items and snapshots, both current and old. This will remove all the Export Data records, and well as the matching artifacts from `getArtifactNames`.
203
+ * the original swingstore must not be opened with `{ keepTranscripts: false }`, otherwise the old spans will be pruned immediately
204
+ * the export must use `makeSwingStoreExporter(dirpath, { artifactMode: 'replay'})`, otherwise the export will omit the old spans
205
+ * 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`).
183
207
 
184
208
  ## Implementation Details
185
209
 
186
- SwingStore contains components to accomodate all the various kinds of state that the SwingSet kernel needs to store. This currently consists of three portions:
210
+ SwingStore contains components to accommodate all the various kinds of state that the SwingSet kernel needs to store. This currently consists of four portions:
187
211
 
188
212
  * `kvStore`, a general-purpose string/string key-value table
189
213
  * `transcriptStore`: append-only vat deliveries, broken into "spans", delimited by heap snapshot events
190
214
  * `snapshotStore`: binary blobs containing JS engine heap state, to limit transcript replay depth
215
+ * `bundleStore`: code bundles that can be imported with `@endo/import-bundle`
191
216
 
192
- Currently, the SwingStore treats transcript spans and heap snapshots as export artifacts, with hashes recorded in the export data for validation (and to remember exactly which artifacts are necessary). The `kvStore` is copied one-to-one into the export data (i.e. we keep a full shadow copy in IAVL), because that is the fastest way to ensure the `kvStore` data is fully available and validated.
217
+ Currently, the SwingStore treats transcript spans, heap snapshots, and bundles as export artifacts, with hashes recorded in the export data for validation (and to remember exactly which artifacts are necessary). The `kvStore` is copied one-to-one into the export data (i.e. we keep a full shadow copy in IAVL), because that is the fastest way to ensure the `kvStore` data is fully available and validated.
193
218
 
194
219
  If some day we implement an IAVL-like Merkle tree inside SwingStore, and use it to automatically generate a root hash for the `kvStore` at the end of each block, we will replace this (large) shadow copy with a single `kvStoreRootHash` entry, and add a new export artifact to contain the full contents of the kvStore. This reduce the size of the IAVL tree, as well as the rate of IAVL updates during block execution, at the cost of increased CPU and complexity within SwingStore.
@@ -0,0 +1,52 @@
1
+ # SwingStore Data Model
2
+
3
+ The "SwingStore" provides a database to hold SwingSet kernel state, with an API crafted to help both the kernel and the host application mutate, commit, export, and import this state.
4
+
5
+ The state is broken up into several pieces, or "stores":
6
+
7
+ * `bundleStore`: a string-keyed Bundle-value table, holding source bundles which can be evaluated by `importBundle` to create vats, or new Compartments within a vat
8
+ * `transcriptStore`: records a linear sequence of deliveries and syscalls (with results), collectively known as "transcript entries", for each vat
9
+ * `snapStore`: records one or more XS heap snapshots for each vat, to rebuild a worker more efficiently than replaying all transcript entries from the beginning
10
+ * `kvStore`: a string-keyed string-valued table, which holds everything else. Currently, this holds each vat's c-list and vatstore data, as well as the kernel-wide object and promise tables, and run-queues.
11
+
12
+ ## Incarnations, Spans, Snapshots
13
+
14
+ The kernel tracks the state of one or more vats. Each vat's execution is split into "incarnations", which are separated by a "vat upgrade" (a call to `E(vatAdminFacet).upgrade(newBundleCap, options)`, see https://github.com/Agoric/agoric-sdk/blob/master/packages/SwingSet/docs/vat-upgrade.md for details). Each incarnation gets a new worker, which erases the heap state and only retains durable vatstore data across the upgrade. Every active vat has a "current incarnation", and zero or more "historic incarnations". Only the current incarnation is instantiated.
15
+
16
+ Within each incarnation, execution is broken into one or more "spans", with a "current span" and zero or more "historic spans". This breaks up the transcript into corresponding spans.
17
+
18
+ Each historic span ends with a `save-snapshot` entry which records the creation and saving of an XS heap snapshot. The initial span starts with a `start-worker` entry, while all non-initial spans start with a `load-snapshot` entry. The final span of historic incarnations each end with a `shutdown-worker` entry.
19
+
20
+ Each `save-snapshot` entry adds a new snapshot to the `snapStore`, so each vat has zero or more snapshots, of which the last one is called the "current" or "in-use" snapshot, and the earlier ones are called "historical snapshots".
21
+
22
+ (note: the `deliveryNum` counter is scoped to the vat and does not reset at incarnation or span boundaries)
23
+
24
+ ## Artifacts
25
+
26
+ The import/export process (using `makeSwingStoreExporter` and `importSwingStore`) defines some number of "artifacts" to contain much of the SwingStore data. Each bundle is a separate artifact, as is each heap snapshot. Each transcript span is a separate artifact (an aggregate of the individual transcript entries comprising that span).
27
+
28
+ During export, the `getArtifactNames()` method provides a list of all available artifacts, while `getArtifact(name)` is used to retrieve the actual data. The import function processes each artifact separately.
29
+
30
+ ## Populated vs Pruned
31
+
32
+ For normal operation, the kernel does not require historical incarnations, spans, or snapshots. It only needs the ability to reconstruct a worker for the current incarnation of each vat, which means loading the current snapshot (if any), and replaying the contents of the current transcript span.
33
+
34
+ For this reason, the swingstore must always contain the current transcript span, and the current snapshot (if any), for every vat.
35
+
36
+ However, to save space, historical spans/snapshots might be pruned, by deleting their contents from the database (but retaining the metadata, which includes a hash of the contents for later validation). Historical snapshots are pruned by default (unless `openSwingStore()` is given an options bag with `keepSnapshots: true`). Historical spans are not currently pruned (the `keepTranscripts` option defaults to `true`), but that may change.
37
+
38
+ In addition, `importSwingStore()` can be used to create a SwingStore from data exported out of some other SwingStore. The export-then-import process might result in a pruned DB in one of three ways:
39
+
40
+ * the import-time options might instruct the import process to ignore some of the available data
41
+ * the export-time options might have done the same
42
+ * the original DB was itself already pruned, so the data was not available in the first place
43
+
44
+ In the future, a separate SwingStore API will exist to allow previously-pruned artifacts to be repopulated. Every artifact has a metadata record which *is* included in the export (in the `exportData` section, but separate from the kvStore shadow table entries, see [data-export.md](./data-export.md)), regardless of pruning modes, to ensure that this API can check the integrity of these repopulated artifacts. This reduces the reliance set and trust burden of the repopulation process (we can safely use untrusted artifact providers).
45
+
46
+ When a snapshot is pruned, the `snapshots` SQL table row is modified, replacing its `compressedSnapshot` BLOB with a NULL. The other columns are left alone, especially the `hash` column, which retains the integrity-checking metadata to support a future repopulation.
47
+
48
+ When a transcript span is pruned, the `transcriptSpans` row is left alone, but the collection of `transcriptItems` rows are deleted. Any span for which all the `transcriptItems` rows are present is said to be "populated", while any span that is missing one or more `transcriptItems` rows is said to be "pruned". (There is no good reason for a span to be only partially pruned, but until we compress historical spans into a single row, in some new table, there remains the possibility of partial pruning).
49
+
50
+ During import, we create the metadata first (as the export-data is parsed), then later, we fill in the details as the artifacts are read.
51
+
52
+ Bundles are never pruned, however during import, the `bundles` table will temporarily contain rows whose `bundle` BLOB is NULL.
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@agoric/swing-store",
3
- "version": "0.9.1",
3
+ "version": "0.9.2-20230923T000433-dev-63b1fb6.0+63b1fb6",
4
4
  "description": "Persistent storage for SwingSet",
5
5
  "type": "module",
6
- "main": "src/swingStore.js",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
7
10
  "repository": "https://github.com/Agoric/agoric-sdk",
8
11
  "author": "Agoric",
9
12
  "license": "Apache-2.0",
@@ -18,16 +21,16 @@
18
21
  "lint:eslint": "eslint ."
19
22
  },
20
23
  "dependencies": {
21
- "@agoric/assert": "^0.6.0",
22
- "@agoric/internal": "^0.3.2",
23
- "@endo/base64": "^0.2.31",
24
- "@endo/bundle-source": "^2.5.1",
25
- "@endo/check-bundle": "^0.2.18",
26
- "@endo/nat": "^4.1.27",
24
+ "@agoric/assert": "0.6.1-20230923T000433-dev-63b1fb6.0+63b1fb6",
25
+ "@agoric/internal": "0.3.3-20230923T000433-dev-63b1fb6.0+63b1fb6",
26
+ "@endo/base64": "0.2.31",
27
+ "@endo/bundle-source": "2.5.2-upstream-rollup",
28
+ "@endo/check-bundle": "0.2.18",
29
+ "@endo/nat": "4.1.27",
27
30
  "better-sqlite3": "^8.2.0"
28
31
  },
29
32
  "devDependencies": {
30
- "@endo/init": "^0.5.56",
33
+ "@endo/init": "0.5.56",
31
34
  "@types/better-sqlite3": "^7.5.0",
32
35
  "ava": "^5.2.0",
33
36
  "c8": "^7.13.0",
@@ -42,5 +45,5 @@
42
45
  ],
43
46
  "timeout": "2m"
44
47
  },
45
- "gitHead": "b66bf7c881ae462fcb617cd6e8d41b5c40ec58ef"
48
+ "gitHead": "63b1fb6a532473af97b72a7b2215af9bc0d6da5f"
46
49
  }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @param {import('./internal.js').SwingStoreInternal} internal
3
+ * @param {Omit<import('./internal.js').ArtifactMode, 'debug'>} checkMode
4
+ * @returns {void}
5
+ */
6
+ export function assertComplete(internal, checkMode) {
7
+ // every bundle must be populated
8
+ internal.bundleStore.assertComplete(checkMode);
9
+
10
+ // every 'isCurrent' transcript span must have all items
11
+ // TODO: every vat with any data must have a isCurrent transcript
12
+ // span
13
+ internal.transcriptStore.assertComplete(checkMode);
14
+
15
+ // every 'inUse' snapshot must be populated
16
+ internal.snapStore.assertComplete(checkMode);
17
+
18
+ // TODO: every isCurrent span that starts with load-snapshot has a
19
+ // matching snapshot (counter-argument: swing-store should not know
20
+ // those details about transcript entries)
21
+ }
@@ -7,7 +7,6 @@ import { checkBundle } from '@endo/check-bundle/lite.js';
7
7
  import { Nat } from '@endo/nat';
8
8
  import { Fail, q } from '@agoric/assert';
9
9
  import { createSHA256 } from './hasher.js';
10
- import { buffer } from './util.js';
11
10
 
12
11
  /**
13
12
  * @typedef { { moduleFormat: 'getExport', source: string, sourceMap?: string } } GetExportBundle
@@ -16,7 +15,8 @@ import { buffer } from './util.js';
16
15
  * @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle } Bundle
17
16
  */
18
17
  /**
19
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
18
+ * @typedef { import('./exporter').SwingStoreExporter } SwingStoreExporter
19
+ * @typedef { import('./internal.js').ArtifactMode } ArtifactMode
20
20
  *
21
21
  * @typedef {{
22
22
  * addBundle: (bundleID: string, bundle: Bundle) => void;
@@ -27,7 +27,10 @@ import { buffer } from './util.js';
27
27
  *
28
28
  * @typedef {{
29
29
  * exportBundle: (name: string) => AsyncIterableIterator<Uint8Array>,
30
- * importBundle: (artifactName: string, exporter: SwingStoreExporter, bundleID: string) => void,
30
+ * repairBundleRecord: (key: string, value: string) => void,
31
+ * importBundleRecord: (key: string, value: string) => void,
32
+ * importBundle: (name: string, dataProvider: () => Promise<Buffer>) => Promise<void>,
33
+ * assertComplete: (checkMode: Omit<ArtifactMode, 'debug'>) => void,
31
34
  * getExportRecords: () => IterableIterator<readonly [key: string, value: string]>,
32
35
  * getArtifactNames: () => AsyncIterableIterator<string>,
33
36
  * getBundleIDs: () => IterableIterator<string>,
@@ -39,6 +42,18 @@ import { buffer } from './util.js';
39
42
  *
40
43
  */
41
44
 
45
+ function bundleIDFromName(name) {
46
+ typeof name === 'string' || Fail`artifact name must be a string`;
47
+ const [tag, ...pieces] = name.split('.');
48
+ if (tag !== 'bundle' || pieces.length !== 1) {
49
+ Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(
50
+ name,
51
+ )}`;
52
+ }
53
+ const bundleID = pieces[0];
54
+ return bundleID;
55
+ }
56
+
42
57
  /**
43
58
  * @param {*} db
44
59
  * @param {() => void} ensureTxn
@@ -54,6 +69,9 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
54
69
  )
55
70
  `);
56
71
 
72
+ // A populated record contains both bundleID and bundle, while a
73
+ // pruned record has a bundle of NULL.
74
+
57
75
  function bundleArtifactName(bundleID) {
58
76
  return `bundle.${bundleID}`;
59
77
  }
@@ -62,20 +80,36 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
62
80
  return `b${Nat(version)}-${hash}`;
63
81
  }
64
82
 
65
- const sqlAddBundle = db.prepare(`
66
- INSERT OR REPLACE INTO bundles
67
- (bundleID, bundle)
68
- VALUES (?, ?)
83
+ // the PRIMARY KEY constraint requires the bundleID not already
84
+ // exist
85
+ const sqlAddBundleRecord = db.prepare(`
86
+ INSERT INTO bundles (bundleID, bundle) VALUES (?, NULL)
69
87
  `);
70
88
 
71
- /**
72
- * Store a bundle. Here the bundle itself is presumed valid.
73
- *
74
- * @param {string} bundleID
75
- * @param {Bundle} bundle
76
- */
77
- function addBundle(bundleID, bundle) {
89
+ // this sees both populated and pruned (not-yet-populated) records
90
+ const sqlHasBundleRecord = db.prepare(`
91
+ SELECT count(*)
92
+ FROM bundles
93
+ WHERE bundleID = ?
94
+ `);
95
+ sqlHasBundleRecord.pluck();
96
+
97
+ const sqlPopulateBundleRecord = db.prepare(`
98
+ UPDATE bundles SET bundle = $serialized WHERE bundleID = $bundleID
99
+ `);
100
+
101
+ function addBundleRecord(bundleID) {
102
+ ensureTxn();
103
+ sqlAddBundleRecord.run(bundleID);
104
+ }
105
+
106
+ function populateBundle(bundleID, serialized) {
78
107
  ensureTxn();
108
+ sqlHasBundleRecord.get(bundleID) || Fail`missing ${bundleID}`;
109
+ sqlPopulateBundleRecord.run({ bundleID, serialized });
110
+ }
111
+
112
+ function serializeBundle(bundleID, bundle) {
79
113
  const { moduleFormat } = bundle;
80
114
  let serialized;
81
115
  if (bundleID.startsWith('b0-')) {
@@ -98,19 +132,55 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
98
132
  } else {
99
133
  throw Fail`unsupported BundleID ${bundleID}`;
100
134
  }
101
- sqlAddBundle.run(bundleID, serialized);
135
+ return serialized;
136
+ }
137
+
138
+ /**
139
+ * Store a complete bundle in a single operation, used by runtime
140
+ * (i.e. not an import). We rely upon the caller to provide a
141
+ * correct bundle (e.g. no unexpected properties), but we still
142
+ * check the ID against the contents.
143
+ *
144
+ * @param {string} bundleID
145
+ * @param {Bundle} bundle
146
+ */
147
+ function addBundle(bundleID, bundle) {
148
+ const serialized = serializeBundle(bundleID, bundle);
149
+ addBundleRecord(bundleID);
150
+ populateBundle(bundleID, serialized);
102
151
  noteExport(bundleArtifactName(bundleID), bundleID);
103
152
  }
104
153
 
105
- const sqlHasBundle = db.prepare(`
154
+ const sqlGetPrunedBundles = db.prepare(`
155
+ SELECT bundleID
156
+ FROM bundles
157
+ WHERE bundle IS NULL
158
+ ORDER BY bundleID
159
+ `);
160
+ sqlGetPrunedBundles.pluck();
161
+
162
+ function getPrunedBundles() {
163
+ return sqlGetPrunedBundles.all();
164
+ }
165
+
166
+ function assertComplete(checkMode) {
167
+ assert(checkMode !== 'debug', checkMode);
168
+ const pruned = getPrunedBundles();
169
+ if (pruned.length) {
170
+ throw Fail`missing bundles for: ${pruned.join(',')}`;
171
+ }
172
+ }
173
+
174
+ const sqlHasPopulatedBundle = db.prepare(`
106
175
  SELECT count(*)
107
176
  FROM bundles
108
177
  WHERE bundleID = ?
178
+ AND bundle IS NOT NULL
109
179
  `);
110
- sqlHasBundle.pluck(true);
180
+ sqlHasPopulatedBundle.pluck(true);
111
181
 
112
182
  function hasBundle(bundleID) {
113
- const count = sqlHasBundle.get(bundleID);
183
+ const count = sqlHasPopulatedBundle.get(bundleID);
114
184
  return count !== 0;
115
185
  }
116
186
 
@@ -119,15 +189,15 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
119
189
  FROM bundles
120
190
  WHERE bundleID = ?
121
191
  `);
122
- sqlGetBundle.pluck(true);
123
192
 
124
193
  /**
125
194
  * @param {string} bundleID
126
195
  * @returns {Bundle}
127
196
  */
128
197
  function getBundle(bundleID) {
129
- const rawBundle = sqlGetBundle.get(bundleID);
130
- rawBundle || Fail`bundle ${q(bundleID)} not found`;
198
+ const row =
199
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
200
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
131
201
  if (bundleID.startsWith('b0-')) {
132
202
  return harden(JSON.parse(rawBundle));
133
203
  } else if (bundleID.startsWith('b1-')) {
@@ -153,6 +223,31 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
153
223
  }
154
224
  }
155
225
 
226
+ // take an export-data record (id/hash but not bundle contents) and
227
+ // insert something in the DB
228
+ function importBundleRecord(key, value) {
229
+ const bundleID = bundleIDFromName(key);
230
+ assert.equal(bundleID, value);
231
+ addBundleRecord(bundleID);
232
+ }
233
+
234
+ function repairBundleRecord(key, value) {
235
+ // Bundle records have no metadata, and all bundles must be
236
+ // present (there's no notion of "historical bundle"). So there's
237
+ // no "repair", and if the repair process supplies a bundle record
238
+ // that isn't already present, we throw an error. The repair
239
+ // process doesn't get artifacts, so adding a new record here
240
+ // would fail the subsequent completeness check anyways.
241
+
242
+ const bundleID = bundleIDFromName(key);
243
+ assert.equal(bundleID, value);
244
+ if (sqlHasBundleRecord.get(bundleID)) {
245
+ // record is present, there's no metadata to mismatch, so ignore quietly
246
+ return;
247
+ }
248
+ throw Fail`unexpected new bundle record for ${bundleID} during repair`;
249
+ }
250
+
156
251
  /**
157
252
  * Read a bundle and return it as a stream of data suitable for export to
158
253
  * another store.
@@ -166,14 +261,10 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
166
261
  * @returns {AsyncIterableIterator<Uint8Array>}
167
262
  */
168
263
  async function* exportBundle(name) {
169
- typeof name === 'string' || Fail`artifact name must be a string`;
170
- const parts = name.split('.');
171
- const [type, bundleID] = parts;
172
- // prettier-ignore
173
- (parts.length === 2 && type === 'bundle') ||
174
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(name)}`;
175
- const rawBundle = sqlGetBundle.get(bundleID);
176
- rawBundle || Fail`bundle ${q(name)} not available`;
264
+ const bundleID = bundleIDFromName(name);
265
+ const row =
266
+ sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
267
+ const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
177
268
  yield* Readable.from(Buffer.from(rawBundle));
178
269
  }
179
270
 
@@ -209,23 +300,17 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
209
300
  }
210
301
 
211
302
  /**
212
- * @param {string} name Artifact name of the bundle
213
- * @param {SwingStoreExporter} exporter Whence to get the bits
214
- * @param {string} bundleID Bundle ID of the bundle
303
+ * Call addBundleRecord() first, then this importBundle() will
304
+ * populate the record.
305
+ *
306
+ * @param {string} name Artifact name, `bundle.${bundleID}`
307
+ * @param {() => Promise<Buffer>} dataProvider Function to get bundle bytes
215
308
  * @returns {Promise<void>}
216
309
  */
217
- async function importBundle(name, exporter, bundleID) {
310
+ async function importBundle(name, dataProvider) {
218
311
  await 0; // no synchronous prefix
219
- const parts = name.split('.');
220
- const [type, bundleIDkey] = parts;
221
- // prettier-ignore
222
- parts.length === 2 && type === 'bundle' ||
223
- Fail`expected artifact name of the form 'bundle.{bundleID}', saw '${q(name)}'`;
224
- bundleIDkey === bundleID ||
225
- Fail`bundle artifact name ${name} doesn't match bundleID ${bundleID}`;
226
- const artifactChunks = exporter.getArtifact(name);
227
- const inStream = Readable.from(artifactChunks);
228
- const data = await buffer(inStream);
312
+ const bundleID = bundleIDFromName(name);
313
+ const data = await dataProvider();
229
314
  if (bundleID.startsWith('b0-')) {
230
315
  // we dissect and reassemble the bundle, to exclude unexpected properties
231
316
  const { moduleFormat, source, sourceMap } = JSON.parse(data.toString());
@@ -234,7 +319,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
234
319
  const serialized = JSON.stringify(bundle);
235
320
  bundleID === bundleIdFromHash(0, createSHA256(serialized).finish()) ||
236
321
  Fail`bundleID ${q(bundleID)} does not match bundle artifact`;
237
- addBundle(bundleID, bundle);
322
+ populateBundle(bundleID, serialized);
238
323
  } else if (bundleID.startsWith('b1-')) {
239
324
  /** @type {EndoZipBase64Bundle} */
240
325
  const bundle = harden({
@@ -245,7 +330,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
245
330
  // Assert that the bundle contents match the ID and hash
246
331
  // eslint-disable-next-line @jessie.js/no-nested-await
247
332
  await checkBundle(bundle, computeSha512, bundleID);
248
- addBundle(bundleID, bundle);
333
+ populateBundle(bundleID, serializeBundle(bundleID, bundle));
249
334
  } else {
250
335
  Fail`unsupported BundleID ${q(bundleID)}`;
251
336
  }
@@ -265,7 +350,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
265
350
  const dump = {};
266
351
  for (const row of sql.iterate()) {
267
352
  const { bundleID, bundle } = row;
268
- dump[bundleID] = encodeBase64(bundle);
353
+ dump[bundleID] = encodeBase64(Buffer.from(bundle, 'utf-8'));
269
354
  }
270
355
  return dump;
271
356
  }
@@ -282,15 +367,20 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
282
367
  }
283
368
 
284
369
  return harden({
370
+ importBundleRecord,
371
+ importBundle,
372
+ assertComplete,
373
+
285
374
  addBundle,
286
375
  hasBundle,
287
376
  getBundle,
288
377
  deleteBundle,
378
+
289
379
  getExportRecords,
290
380
  getArtifactNames,
291
381
  exportBundle,
292
- importBundle,
293
382
  getBundleIDs,
383
+ repairBundleRecord,
294
384
 
295
385
  dumpBundles,
296
386
  });