@agoric/swing-store 0.9.2-u16.0 → 0.9.2-u17.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 +7 -1
- package/docs/bundlestore.md +30 -0
- package/docs/data-export.md +6 -6
- package/docs/kvstore.md +35 -0
- package/docs/snapstore.md +49 -0
- package/docs/swingstore.md +62 -6
- package/docs/transcriptstore.md +70 -0
- package/package.json +10 -10
- package/src/archiver.js +80 -0
- package/src/bundleStore.js +1 -1
- package/src/exporter.js +5 -5
- package/src/hasher.js +1 -1
- package/src/importer.js +1 -1
- package/src/index.js +2 -0
- package/src/internal.js +1 -1
- package/src/kvStore.js +1 -1
- package/src/repairMetadata.js +1 -1
- package/src/snapStore.js +113 -77
- package/src/swingStore.js +11 -2
- package/src/transcriptStore.js +281 -141
- package/test/deletion.test.js +644 -20
- package/test/export.test.js +4 -4
- package/test/exportImport.test.js +1 -1
- package/test/snapstore.test.js +47 -2
- package/test/state.test.js +91 -67
package/CHANGELOG.md
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
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-
|
|
6
|
+
### [0.9.2-u17.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/swing-store@0.9.1...@agoric/swing-store@0.9.2-u17.0) (2024-09-17)
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
### Features
|
|
10
10
|
|
|
11
|
+
* Add consensus-independent vat snapshot archiving configuration to AG_COSMOS_INIT ([ffc594f](https://github.com/Agoric/agoric-sdk/commit/ffc594f9441a9374646c43b69d289cc560962f64)), closes [#10036](https://github.com/Agoric/agoric-sdk/issues/10036)
|
|
12
|
+
* Add consensus-independent vat transcript archiving configuration to AG_COSMOS_INIT ([d2d5803](https://github.com/Agoric/agoric-sdk/commit/d2d5803baab6e6379d179723244b2e92aac6319a)), closes [#10036](https://github.com/Agoric/agoric-sdk/issues/10036)
|
|
11
13
|
* add exporter.getHostKV() API ([eb564f9](https://github.com/Agoric/agoric-sdk/commit/eb564f9635397c0706e1f8255b3e125681e2d031)), closes [#8523](https://github.com/Agoric/agoric-sdk/issues/8523)
|
|
14
|
+
* **swing-store:** budget-limited deletion of snapshot and transcripts ([c43bf63](https://github.com/Agoric/agoric-sdk/commit/c43bf63846aedf3493ac6e8f4bc9f2bb48401d66)), closes [#8928](https://github.com/Agoric/agoric-sdk/issues/8928)
|
|
12
15
|
* **swing-store:** faster import of swing-store ([0170568](https://github.com/Agoric/agoric-sdk/commit/0170568d66748af76f0bd24a4acdaa34b9c79cca))
|
|
16
|
+
* **swing-store:** Limit item deletion to the previously-current transcript span ([766c1bb](https://github.com/Agoric/agoric-sdk/commit/766c1bbb082debe9d6fa94e08466d3596c971843)), closes [#9387](https://github.com/Agoric/agoric-sdk/issues/9387)
|
|
13
17
|
* **swing-store:** prevent SwingSet usage of imported swing-store ([6a833eb](https://github.com/Agoric/agoric-sdk/commit/6a833ebda2b2ff0e72040ca8186f93ae91567add))
|
|
14
18
|
* **swingstore:** add repairMetadata() ([33b5c1c](https://github.com/Agoric/agoric-sdk/commit/33b5c1c1fefd5278a24cd5f06630b238439e2891)), closes [#8025](https://github.com/Agoric/agoric-sdk/issues/8025) [#8025](https://github.com/Agoric/agoric-sdk/issues/8025)
|
|
15
19
|
* tool for auditing dangling kindID references ([eeadc46](https://github.com/Agoric/agoric-sdk/commit/eeadc462d8fb09449e4ea6f0118ae8654e0c8e9b)), closes [#7655](https://github.com/Agoric/agoric-sdk/issues/7655)
|
|
@@ -20,7 +24,9 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
|
|
20
24
|
* misc runtime robustness found by typecheck ([a033f26](https://github.com/Agoric/agoric-sdk/commit/a033f2638f9f11e19d94d7931e4e0614773b1f60))
|
|
21
25
|
* performance.now() binding ([4a3b59b](https://github.com/Agoric/agoric-sdk/commit/4a3b59b486c0cb916e531d5de390fbf90e4421ad))
|
|
22
26
|
* rewrite importSwingStore to preserve metadata properly ([38c9efc](https://github.com/Agoric/agoric-sdk/commit/38c9efce10957e0eb245e25ae5f9f45792eb58ad)), closes [#8025](https://github.com/Agoric/agoric-sdk/issues/8025)
|
|
27
|
+
* **swing-store:** accept budget=Infinity to allow unlimited deletions ([c22b656](https://github.com/Agoric/agoric-sdk/commit/c22b65610007607f13373bc1dcc54007e30e2d60))
|
|
23
28
|
* **swing-store:** add 'replay' artifactMode, make export more strict ([9939ea6](https://github.com/Agoric/agoric-sdk/commit/9939ea699bb1fd0b711f950679b432eef9054fda)), closes [#8105](https://github.com/Agoric/agoric-sdk/issues/8105)
|
|
29
|
+
* **swing-store:** Delete transcript spans in stopUsingTranscript as in rollover ([2d1e478](https://github.com/Agoric/agoric-sdk/commit/2d1e47844760e0534afb3fe410a9635656949dad)), closes [#10054](https://github.com/Agoric/agoric-sdk/issues/10054)
|
|
24
30
|
* **swing-store:** ensure crank savepoint is wrapped in transaction ([9d2dd3f](https://github.com/Agoric/agoric-sdk/commit/9d2dd3f9966940961a4c21351d256fa3615715d7))
|
|
25
31
|
* **swing-store:** explicitly harden prototypes ([86c128a](https://github.com/Agoric/agoric-sdk/commit/86c128a29b5ed61764a67644c3734b0c05df2993))
|
|
26
32
|
* **swing-store:** no completeness check when creating exporter ([d4df073](https://github.com/Agoric/agoric-sdk/commit/d4df073ffcc48b0f0e62bac107ee8edf21150ad9))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# BundleStore
|
|
2
|
+
|
|
3
|
+
The `kernelStorage.bundleStore` sub-store manages code bundles. These can be used to hold vat-worker supervisor code (e.g. the [`@endo/lockdown`](https://github.com/endojs/endo/tree/master/packages/lockdown) bundle, or the [`@agoric/swingset-xsnap-supervisor` package](../../swingset-xsnap-supervisor), which incorporates liveslots), or the initial vat code bundles (for both kernel-defined bundles like vat-comms or vat-timer, or for application-defined bundles like vat-zoe or the ZCF code). It can also hold bundles that will be loaded later by userspace vat code, such as contract bundles.
|
|
4
|
+
|
|
5
|
+
Each bundle held by the bundleStore is identified by a secure BundleID, which contains a format version integer and a hash, with a format like `b0-123abc456def...` or `b1-789ghi012...`. This contains enough information to securely define the behavior of the code inside the bundle, and to identify the tools needed to load/evaluate it.
|
|
6
|
+
|
|
7
|
+
The bundleStore provides a simple add/get/remove API to the kernel. The kernel adds its own bundles during initialization, and provides the host application with an API to load additional ones in later. The kernel code that creates new vats will read bundles from the bundleStore when necessary, as vats are created. Userspace can get access to "BundleCap" objects that represent bundles, to keep the large bundle blobs out of RAM as much as possible.
|
|
8
|
+
|
|
9
|
+
## Data Model
|
|
10
|
+
|
|
11
|
+
Bundles are actually JavaScript objects: records of at least `{ moduleFormat }`, plus some format-specific fields like `endoZipBase64` and `endoZipBase64Sha512`. They are created by the [`@endo/bundle-source`](https://github.com/endojs/endo/tree/master/packages/bundle-source) package. Many are consumed by [`@endo/import-bundle`](https://github.com/endojs/endo/tree/master/packages/import-bundle), but the `b0-` format bundles can be loaded with some simple string manipulation and a call to `eval()` (which is how supervisor bundles are injected into new vat workers, before `@endo/import-bundle` is available).
|
|
12
|
+
|
|
13
|
+
The bundleStore database treats each bundle as BundleID and a blob of contents. The SQLite `bundles` table is just `(bundleID TEXT, bundle BLOB)`. The bundleStore knows about each `moduleFormat` and how to extract the meaningful data and compress it into a blob, and how to produce the Bundle object during retrieval.
|
|
14
|
+
|
|
15
|
+
The bundleStore also knows about the BundleID computation rules. The `addBundle()` API will verify that the contents match the ID, however it currently relies upon the caller to verify e.g. that the bundle does not contain any unexpected properties. The `importSwingStore()` API performs more extensive validation, to prevent corruption during the export+import process.
|
|
16
|
+
|
|
17
|
+
The kernel is expected to keep track of which bundles are needed and when (with reference counts), and to not delete a bundle unless it is really unneeded. Currently, this means all bundles are retained forever.
|
|
18
|
+
|
|
19
|
+
Unlike the `snapStore`, there is no notion of pruning bundles: either the bundle is present (with all its data), or there is no record of the BundleID at all.
|
|
20
|
+
|
|
21
|
+
## Export Model
|
|
22
|
+
|
|
23
|
+
Each bundle gets a single export-data entry, whose name is `bundle.${bundleID}`, and whose value is just `${bundleID}`. Each bundle also gets a single export artifact, whose name is `bundle.${bundleID}`, and whose contents are the compressed BLOB from the database (from which a Bundle record can be reconstructed).
|
|
24
|
+
|
|
25
|
+
## Slow Deletion
|
|
26
|
+
|
|
27
|
+
Since bundles are not owned by vats, there is nothing to delete when a vat is terminated. So unlike `transcriptStore` and `snapStore`, there is no concept of "slow deletion", and no APIs to support it.
|
|
28
|
+
|
|
29
|
+
When a bundle is deleted by `bundleStore.deleteBundle()`, its export-data item is deleted immediately, and subsequent exports will omit the corresponding artifact.
|
|
30
|
+
|
package/docs/data-export.md
CHANGED
|
@@ -188,13 +188,13 @@ Once the new SwingStore is fully populated with the previously-exported data, th
|
|
|
188
188
|
|
|
189
189
|
Some of the data maintained by SwingStore is not strictly necessary for kernel execution, at least under normal circumstances. For example, once a vat worker performs a heap snapshot, we no longer need the transcript entries from before the snapshot was taken, since vat replay will start from the snapshot point. We split each vat's transcript into "spans", delimited by heap snapshot events, and the "current span" is the most recent one (still growing), whereas the "historical spans" are all closed and immutable. Likewise, we only really need the most recent heap snapshot for each vat: older snapshots might be interesting for experiments that replay old transcripts with different versions of the XS engine, but no normal kernel will ever need them.
|
|
190
190
|
|
|
191
|
-
Most
|
|
191
|
+
Most blockchain validator nodes would prefer to prune this data, to reduce their storage needs. But we can imagine some [extreme upgrade scenarios](https://github.com/Agoric/agoric-sdk/issues/1691) that would require access to these historical transcript spans. Our compromise is to record *validation data* for these historical spans in the export data, but omit the spans themselves from the export artifacts. Validators can delete the old spans at will, and if we ever need them in the future, we can add code that will fetch copies from an archive service, validate them against the export data hashes, and re-insert the relevant entries into the SwingStore.
|
|
192
192
|
|
|
193
193
|
Likewise, each time a heap snapshot is recorded, we cease to need any previous snapshot. And again, as a hedge against even more drastic recovery scenarios, we strike a compromise between minimizing retained data and the ability to validate old snapshots, by retaining only their hashes.
|
|
194
194
|
|
|
195
|
-
As a result, for each active vat, the first-stage Export Data contains a record for every old
|
|
195
|
+
As a result, for each active vat, the first-stage Export Data contains a record for every old heap snapshot, plus one for the most recent heap snapshot, plus a `.current` record that points to the most recent snapshot. It also contains a record for every old transcript span, plus one for the current span. However the exported artifacts may or may not include blobs for the old heap snapshots, or for the old transcript spans.
|
|
196
196
|
|
|
197
|
-
The `openSwingStore()` function has an option named `
|
|
197
|
+
The `openSwingStore()` function has an option named `keepSnapshots` (which defaults to `false`), which causes the snapStore to retain the old heap snapshots. A second option named `keepTranscripts` (which defaults to `true`) causes the transcriptStore to retain the old transcript items. Opening the swingStore with a `false` option does not necessarily delete the old items immediately, but they may get deleted the next time the kernel triggers a heap snapshot or transcript-span rollover. Hosts 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, for use in periodic pruning.
|
|
198
198
|
|
|
199
199
|
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:
|
|
200
200
|
|
|
@@ -218,9 +218,9 @@ While `importSwingStore()`'s options bag accepts the same options as `openSwingS
|
|
|
218
218
|
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:
|
|
219
219
|
|
|
220
220
|
* the original swingstore must not be opened with `{ keepTranscripts: false }`, otherwise the old spans will be pruned immediately
|
|
221
|
-
* the export must use `makeSwingStoreExporter(dirpath, { artifactMode: 'replay'})`, otherwise the export will omit the old spans
|
|
222
|
-
* the import must use `importSwingStore(exporter, dirPath, { artifactMode: 'replay'})`, otherwise the import will ignore the old spans
|
|
223
|
-
* subsequent `openSwingStore` calls must not use `keepTranscripts: false`, otherwise the new swingstore will prune historical spans
|
|
221
|
+
* the export must use `makeSwingStoreExporter(dirpath, { artifactMode: 'replay' })`, otherwise the export will omit the old spans
|
|
222
|
+
* the import must use `importSwingStore(exporter, dirPath, { artifactMode: 'replay' })`, otherwise the import will ignore the old spans
|
|
223
|
+
* subsequent `openSwingStore` calls must not use `keepTranscripts: false`, otherwise the new swingstore will prune historical spans they are replaced during `rolloverSpan`.
|
|
224
224
|
|
|
225
225
|
## Implementation Details
|
|
226
226
|
|
package/docs/kvstore.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# KVStore
|
|
2
|
+
|
|
3
|
+
The `kernelStorage.kvStore` sub-store manages a table of arbitrary key-value (string-to-string) pairs. It provides the usual get/set/has/delete APIs, plus a `getNextKey` call to support lexicographic iteration.
|
|
4
|
+
|
|
5
|
+
There are three separate sections of the namespace. The normal one is the "consensus" section. Each value written here will be given an export-data row, and incorporated into the "crankhash" (described below).
|
|
6
|
+
|
|
7
|
+
The second is "local", and includes any key which is prefixed with `local.`. These keys are *not* given export-data rows, nor are they included in the crankhash.
|
|
8
|
+
|
|
9
|
+
The third is "host", and includes any key which is prefixed with `host.`. This is not available to `kernelStorage.kvStore` at all: it is only accessed by methods on `hostStorage.kvStore` (the `kernelStorage` methods will throw an error if given a key like `host.foo`, and the `hostStorage` methods will throw *unless* given a key like `host.foo`). These are also excluded from export-data and the crankhash. Host keys are reserved for the host application, and are generally used to keep track of things like which block has been executed, to manage consistency between a separate host database (eg IAVL) and the swingstore. The host can record "I told the kernel to execute the contents of block 56" into `hostStorage.kvStore`, and then do `hostStorage.commit()`, and then it can record "I processed the rest of block 56" into is own DB, and then commit its own DB. If, upon startup, it observes a discrepancy between the `hostStorage.kvStore` record and its own DB, it knows it got interrupted between these two commit points, which can trigger recovery code.
|
|
10
|
+
|
|
11
|
+
Any key which doesn't start with `local.` or `host.` is part of the "consensus" section.
|
|
12
|
+
|
|
13
|
+
## CrankHash and ActivityHash
|
|
14
|
+
|
|
15
|
+
Swingset kernels are frequently run in a consensus mode, where multiple instances of the kernel (on different machines) are expected to execute the same deliveries in lock-step. In this mode, every kernel is expected to do exactly the same computation, and any divergence indicates a failure (or attempt at malice). We want to detect such variations quickly, so the diverging/failing member can "fall out of consensus" promptly.
|
|
16
|
+
|
|
17
|
+
The swingstore hashes all changes to the "consensus" portion of the kvStore into the "crank hash". This hash covers every change since the beginning of the current crank, and the kernel logs the result at the end of each crank, at which point the crankhash is reset.
|
|
18
|
+
|
|
19
|
+
Each crank also updates a value called the "activity hash", by hashing the previous activityhash and the latest crankhash together. This records a chain of changes, and is logged at the end of each crank too.
|
|
20
|
+
|
|
21
|
+
The host application can record the activityhash into its own consensus-tracking database (eg IAVL) at the end of each kernel run, to ensure that any internal divergence of swingset behavior is escalated to a proper consensus failure. Without this, one instance of the kernel might "think differently" than the others, but still "act" the same (in terms of IO or externally-visible messages) without triggering a failure, which would be a lurking problem.
|
|
22
|
+
|
|
23
|
+
Logging both the crankhash and the activityhash improves our ability to diagnose consensus failures. By comparing logs between a "good" machine and a "bad" (diverging) one, we can quickly determine which crank caused the problem, and usually compare slogfile delivery/syscall records to narrow the divergence down to a specific syscall.
|
|
24
|
+
|
|
25
|
+
kvStore changes are also recorded by the export-data, but these are too voluminous to be logged, and do not capture multiple changes to the same key. And not all host applications use exports, so there might not be anything watching export data.
|
|
26
|
+
|
|
27
|
+
## Data Model
|
|
28
|
+
|
|
29
|
+
The kvStore holds a simple string-to-string key/value store. The SQLite schema for the `kvStore` table is simply `(key TEXT, value TEXT)`.
|
|
30
|
+
|
|
31
|
+
## Export Model
|
|
32
|
+
|
|
33
|
+
To ensure that every key/value pair is correctly validatable, *all* in-consensus kvStore rows get their own export-data item. The name is just `kv.${key}`, and the value is just the value. `kvStore.delete(key)` will delete the export-data item. There are no artifacts.
|
|
34
|
+
|
|
35
|
+
These make up the vast majority of the export-data items, both by count and by "churn" (the number of export-data items changed in a single crank). In the future, we would prefer to keep the kvStore in some sort of Merkle-tree data structure, and emit only a handful of export-data rows that contain hashes (perhaps just a single root hash). In this approach, the actual data would be exported in one or more artifacts. However, our SQLite backend does not provide the same kind of automatic Merkleization as IAVL, and only holds a single version of data at a time, making this impractical.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SnapStore
|
|
2
|
+
|
|
3
|
+
The `kernelStorage.snapStore` sub-store tracks vat heap snapshots. These blobs capture the state of an XS JavaScript engine, between deliveries, to speed up replay-based persistence. The kernel can start a vat worker from a recent heap snapshot, and then it only needs to replay a handful of transcript items (deliveries), instead of replaying every delivery since the beginning of the incarnation.
|
|
4
|
+
|
|
5
|
+
The XS / [`xsnap`](../../xsnap) engine defines the heap snapshot format. It consists of a large table of "slots", which are linked together to form JavaScript objects, strings, Maps, functions, etc. The snapshot also includes "chunks" for large data fields (like strings and BigInts), a stack, and some other supporting tables. The snapStore doesn't care about any of the internal details: it just gets a big blob of bytes.
|
|
6
|
+
|
|
7
|
+
## Data Model
|
|
8
|
+
|
|
9
|
+
Each snapshot is compressed and stored in the SQLite row as a BLOB. The snapStore has a single table named `snapshots`, with a schema of `(vatID TEXT, snapPos INTEGER, inUse INTEGER, hash TEXT, uncompressedSize INTEGER, compressedSize INTEGER, compressedSnapshot BLOB)`.
|
|
10
|
+
|
|
11
|
+
The kernel has a scheduler which decides when to take a heap snapshot for each vat. There is a tradeoff between the immediate cost of creating the snapshot, versus the expected future savings of having a shorter transcript to replay. More frequent snapshots save time later, at the cost of time spent now.
|
|
12
|
+
|
|
13
|
+
The kernel currently uses a [very simple scheduler](../../SwingSet/src/kernel/vat-warehouse.js), which takes a snapshot every `snapshotInterval` deliveries (e.g. 200), plus an extra one a few deliveries (`snapshotInitial`) into the new incarnation, to avoid replaying expensive contract startup code. The [SwingSet configuration documentation](../../SwingSet/docs/configuration.md) has the details.
|
|
14
|
+
|
|
15
|
+
However, the swingstore is unaware of the kernel's scheduling policy. Every once in a while, the kernel tells the snapStore about a new snapshot, and the snapStore updates its data.
|
|
16
|
+
|
|
17
|
+
Like the [transcriptStore](./transcriptstore.md), the snapStore retains a hash of older records, even after it prunes the snapshot data itself. There is at most one `inUse = 1` record for each vatID, and it will always have the highest `snapPos` value. When a particular vatID's active snapshot is replaced, the SQLite table row is updated to clear the `inUse` flag (i.e. set it to NULL). By default, the `compressedSnapshot` field is also set to NULL, removing the large data blob, but there is an option (`keepSnapshots: true`) to retain the full contents of all snapshots, even the ones that are no longer in use.
|
|
18
|
+
|
|
19
|
+
## Export Model
|
|
20
|
+
|
|
21
|
+
Each snapshot, both current and historic, gets an export-data entry. The name is `snapshot.${vatID}.${position}`, where `position` is the latest delivery (eg highest delivery number) that was included in the heap state captured by the snapshot. The value is a JSON-serialized record of `{ vatID, snapPos, hash, inUse }`.
|
|
22
|
+
|
|
23
|
+
If there is a "current" snapshot, there will be one additional export-data record, whose name is `snapshot.${vatID}.current`, and whose value is `snapshot.${vatID}.${position}`. This value is the same as the name of the latest export-data record, and is meant as a convenient pointer to find that latest snapshot.
|
|
24
|
+
|
|
25
|
+
The export *artifacts* will generally only include the current snapshot for each vat. Only the `debug` mode will include historical snapshots (and only if the swingstore was retaining them in the first place).
|
|
26
|
+
|
|
27
|
+
## Slow Deletion
|
|
28
|
+
|
|
29
|
+
As soon as a vat is terminated, the kernel will call `snapStore.stopUsingLastSnapshot()`. The DB is updated to clear the `inUse` flag of the latest snapshot, leaving no rows with `inUse = 1`. This immediately makes the vat non-loadable by the kernel. The snapshot data itself is deleted (unless `keepSnapshots: true`).
|
|
30
|
+
|
|
31
|
+
This also modifies the latest `snapshot.${vatID}.${snapPos}` export-data record, to change `inUse` to 0, and removes the `snapshot.${vatID}.current` record. The modification and deletion are added to the export-data callback queue, so the host-app can learn about them after the next commit. Any subsequent `getExportData()` calls will observe the changes.
|
|
32
|
+
|
|
33
|
+
As a result, all non-`debug` swing-store exports after this point will omit any artifacts for the snapshot blob, but they will still include export-data records (hashes) for all snapshots. (Deleting all the export-data records is too much work to do in a single step, so it is spread out over time).
|
|
34
|
+
|
|
35
|
+
Later, as the kernel performs cleanup work for this vatID, the cleanup call will delete DB rows (one per `budget`). Each row deleted will also remove one export-data record, which feeds the callback queue, as well as affecting the full `getExportData()` results.
|
|
36
|
+
|
|
37
|
+
Eventually, the snapStore runs out of rows to delete, and `deleteVatSnapshots(budget)` returns `{ done: true }`, so the kernel can finally rest.
|
|
38
|
+
|
|
39
|
+
### SnapStore Vat Lifetime
|
|
40
|
+
|
|
41
|
+
The SnapStore doesn't provide an explicit API to call when a vat is first created. The kernel just calls `saveSnapshot()` for both the first and all subsequent snapshots. Each `saveSnapshot()` marks the previous snapshot as unused, so there is at most one `inUse = 1` snapshot at any time. There will be zero in-use snapshots just after each incarnation starts, until enough deliveries have been made to trigger the first snapshot.
|
|
42
|
+
|
|
43
|
+
When terminating a vat, the kernel should first call `snapStore.stopUsingLastSnapshot(vatID)`, the same call it would make at the end of an incarnation, to indicate that we're no longer using the last snapshot. This results in zero in-use snapshots.
|
|
44
|
+
|
|
45
|
+
Then, the kernel must either call `snapStore.deleteVatSnapshots(vatID)` or `deleteVatSnapshots(vatID, Infinity)` to delete everything at once, or make a series of calls (spread out over time/blocks) to `snapStore.deleteVatSnapshots(vatID, budget)`. Each will return `{ done, cleanups }`, which can be used to manage the rate-limiting and know when the process is finished.
|
|
46
|
+
|
|
47
|
+
The `stopUsingLastSnapshot()` is a performance improvement, but is not mandatory. If omitted, exports will continue to include the vat's snapshot artifacts until the first call to `deleteVatSnapshots()`, after which they will go away. Snapshots are deleted in descending `snapPos` order, so the first call will delete the only `inUse = 1` snapshot, after which exports will omit all artifacts for the vatID. `stopUsingLastSnapshot()` is idempotent, and extra calls will leave the DB unchanged.
|
|
48
|
+
|
|
49
|
+
The kernel must keep calling `deleteVatSnapshots(vatID, budget)` until the `{ done }` return value is `true`. It is safe to call it again after that point; the function will keep returning `true`. But note, this costs one DB txn, so it may be cheaper for the kernel to somehow remember that we've reached the end.
|
package/docs/swingstore.md
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
|
-
# SwingStore
|
|
1
|
+
# The SwingStore
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
The state
|
|
5
|
+
The entire durable state of the kernel lives in the SwingStore: it does not use any other files or databases, and the only commit point is in `hostStorage.commit()`. Careful host applications can use this to avoid "hangover inconsistency", by storing all device output messages in the same database, and only releasing them once the kernel changes have been committed.
|
|
6
|
+
|
|
7
|
+
In theory, an alternate implementation of this API could be provided with e.g. a different backend database, such as the host application's own native database (eg IAVL, for cosmos-sdk -based apps). This could simplify the atomicity domains by using just one database instead of two. This must be balanced against performance tradeoffs: swing-store takes advantage of SQL's indexing and iteration abilities, which might not be present in the other database.
|
|
8
|
+
|
|
9
|
+
## Creating and Opening a SwingStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
`initSwingStore(dirPath, options)` will create a new swingstore in the given directory, which will be created if it doesn't already exist. The entire directory is reserved for the swingstore: the host application should not put any other files there. The swingstore library will populated it with the SQLite DB's backing files: `swingstore.sqlite`, `swingstore.sqlite-wal`, and `swingstore.sqlite-shm`. If called on a directory that already contains a database, the DB will be erased first.
|
|
13
|
+
|
|
14
|
+
`openSwingStore(dirPath, options)` does the same, but will not erase a pre-existing DB. In general, use `initSwingStore` for the initial creation of the DB, and `openSwingStore` for all subsequent access.
|
|
15
|
+
|
|
16
|
+
Both calls return a record with `{ hostStorage, kernelStorage }`, along with some additional facets for testing and debugging. `dirPath` can be null to use a ephemeral (in-memory) DB, which is only useful for unit tests.
|
|
17
|
+
|
|
18
|
+
## HostStorage
|
|
19
|
+
|
|
20
|
+
The `hostStorage` facet is reserved for the host application. It is mostly used to manage commit points for the application.
|
|
21
|
+
|
|
22
|
+
The host is responsible for calling `hostStorage.commit()` when it is done with kernel execution. This causes a SQLite `COMMIT` of the underlying database. It should perform this commit before it releases any device output messages. This facet is the only one with a `commit()` method: the kernel is explicitly unable to commit its own changes to the underlying SQLite database, because the kernel does not know anything about the host's application lifecycle or input/output activity, so it cannot know what qualifies as a safe commit point.
|
|
23
|
+
|
|
24
|
+
If, for some reason, the host wants to abandon execution, it can call `hostStorage.close()`, which will close the swingstore without committing any changes. This is not normally useful: the kernel must be abandoned at this point too, so most of the time the host application should just exit entirely.
|
|
25
|
+
|
|
26
|
+
`hostStorage.kvStore` is also available to let the host add items to a separate portion of the kvStore, using keys which start with a `host.` prefix. It can use this to coordinate with a separately-committed host database (e.g. to remember how much work has been given to the kernel, and how much has been successfully executed). This portion of the kvStore is unreachable by the kernel.
|
|
27
|
+
|
|
28
|
+
`hostStorage.setExportCallback()` is used to register an export callback after swingstore creation, see [data-export.md](./data-export.md) for details. Most applications will instead provide `options.exportCallback` to `openSwingStore()`.
|
|
29
|
+
|
|
30
|
+
`hostStorage.repairMetadata()` was used to repair a historical flaw in the database format, and is not needed by new installations.
|
|
31
|
+
|
|
32
|
+
## KernelStorage
|
|
33
|
+
|
|
34
|
+
The host application is supposed to deliver the `kernelStorage` facet to the kernel, by passing it into `initializeSwingset()`, `upgradeSwingset()`, and `buildVatController()`. The host application should not use `kernelStorage` itself.
|
|
35
|
+
|
|
36
|
+
The kernel receives a facet named `kernelStorage`, from which it can access four sub-stores:
|
|
37
|
+
|
|
38
|
+
* [`bundleStore`](./bundlestore.md): a string-keyed Bundle-value table, holding source bundles which can be evaluated by `importBundle` to create vats, or new Compartments within a vat
|
|
39
|
+
* [`transcriptStore`](./transcriptstore.md): records a linear sequence of deliveries and syscalls (with results), collectively known as "transcript entries", for each vat
|
|
40
|
+
* [`snapStore`](./snapstore.md): records one or more XS heap snapshots for each vat, to rebuild a worker more efficiently than replaying all transcript entries from the beginning
|
|
41
|
+
* [`kvStore`](./kvstore.md): 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.
|
|
42
|
+
|
|
43
|
+
These pieces operate independently: data in one substore does not affect the operation of the others.
|
|
44
|
+
|
|
45
|
+
`kernelStorage` also provides access to the "crank" tools. Kernel execution proceeds in a series of steps named "cranks", many of which involve delivering a message to a vat worker. Sometimes these messages cause a failure halfway through the delivery, where it is better to record either complete deliveries or nothing at all. To support this, the kernel can mark the beginning of the crank (by calling `kernelStorage.startCrank()`), and then either discard the changes (`rollbackCrank()`) or accept them (`endCrank()`). The `emitCrankHashes()` method rotates the crankhash and updates the activityhash (see the kvStore documentation for details).
|
|
46
|
+
|
|
47
|
+
Note that `endCrank()` does *not* perform a SQLite `COMMIT`, as that power is reserved for the host application (through `hostStorage.commit()`). Instead, the kernel only has access to SQLite "savepoints", which are smaller-scale than full transactions.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# SwingStore Data Model
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
The state is broken up into several pieces, or "sub-stores":
|
|
6
54
|
|
|
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
55
|
|
|
12
56
|
## Incarnations, Spans, Snapshots
|
|
13
57
|
|
|
@@ -50,3 +94,15 @@ When a transcript span is pruned, the `transcriptSpans` row is left alone, but t
|
|
|
50
94
|
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
95
|
|
|
52
96
|
Bundles are never pruned, however during import, the `bundles` table will temporarily contain rows whose `bundle` BLOB is NULL.
|
|
97
|
+
|
|
98
|
+
## Vat Lifetimes
|
|
99
|
+
|
|
100
|
+
Two sub-stores are keyed by VatID: `transcriptStore` and `snapStore` (the `bundleStore` does not know which vats might know about each bundle, and the `kvStore` entries which relate to a specific vat will have the VatID embedded in the key, so the swing-store doesn't need to know about them).
|
|
101
|
+
|
|
102
|
+
When the kernel terminates a vat, we want to delete the no-longer-necessary data. However, if the vat had a large number of transcript entries and/or heap snapshots, deleting all this data at the same time might cause excessing CPU or I/O usage (eg thousands of DB queries, or a multi-gigabyte `swingstore.sqlite-wal` file. It might also push a large number of changes into the export-data callbacks, which can cause memory or CPU stall problems in the host application. In the worst case, the entire application could crash.
|
|
103
|
+
|
|
104
|
+
To limit this usage, and allow the kernel to delete vat state slowly, the swing-store is somewhat more aware of a vat's lifetime than a mere database should be. In particular, we split the shutdown process into two pieces. "Terminating a vat" happens first, and tells the sub-store to hide the vat from exports and from API calls that are meant to find out which vats are available. The kernel should call this exactly once, when the vat is terminated.
|
|
105
|
+
|
|
106
|
+
The second part is "deletion", and it can happen either all-at-once or in multiple budget-limited calls. Both forms share the same API calls, differing only in their `budget` argument (`undefined` means all-at-once). The deletion API can be called multiple times, with a small budget, and each call will only delete a small portion of the state. They will return a value that indicates when the last bit of state has been deleted, so the kernel can know when to stop calling them.
|
|
107
|
+
|
|
108
|
+
See [transcriptstore.md](./transcriptstore.md) and [snapstore.md](./snapstore.md) for more details.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# TranscriptStore
|
|
2
|
+
|
|
3
|
+
The `kernelStorage.transcriptStore` sub-store tracks vat delivery transcripts, through which the kernel can provide orthogonal persistence of JavaScript runtime environments (vats).
|
|
4
|
+
|
|
5
|
+
Each vat is a JavaScript runtime environment, initialized by evaluating some starting code bundle, and then fed a series of deliveries. Each delivery may provoke some number of syscalls back to the kernel, with each get some response data. The delivery finishes with a "delivery result".
|
|
6
|
+
|
|
7
|
+
For each delivery, this data (delivery, syscall/response pairs, delivery-result) is serialized and stored in a single "transcript item". Each item is indexed by an incrementing "delivery number" (`deliveryNum`).
|
|
8
|
+
|
|
9
|
+
When a vat worker is brought online, the kernel retrieves these transcript items from the transcriptStore and replays them, by performing the delivery and responding to the syscalls, even though the syscall responses are pulled from the transcript instead of causing actual execution. The kernel asserts that the new worker behaves exactly like the original one did. For xsnap workers, the kernel doesn't actually have to replay the *entire* transcript, because it can start from a heap snapshot (stored in the adjoining [`snapStore`](./snapstore.md)). So generally it only needs to replay a single span.
|
|
10
|
+
|
|
11
|
+
## Data Model
|
|
12
|
+
|
|
13
|
+
Vat lifetimes are broken up into "incarnations", separated by upgrade events. Within each incarnation, the transcript is broken up into "spans", separated by heap-snapshot cycles. To end a span, the kernel records the worker's heap snapshot, then "closes" the old span, and opens a new one.
|
|
14
|
+
|
|
15
|
+
This results in a single open or "current" span for each active vat, and a series of historical spans. For operational purposes, we only care about the current span. But to support some potential deep-replay needs, the transcriptStore can retain data about earlier spans.
|
|
16
|
+
|
|
17
|
+
The SQLite database has one table that tracks transcript spans, named `transcriptSpans`. All vatIDs and incarnations are stored in the same table, whose schema is `(vatID TEXT, startPos INTEGER, endPos INTEGER, hash TEXT, isCurrent INTEGER, incarnation INTEGER)`. `startPos` and `endPos` define a zero-based range over the sequence of all deliveries into a vat (the former inclusive and the latter exclusive, such that e.g. `startPos=0` and `endPos=3` would encompass the first three deliveries, with positions 0, 1, and 2).
|
|
18
|
+
|
|
19
|
+
A separate table named `transcriptItems` tracks the items themselves, with a schema of `(vatID TEXT, position INTEGER, item TEXT, incarnation INTEGER)`. This table has one row per transcript item, each of which is "owned" by a single span with matching values for `vatID` and `incarnation` and having `startPos <= position` and `endPos > position`. Each span owns multiple items (typically 200, but it depends upon how frequently the kernel rolls over new spans).
|
|
20
|
+
|
|
21
|
+
In the future, historical spans may be compressed, and their item rows replaced with a single compressed blob in the span record. This would reduce the space needed without actually pruning the data.
|
|
22
|
+
|
|
23
|
+
## Retention / Pruning
|
|
24
|
+
|
|
25
|
+
If the current swingstore was opened with the `keepTranscripts = false` option, then the transcriptStore will "prune" each span as soon as it becomes historical. Pruned spans will still have a span record, with a hash, to enable safely-validated restoration of the transcript items later, if necessary. However their item records will be deleted, to save space.
|
|
26
|
+
|
|
27
|
+
When `keepTranscripts = true`, all span items are retained.
|
|
28
|
+
|
|
29
|
+
Pruned spans are not available for export artifacts, of course, because the data is missing. However the span *hashes* are still included in the export-data, to support safe validation. You can start with a pruned swingstore, produce an export dataset, import that dataset into a new swingstore, and the new swingstore will be just as capable of validating replacement span records as the original was.
|
|
30
|
+
|
|
31
|
+
## Export Model
|
|
32
|
+
|
|
33
|
+
Every transcript span, both current and historic, gets an export-data record. The record name is different for the two types of spans.
|
|
34
|
+
|
|
35
|
+
Historical spans, which are "closed" and no longer growing, use a record name of `transcript.${vatID}.${startPos}.${endPos}`, where `startPos` is the delivery number of the first delivery included in the span and `endPos` is the the delivery number of the first delivery included in the **next** span (i.e., the former is an inclusive lower bound and the latter is an exclusive upper bound).
|
|
36
|
+
The value is a JSON-serialized record of `{ vatID, startPos, endPos, hash, isCurrent, incarnation }` (where `isCurrent = 0`).
|
|
37
|
+
|
|
38
|
+
The current span, if any, uses a record name of `transcript.${vatID}.current`, and has the same value as historical spans (except `isCurrent = 1`). Current spans are growing: new transcript items are added as more deliveries are made, until the span is closed off (becomes historical) and replaced with a new current span. There is at most one current span per vatID.
|
|
39
|
+
|
|
40
|
+
The available export *artifacts* will depend upon the export mode, and upon the swingstore's `keepTranscripts` setting. Each export artifact corresponds to a single span, and the artifact names are always `transcript.${vatID}.${startPos}.${endPos}` (for both historical and current spans).
|
|
41
|
+
|
|
42
|
+
In the most-minimal `operational` mode, the export includes one artifact for each active (non-terminated) vat: just the current span. If `keepTranscripts` is not true, these will be the only available artifacts anyways.
|
|
43
|
+
|
|
44
|
+
The `replay` mode includes all spans for each vat's current incarnation, but omits spans from earlier incarnations. The `archival` mode includes all spans from all incarnations.
|
|
45
|
+
|
|
46
|
+
The `debug` mode includes all available spans, even for terminated vats. For the non-`debug` modes, terminated vats will not provide export-data or artifacts.
|
|
47
|
+
|
|
48
|
+
## Slow Deletion
|
|
49
|
+
|
|
50
|
+
As soon as a vat is terminated, the kernel will call `transcriptStore.stopUsingTranscript()`. The DB is updated to clear the `isCurrent` flag of the latest span, leaving no rows with `isCurrent = 1`. This immediately makes the vat non-loadable by the kernel.
|
|
51
|
+
|
|
52
|
+
This also removes the `transcript.${vatID}.current` export-data record, and replaces it with a `transcript.${vatID}.${startPos}` one, effectively making the span historical. This change (one deletion, one addition) is added to the export-data callback queue, so the host-app can learn about it after the next commit, and any subsequent `getExportData()` calls will see the replacement record, instead of a `.current` record.
|
|
53
|
+
|
|
54
|
+
At this point, all non-`debug` swing-store exports after this point will omit any artifacts for the vat, but they will still include export-data records (hashes) for all spans, all of which look historical. (Deleting all the span records, and their corresponding export-data records, is too much work to do in a single step).
|
|
55
|
+
|
|
56
|
+
Later, as the kernel performs cleanup work for this vatID, the `transcriptStore.deleteVatTranscripts(budget)` cleanup call will delete one span row per `budget`, along with all related item rows (typically 200). Each span deleted will also remove one export-data record (which feeds the callback queue, as well as affecting the full `getExportData()` results).
|
|
57
|
+
|
|
58
|
+
Eventually, the transcriptStore runs out of rows to delete, and `deleteVatTranscripts(budget)` returns `{ done: true }`, so the kernel can finally rest.
|
|
59
|
+
|
|
60
|
+
### TranscriptStore Vat Lifetime
|
|
61
|
+
|
|
62
|
+
Unlike the [SnapStore](./snapstore.md), the TranscriptStore *does* have an explicit call to be made when a vat is first created: `transcriptStore.initTranscript(vatID)`. Also unlike SnapStore, TranscriptStore (normally) always has an `isCurrent = 1` span for each vat (it might just be empty of items, immediately after the span rolls over).
|
|
63
|
+
|
|
64
|
+
When a vat is terminated, the kernel should first call `transcriptStore.stopUsingTranscript(vatID)`. This will mark the single current span as `isCurrent = 0`. The kernel must not attempt to read, add, or rollover spans or items while in this state. While in this state, exports (export for `mode = debug`) will not emit artifacts for this VatID: export-data records will still exist for all spans, as these must be deleted slowly, however there will be no associated artifacts or artifact names.
|
|
65
|
+
|
|
66
|
+
Then, the kernel should either call `transcriptStore.deleteVatTranscripts(vatID)` exactly once, or it should call `transcriptStore.deleteVatTranscripts(vatID, budget)` until it returns `{ done: true }`.
|
|
67
|
+
|
|
68
|
+
As with snapshots, the `stopUsingTranscript()` is a non-mandatory performance improvement. If omitted, exports will continue to include (many) span artifacts for this vat until the first call to `deleteVatTranscripts()` removes the one `isCurrent = 1` span (since spans are deleted most-recent-first). After that point, exports will stop including any artifacts for the vatID. `stopUsingTranscript()` is idempotent, and extra calls will leave the DB unchanged.
|
|
69
|
+
|
|
70
|
+
The kernel must keep calling `deleteVatTranscripts(vatID, budget)` until the `{ done }` return value is `true`. As with the SnapStore, it is safe to call it again after that point; the function will keep returning `true`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/swing-store",
|
|
3
|
-
"version": "0.9.2-
|
|
3
|
+
"version": "0.9.2-u17.0",
|
|
4
4
|
"description": "Persistent storage for SwingSet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
"lint:eslint": "eslint ."
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@agoric/
|
|
25
|
-
"@
|
|
26
|
-
"@endo/
|
|
27
|
-
"@endo/bundle
|
|
28
|
-
"@endo/
|
|
29
|
-
"@endo/nat": "^5.0.
|
|
24
|
+
"@agoric/internal": "^0.4.0-u17.0",
|
|
25
|
+
"@endo/base64": "^1.0.7",
|
|
26
|
+
"@endo/bundle-source": "^3.4.0",
|
|
27
|
+
"@endo/check-bundle": "^1.0.9",
|
|
28
|
+
"@endo/errors": "^1.2.5",
|
|
29
|
+
"@endo/nat": "^5.0.10",
|
|
30
30
|
"better-sqlite3": "^9.1.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@endo/init": "^1.1.
|
|
33
|
+
"@endo/init": "^1.1.4",
|
|
34
34
|
"@types/better-sqlite3": "^7.6.9",
|
|
35
35
|
"ava": "^5.3.0",
|
|
36
36
|
"c8": "^9.1.0",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"timeout": "2m"
|
|
50
50
|
},
|
|
51
51
|
"typeCoverage": {
|
|
52
|
-
"atLeast":
|
|
52
|
+
"atLeast": 78.77
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "515c4c0efccfc91b97da30037c10fc4b076851e2"
|
|
55
55
|
}
|
package/src/archiver.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { finished as streamFinishedCallback, Readable } from 'node:stream';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
import { createGzip } from 'node:zlib';
|
|
4
|
+
import { withDeferredCleanup } from '@agoric/internal';
|
|
5
|
+
|
|
6
|
+
const streamFinished = promisify(streamFinishedCallback);
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* @param {string} dirPath
|
|
10
|
+
* @param {object} powers
|
|
11
|
+
* @param {Pick<import('fs'), 'createWriteStream' | 'mkdirSync' | 'renameSync'>} powers.fs
|
|
12
|
+
* @param {Pick<import('path'), 'join'>} powers.path
|
|
13
|
+
* @param {Pick<import('tmp'), 'fileSync'>} powers.tmp
|
|
14
|
+
*/
|
|
15
|
+
export const makeArchiveSnapshot = (dirPath, powers) => {
|
|
16
|
+
const { fs, path, tmp } = powers;
|
|
17
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
18
|
+
const archiveSnapshot = (name, gzData) => {
|
|
19
|
+
const destPath = path.join(dirPath, `${name}.gz`);
|
|
20
|
+
return withDeferredCleanup(async addCleanup => {
|
|
21
|
+
const {
|
|
22
|
+
name: tmpName,
|
|
23
|
+
fd,
|
|
24
|
+
removeCallback,
|
|
25
|
+
} = tmp.fileSync({
|
|
26
|
+
prefix: name,
|
|
27
|
+
postfix: '.gz',
|
|
28
|
+
detachDescriptor: true,
|
|
29
|
+
});
|
|
30
|
+
addCleanup(() => removeCallback());
|
|
31
|
+
const writer = fs.createWriteStream('', { fd, flush: true });
|
|
32
|
+
const reader = Readable.from(gzData);
|
|
33
|
+
const destroyReader = promisify(reader.destroy.bind(reader));
|
|
34
|
+
addCleanup(() => destroyReader(null));
|
|
35
|
+
reader.pipe(writer);
|
|
36
|
+
await streamFinished(writer);
|
|
37
|
+
fs.renameSync(tmpName, destPath);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
return archiveSnapshot;
|
|
41
|
+
};
|
|
42
|
+
harden(makeArchiveSnapshot);
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* @param {string} dirPath
|
|
46
|
+
* @param {object} powers
|
|
47
|
+
* @param {Pick<import('fs'), 'createWriteStream' | 'mkdirSync' | 'renameSync'>} powers.fs
|
|
48
|
+
* @param {Pick<import('path'), 'join'>} powers.path
|
|
49
|
+
* @param {Pick<import('tmp'), 'fileSync'>} powers.tmp
|
|
50
|
+
*/
|
|
51
|
+
export const makeArchiveTranscript = (dirPath, powers) => {
|
|
52
|
+
const { fs, path, tmp } = powers;
|
|
53
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
54
|
+
const archiveTranscript = (spanName, entries) => {
|
|
55
|
+
const destPath = path.join(dirPath, `${spanName}.gz`);
|
|
56
|
+
return withDeferredCleanup(async addCleanup => {
|
|
57
|
+
const {
|
|
58
|
+
name: tmpName,
|
|
59
|
+
fd,
|
|
60
|
+
removeCallback,
|
|
61
|
+
} = tmp.fileSync({
|
|
62
|
+
prefix: spanName,
|
|
63
|
+
postfix: '.gz',
|
|
64
|
+
detachDescriptor: true,
|
|
65
|
+
});
|
|
66
|
+
addCleanup(() => removeCallback());
|
|
67
|
+
const writer = fs.createWriteStream('', { fd, flush: true });
|
|
68
|
+
const gzip = createGzip();
|
|
69
|
+
gzip.pipe(writer);
|
|
70
|
+
const reader = Readable.from(entries);
|
|
71
|
+
const destroyReader = promisify(reader.destroy.bind(reader));
|
|
72
|
+
addCleanup(() => destroyReader(null));
|
|
73
|
+
reader.pipe(gzip);
|
|
74
|
+
await streamFinished(gzip);
|
|
75
|
+
fs.renameSync(tmpName, destPath);
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
return archiveTranscript;
|
|
79
|
+
};
|
|
80
|
+
harden(makeArchiveTranscript);
|
package/src/bundleStore.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { Readable } from 'stream';
|
|
4
4
|
import { Buffer } from 'buffer';
|
|
5
|
+
import { Fail, q } from '@endo/errors';
|
|
5
6
|
import { encodeBase64, decodeBase64 } from '@endo/base64';
|
|
6
7
|
import { checkBundle } from '@endo/check-bundle/lite.js';
|
|
7
8
|
import { Nat } from '@endo/nat';
|
|
8
|
-
import { Fail, q } from '@agoric/assert';
|
|
9
9
|
import { createSHA256 } from './hasher.js';
|
|
10
10
|
|
|
11
11
|
/**
|
package/src/exporter.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sqlite3 from 'better-sqlite3';
|
|
2
2
|
|
|
3
|
-
import { Fail, q } from '@
|
|
3
|
+
import { Fail, q } from '@endo/errors';
|
|
4
4
|
|
|
5
5
|
import { dbFileInDirectory } from './util.js';
|
|
6
6
|
import { getKeyType } from './kvStore.js';
|
|
@@ -163,23 +163,23 @@ export function makeSwingStoreExporter(dirPath, options = {}) {
|
|
|
163
163
|
harden(getExportData);
|
|
164
164
|
|
|
165
165
|
/** @yields {string} */
|
|
166
|
-
async function*
|
|
166
|
+
async function* generateArtifactNames() {
|
|
167
167
|
yield* snapStore.getArtifactNames(artifactMode);
|
|
168
168
|
yield* transcriptStore.getArtifactNames(artifactMode);
|
|
169
169
|
yield* bundleStore.getArtifactNames();
|
|
170
170
|
}
|
|
171
|
-
harden(
|
|
171
|
+
harden(generateArtifactNames);
|
|
172
172
|
|
|
173
173
|
/**
|
|
174
174
|
* @returns {AsyncIterableIterator<string>}
|
|
175
175
|
*/
|
|
176
176
|
function getArtifactNames() {
|
|
177
177
|
if (artifactMode !== 'debug') {
|
|
178
|
-
// throw if this DB will not be able to yield all the desired artifacts
|
|
178
|
+
// synchronously throw if this DB will not be able to yield all the desired artifacts
|
|
179
179
|
const internal = { snapStore, bundleStore, transcriptStore };
|
|
180
180
|
assertComplete(internal, artifactMode);
|
|
181
181
|
}
|
|
182
|
-
return
|
|
182
|
+
return generateArtifactNames();
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
/**
|
package/src/hasher.js
CHANGED
package/src/importer.js
CHANGED
package/src/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export { initSwingStore, openSwingStore, isSwingStore } from './swingStore.js';
|
|
|
2
2
|
export { makeSwingStoreExporter } from './exporter.js';
|
|
3
3
|
export { importSwingStore } from './importer.js';
|
|
4
4
|
|
|
5
|
+
export { makeArchiveSnapshot, makeArchiveTranscript } from './archiver.js';
|
|
6
|
+
|
|
5
7
|
// temporary, for the benefit of SwingSet/misc-tools/replay-transcript.js
|
|
6
8
|
export { makeSnapStore } from './snapStore.js';
|
|
7
9
|
// and less temporary, for SwingSet/test/vat-warehouse/test-reload-snapshot.js
|
package/src/internal.js
CHANGED
package/src/kvStore.js
CHANGED
package/src/repairMetadata.js
CHANGED