@agoric/swing-store 0.9.2-dev-ad1e50e.0 → 0.9.2-dev-0eaf7cc.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.
@@ -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
+
@@ -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, undefined)` 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.
@@ -1,13 +1,57 @@
1
- # SwingStore Data Model
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 is broken up into several pieces, or "stores":
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
36
+ `transcript.${vatID}.${startPos}`, where `startPos` is the delivery number of the first delivery included in the span. 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 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, undefined)` 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-dev-ad1e50e.0+ad1e50e",
3
+ "version": "0.9.2-dev-0eaf7cc.0+0eaf7cc",
4
4
  "description": "Persistent storage for SwingSet",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -21,7 +21,7 @@
21
21
  "lint:eslint": "eslint ."
22
22
  },
23
23
  "dependencies": {
24
- "@agoric/internal": "0.3.3-dev-ad1e50e.0+ad1e50e",
24
+ "@agoric/internal": "0.3.3-dev-0eaf7cc.0+0eaf7cc",
25
25
  "@endo/base64": "^1.0.6",
26
26
  "@endo/bundle-source": "^3.3.0",
27
27
  "@endo/check-bundle": "^1.0.8",
@@ -51,5 +51,5 @@
51
51
  "typeCoverage": {
52
52
  "atLeast": 76.31
53
53
  },
54
- "gitHead": "ad1e50e1abf378cc5421da7144c7f11fb3fde45f"
54
+ "gitHead": "0eaf7cc8151b4e1966f3f33e43362d3d021457aa"
55
55
  }
package/src/snapStore.js CHANGED
@@ -39,7 +39,7 @@ import { buffer } from './util.js';
39
39
  * loadSnapshot: (vatID: string) => AsyncIterableIterator<Uint8Array>,
40
40
  * saveSnapshot: (vatID: string, snapPos: number, snapshotStream: AsyncIterable<Uint8Array>) => Promise<SnapshotResult>,
41
41
  * deleteAllUnusedSnapshots: () => void,
42
- * deleteVatSnapshots: (vatID: string) => void,
42
+ * deleteVatSnapshots: (vatID: string, budget?: number) => { done: boolean, cleanups: number },
43
43
  * stopUsingLastSnapshot: (vatID: string) => void,
44
44
  * getSnapshotInfo: (vatID: string) => SnapshotInfo,
45
45
  * }} SnapStore
@@ -173,11 +173,13 @@ export function makeSnapStore(
173
173
  `);
174
174
 
175
175
  function stopUsingLastSnapshot(vatID) {
176
+ // idempotent
176
177
  ensureTxn();
177
178
  const oldInfo = sqlGetPriorSnapshotInfo.get(vatID);
178
179
  if (oldInfo) {
179
180
  const rec = snapshotRec(vatID, oldInfo.snapPos, oldInfo.hash, 0);
180
181
  noteExport(snapshotMetadataKey(rec), JSON.stringify(rec));
182
+ noteExport(currentSnapshotMetadataKey(rec), undefined);
181
183
  if (keepSnapshots) {
182
184
  sqlStopUsingLastSnapshot.run(vatID);
183
185
  } else {
@@ -354,28 +356,74 @@ export function makeSnapStore(
354
356
  WHERE vatID = ?
355
357
  `);
356
358
 
359
+ const sqlDeleteOneVatSnapshot = db.prepare(`
360
+ DELETE FROM snapshots
361
+ WHERE vatID = ? AND snapPos = ?
362
+ `);
363
+
357
364
  const sqlGetSnapshotList = db.prepare(`
358
365
  SELECT snapPos
359
366
  FROM snapshots
360
367
  WHERE vatID = ?
361
368
  ORDER BY snapPos
362
369
  `);
363
- sqlGetSnapshotList.pluck(true);
370
+
371
+ const sqlGetSnapshotListLimited = db.prepare(`
372
+ SELECT snapPos, inUse
373
+ FROM snapshots
374
+ WHERE vatID = ?
375
+ ORDER BY snapPos DESC
376
+ LIMIT ?
377
+ `);
364
378
 
365
379
  /**
366
- * Delete all snapshots for a given vat (for use when, e.g., a vat is terminated)
380
+ * @param {string} vatID
381
+ * @returns {boolean}
382
+ */
383
+ function hasSnapshots(vatID) {
384
+ // the LIMIT 1 means we aren't really getting all entries
385
+ return sqlGetSnapshotListLimited.all(vatID, 1).length > 0;
386
+ }
387
+
388
+ /**
389
+ * Delete some or all snapshots for a given vat (for use when, e.g.,
390
+ * a vat is terminated)
367
391
  *
368
392
  * @param {string} vatID
393
+ * @param {number} [budget]
394
+ * @returns {{ done: boolean, cleanups: number }}
369
395
  */
