@agoric/swing-store 0.9.2-dev-fb3af6c.0 → 0.9.2-dev-d6f50e3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/data-export.md +6 -6
- package/package.json +3 -3
- package/src/transcriptStore.js +23 -16
- package/test/state.test.js +91 -67
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/swing-store",
|
|
3
|
-
"version": "0.9.2-dev-
|
|
3
|
+
"version": "0.9.2-dev-d6f50e3.0+d6f50e3",
|
|
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-
|
|
24
|
+
"@agoric/internal": "0.3.3-dev-d6f50e3.0+d6f50e3",
|
|
25
25
|
"@endo/base64": "^1.0.7",
|
|
26
26
|
"@endo/bundle-source": "^3.4.0",
|
|
27
27
|
"@endo/check-bundle": "^1.0.9",
|
|
@@ -51,5 +51,5 @@
|
|
|
51
51
|
"typeCoverage": {
|
|
52
52
|
"atLeast": 76.31
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "d6f50e32f6f19bb14cfe4530812609dff00d21b8"
|
|
55
55
|
}
|
package/src/transcriptStore.js
CHANGED
|
@@ -80,15 +80,21 @@ export function makeTranscriptStore(
|
|
|
80
80
|
)
|
|
81
81
|
`);
|
|
82
82
|
|
|
83
|
-
// Transcripts are broken up into "spans", delimited by heap snapshots.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
83
|
+
// Transcripts are broken up into "spans", delimited by heap snapshots.
|
|
84
|
+
// The items of each transcript consist of deliveries and pseudo-deliveries
|
|
85
|
+
// such as initialize-worker and load-snapshot.
|
|
86
|
+
// For every vatID, there will be exactly one current span (with isCurrent=1),
|
|
87
|
+
// and zero or more non-current (historical) spans (with isCurrent=null).
|
|
88
|
+
// If we take a heap snapshot after the first hundred items and again after
|
|
89
|
+
// the second hundred (i.e., after zero-indexed items 99 and 199),
|
|
90
|
+
// and have not yet extended the transcript after the second snapshot, we'll
|
|
91
|
+
// have two historical spans (one with startPos=0 and endPos=100, the second
|
|
92
|
+
// with startPos=100 and endPos=200) and a single empty current span with
|
|
93
|
+
// startPos=200 and endPos=200. But this situation is transient, and will
|
|
94
|
+
// generally be followed by a load-snapshot pseudo-delivery before the next
|
|
95
|
+
// commit (at which point the single current span will still have startPos=200
|
|
96
|
+
// but will have endPos=201). After we perform the next delivery, the single
|
|
97
|
+
// current span will still have startPos=200 but will now have endPos=202.
|
|
92
98
|
//
|
|
93
99
|
// The transcriptItems associated with historical spans may or may not exist,
|
|
94
100
|
// depending on pruning. However, the items associated with the current span
|
|
@@ -246,7 +252,7 @@ export function makeTranscriptStore(
|
|
|
246
252
|
|
|
247
253
|
const sqlDeleteOldItems = db.prepare(`
|
|
248
254
|
DELETE FROM transcriptItems
|
|
249
|
-
WHERE vatID = ? AND position < ?
|
|
255
|
+
WHERE vatID = ? AND position >= ? AND position < ?
|
|
250
256
|
`);
|
|
251
257
|
|
|
252
258
|
function doSpanRollover(vatID, isNewIncarnation) {
|
|
@@ -276,12 +282,13 @@ export function makeTranscriptStore(
|
|
|
276
282
|
noteExport(spanMetadataKey(newRec), JSON.stringify(newRec));
|
|
277
283
|
|
|
278
284
|
if (!keepTranscripts) {
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
+
// Delete items of the previously-current span.
|
|
286
|
+
// There may still be items associated with even older spans, but we leave
|
|
287
|
+
// those, to avoid excessive DB churn (for details, see #9387 and #9174).
|
|
288
|
+
// Recovery of space claimed by such ancient items is expected to use an
|
|
289
|
+
// external mechanism such as restoration from an operational snapshot
|
|
290
|
+
// that doesn't include them.
|
|
291
|
+
sqlDeleteOldItems.run(vatID, startPos, endPos);
|
|
285
292
|
}
|
|
286
293
|
return incarnationToUse;
|
|
287
294
|
}
|
package/test/state.test.js
CHANGED
|
@@ -155,82 +155,106 @@ test('persistent kvStore maxKeySize write', async t => {
|
|
|
155
155
|
await hostStorage.close();
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
const testTranscriptStore = test.macro({
|
|
159
|
+
title(prefix = '', { ephemeral, keepTranscripts }) {
|
|
160
|
+
const type = ephemeral ? 'in-memory' : 'persistent';
|
|
161
|
+
const detail = keepTranscripts ? 'with retention' : 'without retention';
|
|
162
|
+
return `${prefix.replace(/.$/, '$& ')}${type} transcriptStore ${detail}`;
|
|
163
|
+
},
|
|
164
|
+
async exec(t, { ephemeral, keepTranscripts }) {
|
|
165
|
+
let dbDir = null;
|
|
166
|
+
if (!ephemeral) {
|
|
167
|
+
const [tmpPath, cleanup] = await tmpDir('testdb');
|
|
168
|
+
t.teardown(cleanup);
|
|
169
|
+
t.is(isSwingStore(tmpPath), false);
|
|
170
|
+
dbDir = tmpPath;
|
|
171
|
+
}
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
173
|
+
const exportLog = makeExportLog();
|
|
174
|
+
const { kernelStorage, hostStorage } = initSwingStore(dbDir, {
|
|
175
|
+
exportCallback: exportLog.callback,
|
|
176
|
+
keepTranscripts,
|
|
177
|
+
});
|
|
178
|
+
const { transcriptStore } = kernelStorage;
|
|
179
|
+
const { commit, close } = hostStorage;
|
|
180
|
+
|
|
181
|
+
transcriptStore.initTranscript('st1');
|
|
182
|
+
transcriptStore.initTranscript('st2');
|
|
183
|
+
transcriptStore.addItem('st1', 'zeroth');
|
|
184
|
+
transcriptStore.rolloverSpan('st1');
|
|
185
|
+
transcriptStore.addItem('st1', 'first');
|
|
186
|
+
transcriptStore.addItem('st1', 'second');
|
|
187
|
+
transcriptStore.rolloverSpan('st1');
|
|
188
|
+
transcriptStore.addItem('st1', 'third');
|
|
189
|
+
transcriptStore.addItem('st2', 'oneth');
|
|
190
|
+
transcriptStore.addItem('st1', 'fourth');
|
|
191
|
+
transcriptStore.addItem('st2', 'twoth');
|
|
192
|
+
transcriptStore.addItem('st2', 'threeth');
|
|
193
|
+
transcriptStore.addItem('st2', 'fourst');
|
|
194
|
+
const reader1a = transcriptStore.readSpan('st1', 0);
|
|
195
|
+
const reader1b = transcriptStore.readSpan('st1', 1);
|
|
196
|
+
if (keepTranscripts) {
|
|
197
|
+
t.deepEqual(Array.from(reader1a), ['zeroth']);
|
|
198
|
+
t.deepEqual(Array.from(reader1b), ['first', 'second']);
|
|
199
|
+
} else {
|
|
200
|
+
const fna = async () => Array.from(reader1a);
|
|
201
|
+
const fnb = async () => Array.from(reader1b);
|
|
202
|
+
await t.throwsAsync(fna, undefined, 'pruned spans must not be readable');
|
|
203
|
+
await t.throwsAsync(fnb, undefined, 'pruned spans must not be readable');
|
|
204
|
+
}
|
|
205
|
+
const reader2 = transcriptStore.readSpan('st2', 0);
|
|
206
|
+
t.deepEqual(Array.from(reader2), ['oneth', 'twoth', 'threeth', 'fourst']);
|
|
186
207
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
t.deepEqual(Array.from(reader1alt2), ['third', 'fourth']);
|
|
208
|
+
t.throws(() => transcriptStore.readSpan('st2', 3), {
|
|
209
|
+
message: 'no transcript span for "st2" at 3',
|
|
210
|
+
});
|
|
191
211
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
212
|
+
const reader1alt = transcriptStore.readSpan('st1');
|
|
213
|
+
t.deepEqual(Array.from(reader1alt), ['third', 'fourth']);
|
|
214
|
+
const reader1alt2 = transcriptStore.readSpan('st1', 3);
|
|
215
|
+
t.deepEqual(Array.from(reader1alt2), ['third', 'fourth']);
|
|
195
216
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
transcriptStore.initTranscript('empty');
|
|
218
|
+
const readerEmpty = transcriptStore.readSpan('empty');
|
|
219
|
+
t.deepEqual(Array.from(readerEmpty), []);
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
],
|
|
207
|
-
[
|
|
208
|
-
'transcript.st1.0',
|
|
209
|
-
'{"vatID":"st1","startPos":0,"endPos":2,"hash":"d385c43882cfb5611d255e362a9a98626ba4e55dfc308fc346c144c696ae734e","isCurrent":0,"incarnation":0}',
|
|
210
|
-
],
|
|
211
|
-
[
|
|
212
|
-
'transcript.st1.current',
|
|
213
|
-
'{"vatID":"st1","startPos":2,"endPos":4,"hash":"789342fab468506c624c713c46953992f53a7eaae390d634790d791636b96cab","isCurrent":1,"incarnation":0}',
|
|
214
|
-
],
|
|
221
|
+
t.throws(() => transcriptStore.readSpan('nonexistent'), {
|
|
222
|
+
message: 'no current transcript for "nonexistent"',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await commit();
|
|
226
|
+
t.deepEqual(exportLog.getLog(), [
|
|
215
227
|
[
|
|
216
|
-
|
|
217
|
-
|
|
228
|
+
[
|
|
229
|
+
'transcript.empty.current',
|
|
230
|
+
'{"vatID":"empty","startPos":0,"endPos":0,"hash":"43e6be43a3a34d60c0ebeb8498b5849b094fc20fc68483a7aeb3624fa10f79f6","isCurrent":1,"incarnation":0}',
|
|
231
|
+
],
|
|
232
|
+
[
|
|
233
|
+
'transcript.st1.0',
|
|
234
|
+
'{"vatID":"st1","startPos":0,"endPos":1,"hash":"92d0cf6ecd39b60b4e32dc65d4c6f343495928cb041f25b19e2825b17f4daa9a","isCurrent":0,"incarnation":0}',
|
|
235
|
+
],
|
|
236
|
+
[
|
|
237
|
+
'transcript.st1.1',
|
|
238
|
+
'{"vatID":"st1","startPos":1,"endPos":3,"hash":"d385c43882cfb5611d255e362a9a98626ba4e55dfc308fc346c144c696ae734e","isCurrent":0,"incarnation":0}',
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
'transcript.st1.current',
|
|
242
|
+
'{"vatID":"st1","startPos":3,"endPos":5,"hash":"789342fab468506c624c713c46953992f53a7eaae390d634790d791636b96cab","isCurrent":1,"incarnation":0}',
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
'transcript.st2.current',
|
|
246
|
+
'{"vatID":"st2","startPos":0,"endPos":4,"hash":"45de7ae9d2be34148f9cf3000052e5d1374932d663442fe9f39a342d221cebf1","isCurrent":1,"incarnation":0}',
|
|
247
|
+
],
|
|
218
248
|
],
|
|
219
|
-
]
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
test('in-memory transcriptStore read/write', async t => {
|
|
225
|
-
await testTranscriptStore(t, null);
|
|
249
|
+
]);
|
|
250
|
+
await close();
|
|
251
|
+
},
|
|
226
252
|
});
|
|
227
253
|
|
|
228
|
-
test(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
await testTranscriptStore(t, dbDir);
|
|
233
|
-
});
|
|
254
|
+
test(testTranscriptStore, { ephemeral: true, keepTranscripts: true });
|
|
255
|
+
test(testTranscriptStore, { ephemeral: true, keepTranscripts: false });
|
|
256
|
+
test(testTranscriptStore, { ephemeral: false, keepTranscripts: true });
|
|
257
|
+
test(testTranscriptStore, { ephemeral: false, keepTranscripts: false });
|
|
234
258
|
|
|
235
259
|
test('transcriptStore abort', async t => {
|
|
236
260
|
const [dbDir, cleanup] = await tmpDir('testdb');
|