@agoric/swing-store 0.9.2-dev-2f092c3.0 → 0.9.2-dev-aa10ecd.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/data-export.md +20 -4
- package/docs/swingstore.md +52 -0
- package/package.json +8 -5
- package/src/assertComplete.js +22 -0
- package/src/bundleStore.js +136 -47
- package/src/exporter.js +175 -0
- package/src/importer.js +121 -0
- package/src/index.js +11 -0
- package/src/internal.js +14 -0
- package/src/kvStore.js +172 -0
- package/src/repairMetadata.js +65 -0
- package/src/snapStore.js +160 -33
- package/src/snapStoreIO.js +8 -0
- package/src/swingStore.js +52 -587
- package/src/transcriptStore.js +170 -37
- package/src/types.d.ts +14 -0
- package/src/types.js +6 -0
- package/src/util.js +7 -1
- package/test/test-bundles.js +9 -7
- package/test/test-export.js +308 -0
- package/test/test-exportImport.js +36 -54
- package/test/test-import.js +476 -0
- package/test/test-repair-metadata.js +131 -0
- package/test/util.js +26 -0
package/docs/data-export.md
CHANGED
|
@@ -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,32 @@ 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
|
-
|
|
182
|
+
When exporting, the `makeSwingStoreExporter()` function takes an `exportMode=` argument. This serves to limit the set of artifacts that will be provided in the export. The defined values of `exportMode` are:
|
|
183
|
+
* `current`: include only the current transcript span and current snapshot for each vat: just the minimum set necessary for current operations
|
|
184
|
+
* `archival`: include all available transcript spans
|
|
185
|
+
* `debug`: include all available transcript spans *and* all available snapshots. The old snapshots are never necessary for normal operations, nor are they likely to be usefor for extreme upgrade scenarios, but they might be useful for some unusual debugging operation
|
|
186
|
+
|
|
187
|
+
Note that `exportMode` does not affect the Export Data generated by the exporter (if we *ever* want to validate this optional data, the hashes are mandatory). It only affects the names returned by `getArtifactNames()`: the list will be smaller for `current` than for `archival`. Re-exporting from a pruned copy will lack the old data, even if the re-export uses `archival`, because the second SwingStore cannot magically reconstruct the missing data.
|
|
181
188
|
|
|
182
189
|
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`.
|
|
183
190
|
|
|
191
|
+
When importing, the `importSwingStore()` function takes an options bag, which has property named `includeHistorical`. This property defaults to `false`, which makes the importer ignore any historical artifacts present in the export dataset. To import the historical transcript spans (and snapshots), you must set it to `true`.
|
|
192
|
+
|
|
193
|
+
So, to convey historical transcript spans from one swingstore to another, you must set three options along the way:
|
|
194
|
+
|
|
195
|
+
* the original swingstore must be opened with `{ includeHistorical: true }`, otherwise the old spans will be pruned immediately
|
|
196
|
+
* the export must use `makeSwingStoreExporter(dirpath, 'archival')`, otherwise the export will omit the old spans
|
|
197
|
+
* the import must use `importSwingStore(exporter, dirPath, { includeHistorical: true })`, otherwide teh import will ignore the old spans
|
|
198
|
+
|
|
184
199
|
## Implementation Details
|
|
185
200
|
|
|
186
|
-
SwingStore contains components to
|
|
201
|
+
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
202
|
|
|
188
203
|
* `kvStore`, a general-purpose string/string key-value table
|
|
189
204
|
* `transcriptStore`: append-only vat deliveries, broken into "spans", delimited by heap snapshot events
|
|
190
205
|
* `snapshotStore`: binary blobs containing JS engine heap state, to limit transcript replay depth
|
|
206
|
+
* `bundleStore`: code bundles that can be imported with `@endo/import-bundle`
|
|
191
207
|
|
|
192
|
-
Currently, the SwingStore treats transcript spans
|
|
208
|
+
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
209
|
|
|
194
210
|
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.2-dev-
|
|
3
|
+
"version": "0.9.2-dev-aa10ecd.0+aa10ecd",
|
|
4
4
|
"description": "Persistent storage for SwingSet",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "src/
|
|
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,8 +21,8 @@
|
|
|
18
21
|
"lint:eslint": "eslint ."
|
|
19
22
|
},
|
|
20
23
|
"dependencies": {
|
|
21
|
-
"@agoric/assert": "0.6.1-dev-
|
|
22
|
-
"@agoric/internal": "0.3.3-dev-
|
|
24
|
+
"@agoric/assert": "0.6.1-dev-aa10ecd.0+aa10ecd",
|
|
25
|
+
"@agoric/internal": "0.3.3-dev-aa10ecd.0+aa10ecd",
|
|
23
26
|
"@endo/base64": "^0.2.32",
|
|
24
27
|
"@endo/bundle-source": "^2.5.2",
|
|
25
28
|
"@endo/check-bundle": "^0.2.19",
|
|
@@ -42,5 +45,5 @@
|
|
|
42
45
|
],
|
|
43
46
|
"timeout": "2m"
|
|
44
47
|
},
|
|
45
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "aa10ecdb73b23c9a11077b118fa562bff1708c74"
|
|
46
49
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('./internal.js').SwingStoreInternal} internal
|
|
3
|
+
* @param {'operational'} level
|
|
4
|
+
* @returns {void}
|
|
5
|
+
*/
|
|
6
|
+
export function assertComplete(internal, level) {
|
|
7
|
+
assert.equal(level, 'operational'); // only option for now
|
|
8
|
+
// every bundle must be populated
|
|
9
|
+
internal.bundleStore.assertComplete(level);
|
|
10
|
+
|
|
11
|
+
// every 'isCurrent' transcript span must have all items
|
|
12
|
+
// TODO: every vat with any data must have a isCurrent transcript
|
|
13
|
+
// span
|
|
14
|
+
internal.transcriptStore.assertComplete(level);
|
|
15
|
+
|
|
16
|
+
// every 'inUse' snapshot must be populated
|
|
17
|
+
internal.snapStore.assertComplete(level);
|
|
18
|
+
|
|
19
|
+
// TODO: every isCurrent span that starts with load-snapshot has a
|
|
20
|
+
// matching snapshot (counter-argument: swing-store should not know
|
|
21
|
+
// those details about transcript entries)
|
|
22
|
+
}
|
package/src/bundleStore.js
CHANGED
|
@@ -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,7 @@ import { buffer } from './util.js';
|
|
|
16
15
|
* @typedef { EndoZipBase64Bundle | GetExportBundle | NestedEvaluateBundle } Bundle
|
|
17
16
|
*/
|
|
18
17
|
/**
|
|
19
|
-
* @typedef { import('./
|
|
18
|
+
* @typedef { import('./exporter').SwingStoreExporter } SwingStoreExporter
|
|
20
19
|
*
|
|
21
20
|
* @typedef {{
|
|
22
21
|
* addBundle: (bundleID: string, bundle: Bundle) => void;
|
|
@@ -27,7 +26,10 @@ import { buffer } from './util.js';
|
|
|
27
26
|
*
|
|
28
27
|
* @typedef {{
|
|
29
28
|
* exportBundle: (name: string) => AsyncIterableIterator<Uint8Array>,
|
|
30
|
-
*
|
|
29
|
+
* repairBundleRecord: (key: string, value: string) => void,
|
|
30
|
+
* importBundleRecord: (key: string, value: string) => void,
|
|
31
|
+
* importBundle: (name: string, dataProvider: () => Promise<Buffer>) => Promise<void>,
|
|
32
|
+
* assertComplete: (level: 'operational') => void,
|
|
31
33
|
* getExportRecords: () => IterableIterator<readonly [key: string, value: string]>,
|
|
32
34
|
* getArtifactNames: () => AsyncIterableIterator<string>,
|
|
33
35
|
* getBundleIDs: () => IterableIterator<string>,
|
|
@@ -39,6 +41,18 @@ import { buffer } from './util.js';
|
|
|
39
41
|
*
|
|
40
42
|
*/
|
|
41
43
|
|
|
44
|
+
function bundleIDFromName(name) {
|
|
45
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
46
|
+
const [tag, ...pieces] = name.split('.');
|
|
47
|
+
if (tag !== 'bundle' || pieces.length !== 1) {
|
|
48
|
+
Fail`expected artifact name of the form 'bundle.{bundleID}', saw ${q(
|
|
49
|
+
name,
|
|
50
|
+
)}`;
|
|
51
|
+
}
|
|
52
|
+
const bundleID = pieces[0];
|
|
53
|
+
return bundleID;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
/**
|
|
43
57
|
* @param {*} db
|
|
44
58
|
* @param {() => void} ensureTxn
|
|
@@ -54,6 +68,9 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
54
68
|
)
|
|
55
69
|
`);
|
|
56
70
|
|
|
71
|
+
// A populated record contains both bundleID and bundle, while a
|
|
72
|
+
// pruned record has a bundle of NULL.
|
|
73
|
+
|
|
57
74
|
function bundleArtifactName(bundleID) {
|
|
58
75
|
return `bundle.${bundleID}`;
|
|
59
76
|
}
|
|
@@ -62,20 +79,36 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
62
79
|
return `b${Nat(version)}-${hash}`;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
VALUES (?,
|
|
82
|
+
// the PRIMARY KEY constraint requires the bundleID not already
|
|
83
|
+
// exist
|
|
84
|
+
const sqlAddBundleRecord = db.prepare(`
|
|
85
|
+
INSERT INTO bundles (bundleID, bundle) VALUES (?, NULL)
|
|
69
86
|
`);
|
|
70
87
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
// this sees both populated and pruned (not-yet-populated) records
|
|
89
|
+
const sqlHasBundleRecord = db.prepare(`
|
|
90
|
+
SELECT count(*)
|
|
91
|
+
FROM bundles
|
|
92
|
+
WHERE bundleID = ?
|
|
93
|
+
`);
|
|
94
|
+
sqlHasBundleRecord.pluck();
|
|
95
|
+
|
|
96
|
+
const sqlPopulateBundleRecord = db.prepare(`
|
|
97
|
+
UPDATE bundles SET bundle = $serialized WHERE bundleID = $bundleID
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
function addBundleRecord(bundleID) {
|
|
101
|
+
ensureTxn();
|
|
102
|
+
sqlAddBundleRecord.run(bundleID);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function populateBundle(bundleID, serialized) {
|
|
78
106
|
ensureTxn();
|
|
107
|
+
sqlHasBundleRecord.get(bundleID) || Fail`missing ${bundleID}`;
|
|
108
|
+
sqlPopulateBundleRecord.run({ bundleID, serialized });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function serializeBundle(bundleID, bundle) {
|
|
79
112
|
const { moduleFormat } = bundle;
|
|
80
113
|
let serialized;
|
|
81
114
|
if (bundleID.startsWith('b0-')) {
|
|
@@ -98,19 +131,55 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
98
131
|
} else {
|
|
99
132
|
throw Fail`unsupported BundleID ${bundleID}`;
|
|
100
133
|
}
|
|
101
|
-
|
|
134
|
+
return serialized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Store a complete bundle in a single operation, used by runtime
|
|
139
|
+
* (i.e. not an import). We rely upon the caller to provide a
|
|
140
|
+
* correct bundle (e.g. no unexpected properties), but we still
|
|
141
|
+
* check the ID against the contents.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} bundleID
|
|
144
|
+
* @param {Bundle} bundle
|
|
145
|
+
*/
|
|
146
|
+
function addBundle(bundleID, bundle) {
|
|
147
|
+
const serialized = serializeBundle(bundleID, bundle);
|
|
148
|
+
addBundleRecord(bundleID);
|
|
149
|
+
populateBundle(bundleID, serialized);
|
|
102
150
|
noteExport(bundleArtifactName(bundleID), bundleID);
|
|
103
151
|
}
|
|
104
152
|
|
|
105
|
-
const
|
|
153
|
+
const sqlGetPrunedBundles = db.prepare(`
|
|
154
|
+
SELECT bundleID
|
|
155
|
+
FROM bundles
|
|
156
|
+
WHERE bundle IS NULL
|
|
157
|
+
ORDER BY bundleID
|
|
158
|
+
`);
|
|
159
|
+
sqlGetPrunedBundles.pluck();
|
|
160
|
+
|
|
161
|
+
function getPrunedBundles() {
|
|
162
|
+
return sqlGetPrunedBundles.all();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assertComplete(level) {
|
|
166
|
+
assert.equal(level, 'operational'); // for now
|
|
167
|
+
const pruned = getPrunedBundles();
|
|
168
|
+
if (pruned.length) {
|
|
169
|
+
throw Fail`missing bundles for: ${pruned.join(',')}`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sqlHasPopulatedBundle = db.prepare(`
|
|
106
174
|
SELECT count(*)
|
|
107
175
|
FROM bundles
|
|
108
176
|
WHERE bundleID = ?
|
|
177
|
+
AND bundle IS NOT NULL
|
|
109
178
|
`);
|
|
110
|
-
|
|
179
|
+
sqlHasPopulatedBundle.pluck(true);
|
|
111
180
|
|
|
112
181
|
function hasBundle(bundleID) {
|
|
113
|
-
const count =
|
|
182
|
+
const count = sqlHasPopulatedBundle.get(bundleID);
|
|
114
183
|
return count !== 0;
|
|
115
184
|
}
|
|
116
185
|
|
|
@@ -119,15 +188,15 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
119
188
|
FROM bundles
|
|
120
189
|
WHERE bundleID = ?
|
|
121
190
|
`);
|
|
122
|
-
sqlGetBundle.pluck(true);
|
|
123
191
|
|
|
124
192
|
/**
|
|
125
193
|
* @param {string} bundleID
|
|
126
194
|
* @returns {Bundle}
|
|
127
195
|
*/
|
|
128
196
|
function getBundle(bundleID) {
|
|
129
|
-
const
|
|
130
|
-
|
|
197
|
+
const row =
|
|
198
|
+
sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
|
|
199
|
+
const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
|
|
131
200
|
if (bundleID.startsWith('b0-')) {
|
|
132
201
|
return harden(JSON.parse(rawBundle));
|
|
133
202
|
} else if (bundleID.startsWith('b1-')) {
|
|
@@ -153,6 +222,31 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
153
222
|
}
|
|
154
223
|
}
|
|
155
224
|
|
|
225
|
+
// take an export-data record (id/hash but not bundle contents) and
|
|
226
|
+
// insert something in the DB
|
|
227
|
+
function importBundleRecord(key, value) {
|
|
228
|
+
const bundleID = bundleIDFromName(key);
|
|
229
|
+
assert.equal(bundleID, value);
|
|
230
|
+
addBundleRecord(bundleID);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function repairBundleRecord(key, value) {
|
|
234
|
+
// Bundle records have no metadata, and all bundles must be
|
|
235
|
+
// present (there's no notion of "historical bundle"). So there's
|
|
236
|
+
// no "repair", and if the repair process supplies a bundle record
|
|
237
|
+
// that isn't already present, we throw an error. The repair
|
|
238
|
+
// process doesn't get artifacts, so adding a new record here
|
|
239
|
+
// would fail the subsequent completeness check anyways.
|
|
240
|
+
|
|
241
|
+
const bundleID = bundleIDFromName(key);
|
|
242
|
+
assert.equal(bundleID, value);
|
|
243
|
+
if (sqlHasBundleRecord.get(bundleID)) {
|
|
244
|
+
// record is present, there's no metadata to mismatch, so ignore quietly
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
throw Fail`unexpected new bundle record for ${bundleID} during repair`;
|
|
248
|
+
}
|
|
249
|
+
|
|
156
250
|
/**
|
|
157
251
|
* Read a bundle and return it as a stream of data suitable for export to
|
|
158
252
|
* another store.
|
|
@@ -166,14 +260,10 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
166
260
|
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
167
261
|
*/
|
|
168
262
|
async function* exportBundle(name) {
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
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`;
|
|
263
|
+
const bundleID = bundleIDFromName(name);
|
|
264
|
+
const row =
|
|
265
|
+
sqlGetBundle.get(bundleID) || Fail`bundle ${q(bundleID)} not found`;
|
|
266
|
+
const rawBundle = row.bundle || Fail`bundle ${q(bundleID)} pruned`;
|
|
177
267
|
yield* Readable.from(Buffer.from(rawBundle));
|
|
178
268
|
}
|
|
179
269
|
|
|
@@ -209,23 +299,17 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
209
299
|
}
|
|
210
300
|
|
|
211
301
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
302
|
+
* Call addBundleRecord() first, then this importBundle() will
|
|
303
|
+
* populate the record.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} name Artifact name, `bundle.${bundleID}`
|
|
306
|
+
* @param {() => Promise<Buffer>} dataProvider Function to get bundle bytes
|
|
215
307
|
* @returns {Promise<void>}
|
|
216
308
|
*/
|
|
217
|
-
async function importBundle(name,
|
|
309
|
+
async function importBundle(name, dataProvider) {
|
|
218
310
|
await 0; // no synchronous prefix
|
|
219
|
-
const
|
|
220
|
-
const
|
|
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);
|
|
311
|
+
const bundleID = bundleIDFromName(name);
|
|
312
|
+
const data = await dataProvider();
|
|
229
313
|
if (bundleID.startsWith('b0-')) {
|
|
230
314
|
// we dissect and reassemble the bundle, to exclude unexpected properties
|
|
231
315
|
const { moduleFormat, source, sourceMap } = JSON.parse(data.toString());
|
|
@@ -234,7 +318,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
234
318
|
const serialized = JSON.stringify(bundle);
|
|
235
319
|
bundleID === bundleIdFromHash(0, createSHA256(serialized).finish()) ||
|
|
236
320
|
Fail`bundleID ${q(bundleID)} does not match bundle artifact`;
|
|
237
|
-
|
|
321
|
+
populateBundle(bundleID, serialized);
|
|
238
322
|
} else if (bundleID.startsWith('b1-')) {
|
|
239
323
|
/** @type {EndoZipBase64Bundle} */
|
|
240
324
|
const bundle = harden({
|
|
@@ -244,7 +328,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
244
328
|
});
|
|
245
329
|
// Assert that the bundle contents match the ID and hash
|
|
246
330
|
await checkBundle(bundle, computeSha512, bundleID);
|
|
247
|
-
|
|
331
|
+
populateBundle(bundleID, serializeBundle(bundleID, bundle));
|
|
248
332
|
} else {
|
|
249
333
|
Fail`unsupported BundleID ${q(bundleID)}`;
|
|
250
334
|
}
|
|
@@ -264,7 +348,7 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
264
348
|
const dump = {};
|
|
265
349
|
for (const row of sql.iterate()) {
|
|
266
350
|
const { bundleID, bundle } = row;
|
|
267
|
-
dump[bundleID] = encodeBase64(bundle);
|
|
351
|
+
dump[bundleID] = encodeBase64(Buffer.from(bundle, 'utf-8'));
|
|
268
352
|
}
|
|
269
353
|
return dump;
|
|
270
354
|
}
|
|
@@ -281,15 +365,20 @@ export function makeBundleStore(db, ensureTxn, noteExport = () => {}) {
|
|
|
281
365
|
}
|
|
282
366
|
|
|
283
367
|
return harden({
|
|
368
|
+
importBundleRecord,
|
|
369
|
+
importBundle,
|
|
370
|
+
assertComplete,
|
|
371
|
+
|
|
284
372
|
addBundle,
|
|
285
373
|
hasBundle,
|
|
286
374
|
getBundle,
|
|
287
375
|
deleteBundle,
|
|
376
|
+
|
|
288
377
|
getExportRecords,
|
|
289
378
|
getArtifactNames,
|
|
290
379
|
exportBundle,
|
|
291
|
-
importBundle,
|
|
292
380
|
getBundleIDs,
|
|
381
|
+
repairBundleRecord,
|
|
293
382
|
|
|
294
383
|
dumpBundles,
|
|
295
384
|
});
|
package/src/exporter.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import sqlite3 from 'better-sqlite3';
|
|
2
|
+
|
|
3
|
+
import { Fail, q } from '@agoric/assert';
|
|
4
|
+
|
|
5
|
+
import { dbFileInDirectory } from './util.js';
|
|
6
|
+
import { getKeyType } from './kvStore.js';
|
|
7
|
+
import { makeBundleStore } from './bundleStore.js';
|
|
8
|
+
import { makeSnapStore } from './snapStore.js';
|
|
9
|
+
import { makeSnapStoreIO } from './snapStoreIO.js';
|
|
10
|
+
import { makeTranscriptStore } from './transcriptStore.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @template T
|
|
14
|
+
* @typedef { Iterable<T> | AsyncIterable<T> } AnyIterable<T>
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @template T
|
|
18
|
+
* @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator<T>
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @typedef {readonly [
|
|
24
|
+
* key: string,
|
|
25
|
+
* value?: string | null | undefined,
|
|
26
|
+
* ]} KVPair
|
|
27
|
+
*
|
|
28
|
+
* @typedef {object} SwingStoreExporter
|
|
29
|
+
*
|
|
30
|
+
* Allows export of data from a swingStore as a fixed view onto the content as
|
|
31
|
+
* of the most recent commit point at the time the exporter was created. The
|
|
32
|
+
* exporter may be used while another SwingStore instance is active for the same
|
|
33
|
+
* DB, possibly in another thread or process. It guarantees that regardless of
|
|
34
|
+
* the concurrent activity of other swingStore instances, the data representing
|
|
35
|
+
* the commit point will stay consistent and available.
|
|
36
|
+
*
|
|
37
|
+
* @property {() => AnyIterableIterator<KVPair>} getExportData
|
|
38
|
+
*
|
|
39
|
+
* Get a full copy of the first-stage export data (key-value pairs) from the
|
|
40
|
+
* swingStore. This represents both the contents of the KVStore (excluding host
|
|
41
|
+
* and local prefixes), as well as any data needed to validate all artifacts,
|
|
42
|
+
* both current and historical. As such it represents the root of trust for the
|
|
43
|
+
* application.
|
|
44
|
+
*
|
|
45
|
+
* Content of validation data (with supporting entries for indexing):
|
|
46
|
+
* - kv.${key} = ${value} // ordinary kvStore data entry
|
|
47
|
+
* - snapshot.${vatID}.${snapPos} = ${{ vatID, snapPos, hash }};
|
|
48
|
+
* - snapshot.${vatID}.current = `snapshot.${vatID}.${snapPos}`
|
|
49
|
+
* - transcript.${vatID}.${startPos} = ${{ vatID, startPos, endPos, hash }}
|
|
50
|
+
* - transcript.${vatID}.current = ${{ vatID, startPos, endPos, hash }}
|
|
51
|
+
*
|
|
52
|
+
* @property {() => AnyIterableIterator<string>} getArtifactNames
|
|
53
|
+
*
|
|
54
|
+
* Get a list of name of artifacts available from the swingStore. A name
|
|
55
|
+
* returned by this method guarantees that a call to `getArtifact` on the same
|
|
56
|
+
* exporter instance will succeed. The `exportMode` option to
|
|
57
|
+
* `makeSwingStoreExporter` controls the filtering of the artifact names
|
|
58
|
+
* yielded.
|
|
59
|
+
*
|
|
60
|
+
* Artifact names:
|
|
61
|
+
* - transcript.${vatID}.${startPos}.${endPos}
|
|
62
|
+
* - snapshot.${vatID}.${snapPos}
|
|
63
|
+
* - bundle.${bundleID}
|
|
64
|
+
*
|
|
65
|
+
* @property {(name: string) => AnyIterableIterator<Uint8Array>} getArtifact
|
|
66
|
+
*
|
|
67
|
+
* Retrieve an artifact by name as a sequence of binary chunks. May throw if
|
|
68
|
+
* the artifact is not available, which can occur if the artifact is historical
|
|
69
|
+
* and wasn't preserved.
|
|
70
|
+
*
|
|
71
|
+
* @property {() => Promise<void>} close
|
|
72
|
+
*
|
|
73
|
+
* Dispose of all resources held by this exporter. Any further operation on this
|
|
74
|
+
* exporter or its outstanding iterators will fail.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {'current' | 'archival' | 'debug'} ExportMode
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} dirPath
|
|
83
|
+
* @param { ExportMode } exportMode
|
|
84
|
+
* @returns {SwingStoreExporter}
|
|
85
|
+
*/
|
|
86
|
+
export function makeSwingStoreExporter(dirPath, exportMode = 'current') {
|
|
87
|
+
typeof dirPath === 'string' || Fail`dirPath must be a string`;
|
|
88
|
+
exportMode === 'current' ||
|
|
89
|
+
exportMode === 'archival' ||
|
|
90
|
+
exportMode === 'debug' ||
|
|
91
|
+
Fail`invalid exportMode ${q(exportMode)}`;
|
|
92
|
+
const exportHistoricalSnapshots = exportMode === 'debug';
|
|
93
|
+
const exportHistoricalTranscripts = exportMode !== 'current';
|
|
94
|
+
const filePath = dbFileInDirectory(dirPath);
|
|
95
|
+
const db = sqlite3(filePath);
|
|
96
|
+
|
|
97
|
+
// Execute the data export in a (read) transaction, to ensure that we are
|
|
98
|
+
// capturing the state of the database at a single point in time. Our close()
|
|
99
|
+
// will ROLLBACK the txn just in case some bug tried to change the DB.
|
|
100
|
+
const sqlBeginTransaction = db.prepare('BEGIN TRANSACTION');
|
|
101
|
+
sqlBeginTransaction.run();
|
|
102
|
+
|
|
103
|
+
// ensureTxn can be a dummy, we just started one
|
|
104
|
+
const ensureTxn = () => {};
|
|
105
|
+
const snapStore = makeSnapStore(db, ensureTxn, makeSnapStoreIO());
|
|
106
|
+
const bundleStore = makeBundleStore(db, ensureTxn);
|
|
107
|
+
const transcriptStore = makeTranscriptStore(db, ensureTxn, () => {});
|
|
108
|
+
|
|
109
|
+
const sqlGetAllKVData = db.prepare(`
|
|
110
|
+
SELECT key, value
|
|
111
|
+
FROM kvStore
|
|
112
|
+
ORDER BY key
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @returns {AsyncIterableIterator<KVPair>}
|
|
117
|
+
* @yields {KVPair}
|
|
118
|
+
*/
|
|
119
|
+
async function* getExportData() {
|
|
120
|
+
for (const { key, value } of sqlGetAllKVData.iterate()) {
|
|
121
|
+
if (getKeyType(key) === 'consensus') {
|
|
122
|
+
yield [`kv.${key}`, value];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
yield* snapStore.getExportRecords(true);
|
|
126
|
+
yield* transcriptStore.getExportRecords(true);
|
|
127
|
+
yield* bundleStore.getExportRecords();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @returns {AsyncIterableIterator<string>}
|
|
132
|
+
* @yields {string}
|
|
133
|
+
*/
|
|
134
|
+
async function* getArtifactNames() {
|
|
135
|
+
yield* snapStore.getArtifactNames(exportHistoricalSnapshots);
|
|
136
|
+
yield* transcriptStore.getArtifactNames(exportHistoricalTranscripts);
|
|
137
|
+
yield* bundleStore.getArtifactNames();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} name
|
|
142
|
+
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
143
|
+
*/
|
|
144
|
+
function getArtifact(name) {
|
|
145
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
146
|
+
const [type] = name.split('.', 1);
|
|
147
|
+
|
|
148
|
+
if (type === 'snapshot') {
|
|
149
|
+
return snapStore.exportSnapshot(name);
|
|
150
|
+
} else if (type === 'transcript') {
|
|
151
|
+
return transcriptStore.exportSpan(name);
|
|
152
|
+
} else if (type === 'bundle') {
|
|
153
|
+
return bundleStore.exportBundle(name);
|
|
154
|
+
} else {
|
|
155
|
+
throw Fail`invalid type in artifact name ${q(name)}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sqlAbort = db.prepare('ROLLBACK');
|
|
160
|
+
|
|
161
|
+
async function close() {
|
|
162
|
+
// After all the data has been extracted, always abort the export
|
|
163
|
+
// transaction to ensure that the export was read-only (i.e., that no bugs
|
|
164
|
+
// inadvertantly modified the database).
|
|
165
|
+
sqlAbort.run();
|
|
166
|
+
db.close();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return harden({
|
|
170
|
+
getExportData,
|
|
171
|
+
getArtifactNames,
|
|
172
|
+
getArtifact,
|
|
173
|
+
close,
|
|
174
|
+
});
|
|
175
|
+
}
|