370
- function deleteVatSnapshots(vatID) {
396
+ function deleteVatSnapshots(vatID, budget = undefined) {
371
397
  ensureTxn();
372
- const deletions = sqlGetSnapshotList.all(vatID);
373
- for (const snapPos of deletions) {
398
+ const deleteAll = budget === undefined;
399
+ assert(deleteAll || budget >= 1, 'budget must be undefined or positive');
400
+ // We can't use .iterate because noteExport can write to the DB,
401
+ // and overlapping queries are not supported.
402
+ const deletions = deleteAll
403
+ ? sqlGetSnapshotList.all(vatID)
404
+ : sqlGetSnapshotListLimited.all(vatID, budget);
405
+ let clearCurrent = deleteAll;
406
+ for (const deletion of deletions) {
407
+ clearCurrent ||= deletion.inUse;
408
+ const { snapPos } = deletion;
374
409
  const exportRec = snapshotRec(vatID, snapPos, undefined);
375
410
  noteExport(snapshotMetadataKey(exportRec), undefined);
411
+ // Budgeted deletion must delete rows one by one,
412
+ // but full deletion is handled all at once after this loop.
413
+ if (!deleteAll) {
414
+ sqlDeleteOneVatSnapshot.run(vatID, snapPos);
415
+ }
416
+ }
417
+ if (deleteAll) {
418
+ sqlDeleteVatSnapshots.run(vatID);
419
+ }
420
+ if (clearCurrent) {
421
+ noteExport(currentSnapshotMetadataKey({ vatID }), undefined);
376
422
  }
377
- noteExport(currentSnapshotMetadataKey({ vatID }), undefined);
378
- sqlDeleteVatSnapshots.run(vatID);
423
+ return {
424
+ done: deleteAll || deletions.length === 0 || !hasSnapshots(vatID),
425
+ cleanups: deletions.length,
426
+ };
379
427
  }
380
428
 
381
429
  const sqlGetSnapshotInfo = db.prepare(`
@@ -452,7 +500,7 @@ export function makeSnapStore(
452
500
  `);
453
501
 
454
502
  /**
455
- * Obtain artifact metadata records for spanshots contained in this store.
503
+ * Obtain artifact metadata records for snapshots contained in this store.
456
504
  *
457
505
  * @param {boolean} includeHistorical If true, include all metadata that is
458
506
  * present in the store regardless of its currency; if false, only include
package/src/swingStore.js CHANGED
@@ -554,6 +554,7 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
554
554
  getCurrentSpanBounds: transcriptStore.getCurrentSpanBounds,
555
555
  addItem: transcriptStore.addItem,
556
556
  readSpan: transcriptStore.readSpan,
557
+ stopUsingTranscript: transcriptStore.stopUsingTranscript,
557
558
  deleteVatTranscripts: transcriptStore.deleteVatTranscripts,
558
559
  };
559
560
 
@@ -18,7 +18,8 @@ import { createSHA256 } from './hasher.js';
18
18
  * rolloverSpan: (vatID: string) => number,
19
19
  * rolloverIncarnation: (vatID: string) => number,
20
20
  * getCurrentSpanBounds: (vatID: string) => { startPos: number, endPos: number, hash: string, incarnation: number },
21
- * deleteVatTranscripts: (vatID: string) => void,
21
+ * stopUsingTranscript: (vatID: string) => void,
22
+ * deleteVatTranscripts: (vatID: string, budget?: number) => { done: boolean, cleanups: number },
22
23
  * addItem: (vatID: string, item: string) => void,
23
24
  * readSpan: (vatID: string, startPos?: number) => IterableIterator<string>,
24
25
  * }} TranscriptStore
@@ -214,7 +215,7 @@ export function makeTranscriptStore(
214
215
  ensureTxn();
215
216
  const initialIncarnation = 0;
216
217
  sqlWriteSpan.run(vatID, 0, 0, initialHash, 1, initialIncarnation);
217
- const newRec = spanRec(vatID, 0, 0, initialHash, 1, 0);
218
+ const newRec = spanRec(vatID, 0, 0, initialHash, true, 0);
218
219
  noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
219
220
  }
220
221
 
@@ -251,21 +252,35 @@ export function makeTranscriptStore(
251
252
  function doSpanRollover(vatID, isNewIncarnation) {
252
253
  ensureTxn();
253
254
  const { hash, startPos, endPos, incarnation } = getCurrentSpanBounds(vatID);
254
- const rec = spanRec(vatID, startPos, endPos, hash, 0, incarnation);
255
+ const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
256
+
257
+ // add a new record for the now-old span
255
258
  noteExport(spanMetadataKey(rec), JSON.stringify(rec));
259
+
260
+ // and change its DB row to isCurrent=0
256
261
  sqlEndCurrentSpan.run(vatID);
262
+
263
+ // create a new (empty) row, with isCurrent=1
257
264
  const incarnationToUse = isNewIncarnation ? incarnation + 1 : incarnation;
258
265
  sqlWriteSpan.run(vatID, endPos, endPos, initialHash, 1, incarnationToUse);
266
+
267
+ // overwrite the transcript.${vatID}.current record with new span
259
268
  const newRec = spanRec(
260
269
  vatID,
261
270
  endPos,
262
271
  endPos,
263
272
  initialHash,
264
- 1,
273
+ true,
265
274
  incarnationToUse,
266
275
  );
267
276
  noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
277
+
268
278
  if (!keepTranscripts) {
279
+ // TODO: for #9174 (delete historical transcript spans), we need
280
+ // this DB statement to only delete the items of the old span
281
+ // (startPos..endPos), not all previous items, otherwise the
282
+ // first rollover after switching to keepTranscripts=false will
283
+ // do a huge DB commit and probably explode
269
284
  sqlDeleteOldItems.run(vatID, endPos);
270
285
  }
271
286
  return incarnationToUse;
@@ -314,19 +329,101 @@ export function makeTranscriptStore(
314
329
  ORDER BY startPos
315
330
  `);
316
331
 
332
+ // This query is ORDER BY startPos DESC, so deleteVatTranscripts
333
+ // will delete the newest spans first. If the kernel failed to call
334
+ // stopUsingTranscript, that will delete the isCurrent=1 span first,
335
+ // which lets us stop including span artifacts in exports sooner.
336
+
337
+ const sqlGetSomeVatSpans = db.prepare(`
338
+ SELECT vatID, startPos, endPos, isCurrent
339
+ FROM transcriptSpans
340
+ WHERE vatID = ?
341
+ ORDER BY startPos DESC
342
+ LIMIT ?
343
+ `);
344
+
345
+ const sqlDeleteVatSpan = db.prepare(`
346
+ DELETE FROM transcriptSpans
347
+ WHERE vatID = ? AND startPos = ?
348
+ `);
349
+
350
+ const sqlDeleteSomeItems = db.prepare(`
351
+ DELETE FROM transcriptItems
352
+ WHERE vatID = ? AND position >= ? AND position < ?
353
+ `);
354
+
355
+ /**
356
+ * Prepare for vat deletion by marking the isCurrent span as not
357
+ * current. Idempotent.
358
+ *
359
+ * @param {string} vatID The vat being terminated/deleted.
360
+ */
361
+ function stopUsingTranscript(vatID) {
362
+ ensureTxn();
363
+ // this transforms the current span into a (short) historical one
364
+ // (basically doSpanRollover without adding replacement data)
365
+ const bounds = sqlGetCurrentSpanBounds.get(vatID);
366
+ if (bounds) {
367
+ // add a new record for the now-old span
368
+ const { startPos, endPos, hash, incarnation } = bounds;
369
+ const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
370
+ noteExport(spanMetadataKey(rec), JSON.stringify(rec));
371
+
372
+ // and change its DB row to isCurrent=0
373
+ sqlEndCurrentSpan.run(vatID);
374
+
375
+ // remove the transcript.${vatID}.current record
376
+ noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
377
+ }
378
+ }
379
+
380
+ /**
381
+ * @param {string} vatID
382
+ * @returns {boolean}
383
+ */
384
+ function hasSpans(vatID) {
385
+ // the LIMIT 1 means we aren't really getting all spans
386
+ return sqlGetSomeVatSpans.all(vatID, 1).length > 0;
387
+ }
388
+
317
389
  /**
318
- * Delete all transcript data for a given vat (for use when, e.g., a vat is terminated)
390
+ * Delete some or all transcript data for a given vat (for use when,
391
+ * e.g., a vat is terminated)
319
392
  *
320
393
  * @param {string} vatID
394
+ * @param {number} [budget]
395
+ * @returns {{ done: boolean, cleanups: number }}
321
396
  */
322
- function deleteVatTranscripts(vatID) {
397
+ function deleteVatTranscripts(vatID, budget = undefined) {
323
398
  ensureTxn();
324
- const deletions = sqlGetVatSpans.all(vatID);
399
+ const deleteAll = budget === undefined;
400
+ assert(deleteAll || budget >= 1, 'budget must be undefined or positive');
401
+ // We can't use .iterate because noteExport can write to the DB,
402
+ // and overlapping queries are not supported.
403
+ const deletions = deleteAll
404
+ ? sqlGetVatSpans.all(vatID)
405
+ : sqlGetSomeVatSpans.all(vatID, budget);
325
406
  for (const rec of deletions) {
407
+ // If rec.isCurrent is true, this will remove the
408
+ // transcript.$vatID.current export-data record. If false, it
409
+ // will remove the transcript.$vatID.$startPos record.
326
410
  noteExport(spanMetadataKey(rec), undefined);
411
+
412
+ // Budgeted deletion must delete rows one by one,
413
+ // but full deletion is handled all at once after this loop.
414
+ if (!deleteAll) {
415
+ sqlDeleteVatSpan.run(vatID, rec.startPos);
416
+ sqlDeleteSomeItems.run(vatID, rec.startPos, rec.endPos);
417
+ }
418
+ }
419
+ if (deleteAll) {
420
+ sqlDeleteVatItems.run(vatID);
421
+ sqlDeleteVatSpans.run(vatID);
327
422
  }
328
- sqlDeleteVatItems.run(vatID);
329
- sqlDeleteVatSpans.run(vatID);
423
+ return {
424
+ done: deleteAll || deletions.length === 0 || !hasSpans(vatID),
425
+ cleanups: deletions.length,
426
+ };
330
427
  }
331
428
 
332
429
  const sqlGetAllSpanMetadata = db.prepare(`
@@ -379,6 +476,12 @@ export function makeTranscriptStore(
379
476
  * The only code path which could use 'false' would be `swingstore.dump()`,
380
477
  * which takes the same flag.
381
478
  *
479
+ * Note that when a vat is terminated and has been partially
480
+ * deleted, we will retain (and return) a subset of the metadata
481
+ * records, because they must be deleted in-consensus and with
482
+ * updates to the noteExport hook. But we don't create any artifacts
483
+ * for the terminated vats, even for the spans that remain,
484
+ *
382
485
  * @yields {readonly [key: string, value: string]}
383
486
  * @returns {IterableIterator<readonly [key: string, value: string]>}
384
487
  * An iterator over pairs of [spanMetadataKey, rec], where `rec` is a
@@ -432,9 +535,16 @@ export function makeTranscriptStore(
432
535
  }
433
536
  }
434
537
  } else if (artifactMode === 'archival') {
435
- // everything
538
+ // every span for all vatIDs that have an isCurrent span (to
539
+ // ignore terminated/partially-deleted vats)
540
+ const vatIDs = new Set();
541
+ for (const { vatID } of sqlGetCurrentSpanMetadata.iterate()) {
542
+ vatIDs.add(vatID);
543
+ }
436
544
  for (const rec of sqlGetAllSpanMetadata.iterate()) {
437
- yield spanArtifactName(rec);
545
+ if (vatIDs.has(rec.vatID)) {
546
+ yield spanArtifactName(rec);
547
+ }
438
548
  }
439
549
  } else if (artifactMode === 'debug') {
440
550
  // everything that is a complete span
@@ -572,7 +682,7 @@ export function makeTranscriptStore(
572
682
  const newEndPos = endPos + 1;
573
683
  const newHash = updateSpanHash(hash, item);
574
684
  sqlUpdateSpan.run(newEndPos, newHash, vatID);
575
- const rec = spanRec(vatID, startPos, newEndPos, newHash, 1, incarnation);
685
+ const rec = spanRec(vatID, startPos, newEndPos, newHash, true, incarnation);
576
686
  noteExport(spanMetadataKey(rec), JSON.stringify(rec));
577
687
  };
578
688
 
@@ -774,6 +884,7 @@ export function makeTranscriptStore(
774
884
  getCurrentSpanBounds,
775
885
  addItem,
776
886
  readSpan,
887
+ stopUsingTranscript,
777
888
  deleteVatTranscripts,
778
889
 
779
890
  exportSpan,
@@ -1,28 +1,50 @@
1
1
  // @ts-check
2
2
  import test from 'ava';
3
+ import path from 'path';
4
+
3
5
  import { Buffer } from 'node:buffer';
6
+ import sqlite3 from 'better-sqlite3';
7
+ import { tmpDir } from './util.js';
4
8
  import { initSwingStore } from '../src/swingStore.js';
9
+ import { makeSwingStoreExporter } from '../src/exporter.js';
10
+ import { importSwingStore } from '../src/importer.js';
5
11
 
6
12
  async function* getSnapshotStream() {
7
13
  yield Buffer.from('abc');
8
14
  }
9
15
  harden(getSnapshotStream);
10
16
 
17
+ // update 'data' with the callback deltas to get a new current
18
+ // export-data record
19
+ const mergeExportDeltas = (data, exports) => {
20
+ for (const [key, value] of exports) {
21
+ if (value) {
22
+ data[key] = value;
23
+ } else {
24
+ delete data[key];
25
+ }
26
+ }
27
+ };
28
+
29
+ const mapToObj = map => Object.fromEntries(map.entries());
30
+
11
31
  test('delete snapshots with export callback', async t => {
12
32
  const exportLog = [];
33
+ const exportData = {};
13
34
  const exportCallback = exports => {
14
35
  for (const [key, value] of exports) {
15
36
  exportLog.push([key, value]);
16
37
  }
38
+ mergeExportDeltas(exportData, exports);
17
39
  };
18
40
  const store = initSwingStore(null, { exportCallback });
19
41
  const { kernelStorage, hostStorage } = store;
20
42
  const { snapStore } = kernelStorage;
21
43
  const { commit } = hostStorage;
22
-
23
- await snapStore.saveSnapshot('v1', 10, getSnapshotStream());
24
- await snapStore.saveSnapshot('v1', 11, getSnapshotStream());
25
- await snapStore.saveSnapshot('v1', 12, getSnapshotStream());
44
+ const vatID = 'v1';
45
+ await snapStore.saveSnapshot(vatID, 10, getSnapshotStream());
46
+ await snapStore.saveSnapshot(vatID, 11, getSnapshotStream());
47
+ await snapStore.saveSnapshot(vatID, 12, getSnapshotStream());
26
48
  // nothing is written to exportCallback until endCrank() or commit()
27
49
  t.deepEqual(exportLog, []);
28
50
 
@@ -33,11 +55,18 @@ test('delete snapshots with export callback', async t => {
33
55
  t.is(exportLog[1][0], 'snapshot.v1.11');
34
56
  t.is(exportLog[2][0], 'snapshot.v1.12');
35
57
  t.is(exportLog[3][0], 'snapshot.v1.current');
58
+ const hash = JSON.parse(exportLog[0][1]).hash;
59
+ t.deepEqual(exportData, {
60
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
61
+ 'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
62
+ 'snapshot.v1.12': JSON.stringify({ vatID, snapPos: 12, hash, inUse: 1 }),
63
+ 'snapshot.v1.current': 'snapshot.v1.12',
64
+ });
36
65
  exportLog.length = 0;
37
66
 
38
67
  // in a previous version, deleteVatSnapshots caused overlapping SQL
39
68
  // queries, and failed
40
- snapStore.deleteVatSnapshots('v1');
69
+ snapStore.deleteVatSnapshots(vatID);
41
70
  await commit();
42
71
 
43
72
  t.deepEqual(exportLog, [
@@ -47,6 +76,7 @@ test('delete snapshots with export callback', async t => {
47
76
  ['snapshot.v1.current', null],
48
77
  ]);
49
78
  exportLog.length = 0;
79
+ t.deepEqual(exportData, {});
50
80
  });
51
81
 
52
82
  test('delete transcripts with export callback', async t => {
@@ -91,3 +121,546 @@ test('delete transcripts with export callback', async t => {
91
121
 
92
122
  exportLog.length = 0;
93
123
  });
124
+
125
+ const getExport = async (dbDir, artifactMode) => {
126
+ const exporter = makeSwingStoreExporter(dbDir, { artifactMode });
127
+ const exportData = new Map();
128
+ for await (const [key, value] of exporter.getExportData()) {
129
+ exportData.set(key, value);
130
+ }
131
+ const artifactNames = [];
132
+ for await (const name of exporter.getArtifactNames()) {
133
+ artifactNames.push(name);
134
+ }
135
+ await exporter.close();
136
+ return { exportData, artifactNames };
137
+ };
138
+
139
+ const reImport = async (t, dbDir, artifactMode) => {
140
+ const [dbDir2, cleanup] = await tmpDir('testdb2');
141
+ t.teardown(cleanup);
142
+ const exporter = makeSwingStoreExporter(dbDir, { artifactMode });
143
+ const ss2 = await importSwingStore(exporter, dbDir2, { artifactMode });
144
+ await ss2.hostStorage.commit();
145
+ return sqlite3(path.join(dbDir2, 'swingstore.sqlite'));
146
+ };
147
+
148
+ const compareNoHash = (t, obj1, obj2) => {
149
+ const o1 = {};
150
+ for (const [key, value] of Object.entries(obj1)) {
151
+ const { hash: _, ...data } = JSON.parse(value);
152
+ o1[key] = data;
153
+ }
154
+ return t.deepEqual(o1, obj2);
155
+ };
156
+
157
+ const setupTranscript = async t => {
158
+ const vatID = 'v1';
159
+ const exportLog = [];
160
+ const currentExportData = {};
161
+ const exportCallback = exports => {
162
+ for (const [key, value] of exports) {
163
+ exportLog.push([key, value]);
164
+ }
165
+ mergeExportDeltas(currentExportData, exports);
166
+ };
167
+ const [dbDir, cleanup] = await tmpDir('testdb');
168
+ t.teardown(cleanup);
169
+ const store = initSwingStore(dbDir, { exportCallback });
170
+ const { kernelStorage, hostStorage } = store;
171
+ const { transcriptStore } = kernelStorage;
172
+ const { commit } = hostStorage;
173
+ // look directly at DB to confirm changes
174
+ const db = sqlite3(path.join(dbDir, 'swingstore.sqlite'));
175
+
176
+ // two incarnations, two spans each
177
+ transcriptStore.initTranscript(vatID);
178
+ transcriptStore.addItem(vatID, 'aaa');
179
+ transcriptStore.addItem(vatID, 'bbb');
180
+ transcriptStore.rolloverSpan(vatID);
181
+ transcriptStore.addItem(vatID, 'ccc');
182
+ transcriptStore.addItem(vatID, 'ddd');
183
+ transcriptStore.rolloverIncarnation(vatID);
184
+ transcriptStore.addItem(vatID, 'eee');
185
+ transcriptStore.addItem(vatID, 'fff');
186
+ transcriptStore.rolloverSpan(vatID);
187
+ transcriptStore.addItem(vatID, 'ggg');
188
+ transcriptStore.addItem(vatID, 'hhh');
189
+ await commit();
190
+
191
+ return {
192
+ db,
193
+ dbDir,
194
+ commit,
195
+ transcriptStore,
196
+ exportLog,
197
+ currentExportData,
198
+ vatID,
199
+ };
200
+ };
201
+
202
+ test('slow deletion of transcripts', async t => {
203
+ // slow transcript deletion should remove export-data as it removes
204
+ // transcript spans and their items
205
+
206
+ const {
207
+ db,
208
+ dbDir,
209
+ commit,
210
+ transcriptStore,
211
+ exportLog,
212
+ currentExportData,
213
+ vatID,
214
+ } = await setupTranscript(t);
215
+
216
+ t.is(exportLog.length, 4);
217
+ t.is(exportLog[0][0], 'transcript.v1.0');
218
+ t.is(exportLog[1][0], 'transcript.v1.2');
219
+ t.is(exportLog[2][0], 'transcript.v1.4');
220
+ t.is(exportLog[3][0], 'transcript.v1.current');
221
+ exportLog.length = 0;
222
+ const t0 = { vatID, startPos: 0, endPos: 2, isCurrent: 0, incarnation: 0 };
223
+ const t2 = { vatID, startPos: 2, endPos: 4, isCurrent: 0, incarnation: 0 };
224
+ const t4 = { vatID, startPos: 4, endPos: 6, isCurrent: 0, incarnation: 1 };
225
+ const tc = { vatID, startPos: 6, endPos: 8, isCurrent: 1, incarnation: 1 };
226
+ const t6 = { vatID, startPos: 6, endPos: 8, isCurrent: 0, incarnation: 1 };
227
+ compareNoHash(t, currentExportData, {
228
+ 'transcript.v1.0': t0,
229
+ 'transcript.v1.2': t2,
230
+ 'transcript.v1.4': t4,
231
+ 'transcript.v1.current': tc,
232
+ });
233
+
234
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 8);
235
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
236
+
237
+ // an "operational"-mode export should list all spans, but only have
238
+ // artifacts for the current one
239
+ {
240
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
241
+ t.deepEqual(currentExportData, mapToObj(exportData));
242
+ compareNoHash(t, mapToObj(exportData), {
243
+ 'transcript.v1.0': t0,
244
+ 'transcript.v1.2': t2,
245
+ 'transcript.v1.4': t4,
246
+ 'transcript.v1.current': tc,
247
+ });
248
+ t.deepEqual(artifactNames, ['transcript.v1.6.8']);
249
+ const db2 = await reImport(t, dbDir, 'operational');
250
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 2);
251
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
252
+ }
253
+
254
+ // an "archival"-mode export should list all four spans, with
255
+ // artifacts for each
256
+ {
257
+ const { exportData, artifactNames } = await getExport(dbDir, 'archival');
258
+ compareNoHash(t, mapToObj(exportData), {
259
+ 'transcript.v1.0': t0,
260
+ 'transcript.v1.2': t2,
261
+ 'transcript.v1.4': t4,
262
+ 'transcript.v1.current': tc,
263
+ });
264
+ t.deepEqual(artifactNames, [
265
+ 'transcript.v1.0.2',
266
+ 'transcript.v1.2.4',
267
+ 'transcript.v1.4.6',
268
+ 'transcript.v1.6.8',
269
+ ]);
270
+ const db2 = await reImport(t, dbDir, 'archival');
271
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 8);
272
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
273
+ }
274
+
275
+ // prepare for deletion, this adds a new "closed" record, and
276
+ // deletes the .current record (i.e. it transforms .current into a
277
+ // closed record)
278
+ {
279
+ transcriptStore.stopUsingTranscript(vatID);
280
+ await commit();
281
+ compareNoHash(t, currentExportData, {
282
+ 'transcript.v1.0': t0,
283
+ 'transcript.v1.2': t2,
284
+ 'transcript.v1.4': t4,
285
+ 'transcript.v1.6': t6,
286
+ });
287
+ exportLog.length = 0;
288
+ // stopUsingTranscript is idempotent
289
+ transcriptStore.stopUsingTranscript(vatID);
290
+ await commit();
291
+ t.is(exportLog.length, 0);
292
+ }
293
+
294
+ // All exports (debug and non-debug) in this "terminated but not
295
+ // deleted" state will still have the export-data keys. Only
296
+ // debug-mode will have artifacts.
297
+ for (const mode of ['operational', 'replay', 'archival', 'debug']) {
298
+ const { exportData, artifactNames } = await getExport(dbDir, mode);
299
+ compareNoHash(t, mapToObj(exportData), {
300
+ 'transcript.v1.0': t0,
301
+ 'transcript.v1.2': t2,
302
+ 'transcript.v1.4': t4,
303
+ 'transcript.v1.6': t6,
304
+ });
305
+ if (mode === 'debug') {
306
+ t.deepEqual(artifactNames, [
307
+ 'transcript.v1.0.2',
308
+ 'transcript.v1.2.4',
309
+ 'transcript.v1.4.6',
310
+ 'transcript.v1.6.8',
311
+ ]);
312
+ } else {
313
+ t.deepEqual(artifactNames, []);
314
+ }
315
+ const db2 = await reImport(t, dbDir, 'operational');
316
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
317
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
318
+ }
319
+
320
+ // first deletion
321
+ {
322
+ // budget=1 will let it delete one span, the last one
323
+ const dc = transcriptStore.deleteVatTranscripts(vatID, 1);
324
+ t.false(dc.done);
325
+ t.is(dc.cleanups, 1);
326
+ await commit();
327
+ compareNoHash(t, currentExportData, {
328
+ 'transcript.v1.0': t0,
329
+ 'transcript.v1.2': t2,
330
+ 'transcript.v1.4': t4,
331
+ });
332
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 6);
333
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 3);
334
+ }
335
+
336
+ // Exports in this partially-deleted state should be coherent: they
337
+ // provide a subset of the older spans (the not-yet-deleted ones,
338
+ // all of which have isCurrent=0) and no items (even for
339
+ // not-yet-deleted spans). The import-time assertComplete() test
340
+ // must be satisfied.
341
+
342
+ for (const mode of ['operational', 'replay', 'archival', 'debug']) {
343
+ const { exportData, artifactNames } = await getExport(dbDir, mode);
344
+ compareNoHash(t, mapToObj(exportData), {
345
+ 'transcript.v1.0': t0,
346
+ 'transcript.v1.2': t2,
347
+ 'transcript.v1.4': t4,
348
+ });
349
+ if (mode === 'debug') {
350
+ t.deepEqual(artifactNames, [
351
+ 'transcript.v1.0.2',
352
+ 'transcript.v1.2.4',
353
+ 'transcript.v1.4.6',
354
+ ]);
355
+ } else {
356
+ t.deepEqual(artifactNames, []);
357
+ }
358
+ const db2 = await reImport(t, dbDir, 'operational');
359
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
360
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 3);
361
+ }
362
+
363
+ // second deletion
364
+ {
365
+ const dc = transcriptStore.deleteVatTranscripts(vatID, 1);
366
+ t.false(dc.done);
367
+ t.is(dc.cleanups, 1);
368
+ await commit();
369
+ compareNoHash(t, currentExportData, {
370
+ 'transcript.v1.0': t0,
371
+ 'transcript.v1.2': t2,
372
+ });
373
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 4);
374
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 2);
375
+ }
376
+
377
+ for (const mode of ['operational', 'replay', 'archival', 'debug']) {
378
+ const { exportData, artifactNames } = await getExport(dbDir, mode);
379
+ compareNoHash(t, mapToObj(exportData), {
380
+ 'transcript.v1.0': t0,
381
+ 'transcript.v1.2': t2,
382
+ });
383
+ if (mode === 'debug') {
384
+ t.deepEqual(artifactNames, ['transcript.v1.0.2', 'transcript.v1.2.4']);
385
+ } else {
386
+ t.deepEqual(artifactNames, []);
387
+ }
388
+ const db2 = await reImport(t, dbDir, 'operational');
389
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
390
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 2);
391
+ }
392
+
393
+ // last deletion, enough budget to finish
394
+ {
395
+ const dc = transcriptStore.deleteVatTranscripts(vatID, 5);
396
+ t.true(dc.done);
397
+ t.is(dc.cleanups, 2);
398
+ await commit();
399
+ compareNoHash(t, currentExportData, {});
400
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
401
+ t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 0);
402
+ }
403
+
404
+ for (const mode of ['operational', 'replay', 'archival', 'debug']) {
405
+ const { exportData, artifactNames } = await getExport(dbDir, mode);
406
+ compareNoHash(t, mapToObj(exportData), {});
407
+ t.deepEqual(artifactNames, []);
408
+ const db2 = await reImport(t, dbDir, 'operational');
409
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptItems').pluck().get(), 0);
410
+ t.is(db2.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 0);
411
+ }
412
+
413
+ // deleteVatTranscripts is idempotent
414
+ {
415
+ exportLog.length = 0;
416
+ const dc = transcriptStore.deleteVatTranscripts(vatID, 5);
417
+ t.true(dc.done);
418
+ t.is(dc.cleanups, 0);
419
+ await commit();
420
+ t.is(exportLog.length, 0);
421
+ }
422
+ });
423
+
424
+ test('slow deletion without stopUsingTranscript', async t => {
425
+ // slow deletion should work even without stopUsingTranscript
426
+ const { dbDir, commit, transcriptStore, currentExportData, vatID } =
427
+ await setupTranscript(t);
428
+
429
+ // first deletion
430
+ {
431
+ // budget=1 will let it delete one span, the last one. Because we
432
+ // didn't call stopUsingTranscript, this also removes the .current
433
+ // record
434
+ const dc = transcriptStore.deleteVatTranscripts(vatID, 1);
435
+ t.false(dc.done);
436
+ t.is(dc.cleanups, 1);
437
+ await commit();
438
+ const t0 = { vatID, startPos: 0, endPos: 2, isCurrent: 0, incarnation: 0 };
439
+ const t2 = { vatID, startPos: 2, endPos: 4, isCurrent: 0, incarnation: 0 };
440
+ const t4 = { vatID, startPos: 4, endPos: 6, isCurrent: 0, incarnation: 1 };
441
+ compareNoHash(t, currentExportData, {
442
+ 'transcript.v1.0': t0,
443
+ 'transcript.v1.2': t2,
444
+ 'transcript.v1.4': t4,
445
+ });
446
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
447
+ t.deepEqual(mapToObj(exportData), currentExportData);
448
+ t.deepEqual(artifactNames, []);
449
+ }
450
+ transcriptStore.deleteVatTranscripts(vatID);
451
+ await commit();
452
+ t.deepEqual(currentExportData, {});
453
+ {
454
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
455
+ t.deepEqual(exportData, new Map());
456
+ t.deepEqual(artifactNames, []);
457
+ }
458
+ });
459
+
460
+ test('full deletion without stopUsingTranscript', async t => {
461
+ // full deletion should work even without stopUsingTranscript
462
+ const { dbDir, commit, transcriptStore, currentExportData, vatID } =
463
+ await setupTranscript(t);
464
+ const dc = transcriptStore.deleteVatTranscripts(vatID);
465
+ t.true(dc.done);
466
+ await commit();
467
+ t.deepEqual(currentExportData, {});
468
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
469
+ t.deepEqual(exportData, new Map());
470
+ t.deepEqual(artifactNames, []);
471
+ });
472
+
473
+ const setupSnapshots = async t => {
474
+ const vatID = 'v1';
475
+ const exportLog = [];
476
+ const currentExportData = {};
477
+ const exportCallback = exports => {
478
+ for (const [key, value] of exports) {
479
+ exportLog.push([key, value]);
480
+ }
481
+ mergeExportDeltas(currentExportData, exports);
482
+ };
483
+ const [dbDir, cleanup] = await tmpDir('testdb');
484
+ t.teardown(cleanup);
485
+ const store = initSwingStore(dbDir, { exportCallback });
486
+ const { kernelStorage, hostStorage } = store;
487
+ const { snapStore } = kernelStorage;
488
+ const { commit } = hostStorage;
489
+ // look directly at DB to confirm changes
490
+ const db = sqlite3(path.join(dbDir, 'swingstore.sqlite'));
491
+
492
+ await snapStore.saveSnapshot(vatID, 10, getSnapshotStream());
493
+ await snapStore.saveSnapshot(vatID, 11, getSnapshotStream());
494
+ await snapStore.saveSnapshot(vatID, 12, getSnapshotStream());
495
+ // nothing is written to exportCallback until endCrank() or commit()
496
+ t.deepEqual(exportLog, []);
497
+ await commit();
498
+ const hash = JSON.parse(exportLog[0][1]).hash;
499
+
500
+ return {
501
+ db,
502
+ dbDir,
503
+ commit,
504
+ snapStore,
505
+ exportLog,
506
+ currentExportData,
507
+ vatID,
508
+ hash,
509
+ };
510
+ };
511
+
512
+ test('slow deletion of snapshots', async t => {
513
+ // slow snapshot deletion should remove export-data as it removes
514
+ // snapshots
515
+ const {
516
+ db,
517
+ dbDir,
518
+ commit,
519
+ snapStore,
520
+ exportLog,
521
+ currentExportData,
522
+ vatID,
523
+ hash,
524
+ } = await setupSnapshots(t);
525
+ t.deepEqual(currentExportData, {
526
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
527
+ 'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
528
+ 'snapshot.v1.12': JSON.stringify({ vatID, snapPos: 12, hash, inUse: 1 }),
529
+ 'snapshot.v1.current': 'snapshot.v1.12',
530
+ });
531
+
532
+ t.is(db.prepare('SELECT COUNT(*) FROM snapshots').pluck().get(), 3);
533
+ {
534
+ // export should mention all spans, with a single current artifact
535
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
536
+ t.deepEqual(currentExportData, mapToObj(exportData));
537
+ t.is(exportData.get('snapshot.v1.current'), 'snapshot.v1.12');
538
+ t.deepEqual(artifactNames, ['snapshot.v1.12']);
539
+ }
540
+
541
+ // Prepare for deletion, this clears the .inUse flag on the latest
542
+ // record, and deletes the .current record. Exports stop including
543
+ // any artifacts.
544
+ {
545
+ snapStore.stopUsingLastSnapshot(vatID);
546
+ await commit();
547
+ t.deepEqual(currentExportData, {
548
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
549
+ 'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
550
+ 'snapshot.v1.12': JSON.stringify({ vatID, snapPos: 12, hash, inUse: 0 }),
551
+ });
552
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
553
+ t.deepEqual(currentExportData, mapToObj(exportData));
554
+ t.deepEqual(artifactNames, []);
555
+ exportLog.length = 0;
556
+ // stopUsingLastSnapshot is idempotent
557
+ snapStore.stopUsingLastSnapshot(vatID);
558
+ await commit();
559
+ t.is(exportLog.length, 0);
560
+ }
561
+
562
+ // first deletion
563
+ {
564
+ // budget=1 will let it delete one snapshot
565
+ const dc = snapStore.deleteVatSnapshots(vatID, 1);
566
+ t.false(dc.done);
567
+ t.is(dc.cleanups, 1);
568
+ await commit();
569
+ t.deepEqual(currentExportData, {
570
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
571
+ 'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
572
+ });
573
+ t.is(db.prepare('SELECT COUNT(*) FROM snapshots').pluck().get(), 2);
574
+ // export should mention fewer spans, have no .current or
575
+ // artifacts
576
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
577
+ t.deepEqual(currentExportData, mapToObj(exportData));
578
+ t.deepEqual(artifactNames, []);
579
+ // and it should be importable
580
+ const db2 = await reImport(t, dbDir, 'operational');
581
+ t.is(db2.prepare('SELECT COUNT(*) FROM snapshots').pluck().get(), 2);
582
+ const db3 = await reImport(t, dbDir, 'archival');
583
+ t.is(db3.prepare('SELECT COUNT(*) FROM snapshots').pluck().get(), 2);
584
+ }
585
+
586
+ // second+last deletion, enough budget to delete both remaining
587
+ // snapshots
588
+ {
589
+ const dc = snapStore.deleteVatSnapshots(vatID, 5);
590
+ t.true(dc.done);
591
+ t.is(dc.cleanups, 2);
592
+ await commit();
593
+ t.deepEqual(currentExportData, {});
594
+ t.is(db.prepare('SELECT COUNT(*) FROM snapshots').pluck().get(), 0);
595
+ // export should mention nothing
596
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
597
+ t.deepEqual(currentExportData, mapToObj(exportData));
598
+ t.deepEqual(artifactNames, []);
599
+ }
600
+ });
601
+
602
+ test('slow deletion without stopUsingLastSnapshot', async t => {
603
+ // slow snapshot deletion should work even without
604
+ // stopUsingLastSnapshot
605
+ const { dbDir, commit, snapStore, currentExportData, vatID, hash } =
606
+ await setupSnapshots(t);
607
+
608
+ {
609
+ // budget=1 will let it delete one snapshot, the last one. Because
610
+ // we didn't call stopUsingLastSnapshot, this also removes the
611
+ // .current record
612
+ const dc = snapStore.deleteVatSnapshots(vatID, 1);
613
+ t.false(dc.done);
614
+ t.is(dc.cleanups, 1);
615
+ await commit();
616
+ t.deepEqual(currentExportData, {
617
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
618
+ 'snapshot.v1.11': JSON.stringify({ vatID, snapPos: 11, hash, inUse: 0 }),
619
+ });
620
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
621
+ t.deepEqual(mapToObj(exportData), currentExportData);
622
+ t.deepEqual(artifactNames, []);
623
+ }
624
+
625
+ {
626
+ const dc = snapStore.deleteVatSnapshots(vatID, 1);
627
+ t.false(dc.done);
628
+ t.is(dc.cleanups, 1);
629
+ await commit();
630
+ t.deepEqual(currentExportData, {
631
+ 'snapshot.v1.10': JSON.stringify({ vatID, snapPos: 10, hash, inUse: 0 }),
632
+ });
633
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
634
+ t.deepEqual(mapToObj(exportData), currentExportData);
635
+ t.deepEqual(artifactNames, []);
636
+ }
637
+
638
+ {
639
+ const dc = snapStore.deleteVatSnapshots(vatID, 1);
640
+ t.true(dc.done);
641
+ t.is(dc.cleanups, 1);
642
+ await commit();
643
+ t.deepEqual(currentExportData, {});
644
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
645
+ t.deepEqual(mapToObj(exportData), currentExportData);
646
+ t.deepEqual(artifactNames, []);
647
+ }
648
+ });
649
+
650
+ test('full deletion without stopUsingLastSnapshot', async t => {
651
+ // full snapshot deletion should work even without
652
+ // stopUsingLastSnapshot
653
+ const { dbDir, commit, snapStore, currentExportData, vatID } =
654
+ await setupSnapshots(t);
655
+
656
+ {
657
+ const dc = snapStore.deleteVatSnapshots(vatID);
658
+ t.true(dc.done);
659
+ // no budget means no accounting, ignore dc.cleanups
660
+ await commit();
661
+ t.deepEqual(currentExportData, {});
662
+ const { exportData, artifactNames } = await getExport(dbDir, 'operational');
663
+ t.deepEqual(mapToObj(exportData), currentExportData);
664
+ t.deepEqual(artifactNames, []);
665
+ }
666
+ });