@agoric/swing-store 0.9.2-dev-914bee2.0 → 0.9.2-dev-07f6885.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/transcriptstore.md +3 -3
- package/package.json +3 -3
- package/src/archiver.js +80 -0
- package/src/index.js +2 -0
- package/src/snapStore.js +55 -67
- package/src/swingStore.js +9 -1
- package/src/transcriptStore.js +117 -110
- package/test/deletion.test.js +51 -14
- 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 +2 -2
package/docs/transcriptstore.md
CHANGED
|
@@ -32,14 +32,14 @@ Pruned spans are not available for export artifacts, of course, because the data
|
|
|
32
32
|
|
|
33
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
34
|
|
|
35
|
-
Historical spans, which are "closed" and no longer growing, use a record name of
|
|
36
|
-
|
|
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
37
|
|
|
38
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
39
|
|
|
40
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
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.
|
|
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
43
|
|
|
44
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
45
|
|
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-07f6885.0+07f6885",
|
|
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-07f6885.0+07f6885",
|
|
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": 78.77
|
|
53
53
|
},
|
|
54
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "07f6885c560333981d6564581b2a00735a6c428c"
|
|
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/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/snapStore.js
CHANGED
|
@@ -4,10 +4,7 @@ import { finished as finishedCallback, PassThrough, Readable } from 'stream';
|
|
|
4
4
|
import { promisify } from 'util';
|
|
5
5
|
import { createGzip, createGunzip } from 'zlib';
|
|
6
6
|
import { Fail, q } from '@endo/errors';
|
|
7
|
-
import {
|
|
8
|
-
aggregateTryFinally,
|
|
9
|
-
PromiseAllOrErrors,
|
|
10
|
-
} from '@agoric/internal/src/node/utils.js';
|
|
7
|
+
import { withDeferredCleanup } from '@agoric/internal';
|
|
11
8
|
import { buffer } from './util.js';
|
|
12
9
|
|
|
13
10
|
/**
|
|
@@ -17,6 +14,7 @@ import { buffer } from './util.js';
|
|
|
17
14
|
* @property {number} dbSaveSeconds time to write snapshot in DB
|
|
18
15
|
* @property {number} compressedSize size of (compressed) snapshot
|
|
19
16
|
* @property {number} compressSeconds time to generate and compress the snapshot
|
|
17
|
+
* @property {number} [archiveWriteSeconds] time to write an archive to disk (if applicable)
|
|
20
18
|
*/
|
|
21
19
|
|
|
22
20
|
/**
|
|
@@ -73,6 +71,7 @@ const finished = promisify(finishedCallback);
|
|
|
73
71
|
* @param {(key: string, value: string | undefined) => void} noteExport
|
|
74
72
|
* @param {object} [options]
|
|
75
73
|
* @param {boolean | undefined} [options.keepSnapshots]
|
|
74
|
+
* @param {(name: string, compressedData: Parameters<import('stream').Readable.from>[0]) => Promise<void>} [options.archiveSnapshot]
|
|
76
75
|
* @returns {SnapStore & SnapStoreInternal & SnapStoreDebug}
|
|
77
76
|
*/
|
|
78
77
|
export function makeSnapStore(
|
|
@@ -80,7 +79,7 @@ export function makeSnapStore(
|
|
|
80
79
|
ensureTxn,
|
|
81
80
|
{ measureSeconds },
|
|
82
81
|
noteExport = () => {},
|
|
83
|
-
{ keepSnapshots = false } = {},
|
|
82
|
+
{ keepSnapshots = false, archiveSnapshot } = {},
|
|
84
83
|
) {
|
|
85
84
|
db.exec(`
|
|
86
85
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
@@ -196,7 +195,8 @@ export function makeSnapStore(
|
|
|
196
195
|
|
|
197
196
|
/**
|
|
198
197
|
* Generates a new XS heap snapshot, stores a gzipped copy of it into the
|
|
199
|
-
* snapshots table
|
|
198
|
+
* snapshots table (and also to an archiveSnapshot callback if provided for
|
|
199
|
+
* e.g. disk archival), and reports information about the process, including
|
|
200
200
|
* snapshot size and timing metrics.
|
|
201
201
|
*
|
|
202
202
|
* @param {string} vatID
|
|
@@ -205,74 +205,62 @@ export function makeSnapStore(
|
|
|
205
205
|
* @returns {Promise<SnapshotResult>}
|
|
206
206
|
*/
|
|
207
207
|
async function saveSnapshot(vatID, snapPos, snapshotStream) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
new Promise((resolve, reject) =>
|
|
222
|
-
snapReader.destroy(
|
|
223
|
-
null,
|
|
224
|
-
// @ts-expect-error incorrect types
|
|
225
|
-
err => (err ? reject(err) : resolve()),
|
|
226
|
-
),
|
|
227
|
-
),
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
snapReader.on('data', chunk => {
|
|
231
|
-
uncompressedSize += chunk.length;
|
|
232
|
-
});
|
|
233
|
-
snapReader.pipe(hashStream);
|
|
234
|
-
const compressedSnapshotData = await buffer(snapReader.pipe(gzip));
|
|
235
|
-
await finished(snapReader);
|
|
236
|
-
return compressedSnapshotData;
|
|
208
|
+
return withDeferredCleanup(async addCleanup => {
|
|
209
|
+
const hashStream = createHash('sha256');
|
|
210
|
+
const gzip = createGzip();
|
|
211
|
+
let compressedSize = 0;
|
|
212
|
+
let uncompressedSize = 0;
|
|
213
|
+
|
|
214
|
+
const { duration: compressSeconds, result: compressedSnapshot } =
|
|
215
|
+
await measureSeconds(async () => {
|
|
216
|
+
const snapReader = Readable.from(snapshotStream);
|
|
217
|
+
const destroyReader = promisify(snapReader.destroy.bind(snapReader));
|
|
218
|
+
addCleanup(() => destroyReader(null));
|
|
219
|
+
snapReader.on('data', chunk => {
|
|
220
|
+
uncompressedSize += chunk.length;
|
|
237
221
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
stopUsingLastSnapshot(vatID);
|
|
243
|
-
compressedSize = compressedSnapshot.length;
|
|
244
|
-
sqlSaveSnapshot.run(
|
|
245
|
-
vatID,
|
|
246
|
-
snapPos,
|
|
247
|
-
1,
|
|
248
|
-
hash,
|
|
249
|
-
uncompressedSize,
|
|
250
|
-
compressedSize,
|
|
251
|
-
compressedSnapshot,
|
|
252
|
-
);
|
|
253
|
-
const rec = snapshotRec(vatID, snapPos, hash, 1);
|
|
254
|
-
const exportKey = snapshotMetadataKey(rec);
|
|
255
|
-
noteExport(exportKey, JSON.stringify(rec));
|
|
256
|
-
noteExport(
|
|
257
|
-
currentSnapshotMetadataKey(rec),
|
|
258
|
-
snapshotArtifactName(rec),
|
|
259
|
-
);
|
|
222
|
+
snapReader.pipe(hashStream);
|
|
223
|
+
const compressedSnapshotData = await buffer(snapReader.pipe(gzip));
|
|
224
|
+
await finished(snapReader);
|
|
225
|
+
return compressedSnapshotData;
|
|
260
226
|
});
|
|
227
|
+
const hash = hashStream.digest('hex');
|
|
228
|
+
const rec = snapshotRec(vatID, snapPos, hash, 1);
|
|
229
|
+
const exportKey = snapshotMetadataKey(rec);
|
|
261
230
|
|
|
262
|
-
|
|
231
|
+
const { duration: dbSaveSeconds } = await measureSeconds(async () => {
|
|
232
|
+
ensureTxn();
|
|
233
|
+
stopUsingLastSnapshot(vatID);
|
|
234
|
+
compressedSize = compressedSnapshot.length;
|
|
235
|
+
sqlSaveSnapshot.run(
|
|
236
|
+
vatID,
|
|
237
|
+
snapPos,
|
|
238
|
+
1,
|
|
263
239
|
hash,
|
|
264
240
|
uncompressedSize,
|
|
265
|
-
compressSeconds,
|
|
266
|
-
dbSaveSeconds,
|
|
267
241
|
compressedSize,
|
|
268
|
-
|
|
269
|
-
},
|
|
270
|
-
async () => {
|
|
271
|
-
await PromiseAllOrErrors(
|
|
272
|
-
cleanup.reverse().map(fn => Promise.resolve().then(() => fn())),
|
|
242
|
+
compressedSnapshot,
|
|
273
243
|
);
|
|
274
|
-
|
|
275
|
-
|
|
244
|
+
noteExport(exportKey, JSON.stringify(rec));
|
|
245
|
+
noteExport(currentSnapshotMetadataKey(rec), snapshotArtifactName(rec));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let archiveWriteSeconds;
|
|
249
|
+
if (archiveSnapshot) {
|
|
250
|
+
({ duration: archiveWriteSeconds } = await measureSeconds(async () => {
|
|
251
|
+
await archiveSnapshot(exportKey, compressedSnapshot);
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return harden({
|
|
256
|
+
hash,
|
|
257
|
+
uncompressedSize,
|
|
258
|
+
compressSeconds,
|
|
259
|
+
dbSaveSeconds,
|
|
260
|
+
archiveWriteSeconds,
|
|
261
|
+
compressedSize,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
276
264
|
}
|
|
277
265
|
|
|
278
266
|
const sqlGetSnapshot = db.prepare(`
|
package/src/swingStore.js
CHANGED
|
@@ -169,7 +169,13 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
169
169
|
filePath = ':memory:';
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
const {
|
|
172
|
+
const {
|
|
173
|
+
traceFile,
|
|
174
|
+
keepSnapshots,
|
|
175
|
+
keepTranscripts,
|
|
176
|
+
archiveSnapshot,
|
|
177
|
+
archiveTranscript,
|
|
178
|
+
} = options;
|
|
173
179
|
|
|
174
180
|
let traceOutput = traceFile
|
|
175
181
|
? fs.createWriteStream(path.resolve(traceFile), {
|
|
@@ -297,6 +303,7 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
297
303
|
noteExport,
|
|
298
304
|
{
|
|
299
305
|
keepTranscripts,
|
|
306
|
+
archiveTranscript,
|
|
300
307
|
},
|
|
301
308
|
);
|
|
302
309
|
const { dumpSnapshots, ...snapStore } = makeSnapStore(
|
|
@@ -306,6 +313,7 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
|
|
|
306
313
|
noteExport,
|
|
307
314
|
{
|
|
308
315
|
keepSnapshots,
|
|
316
|
+
archiveSnapshot,
|
|
309
317
|
},
|
|
310
318
|
);
|
|
311
319
|
const { dumpBundles, ...bundleStore } = makeBundleStore(
|
package/src/transcriptStore.js
CHANGED
|
@@ -15,10 +15,10 @@ import { createSHA256 } from './hasher.js';
|
|
|
15
15
|
*
|
|
16
16
|
* @typedef {{
|
|
17
17
|
* initTranscript: (vatID: string) => void,
|
|
18
|
-
* rolloverSpan: (vatID: string) => number
|
|
19
|
-
* rolloverIncarnation: (vatID: string) => number
|
|
18
|
+
* rolloverSpan: (vatID: string) => Promise<number>,
|
|
19
|
+
* rolloverIncarnation: (vatID: string) => Promise<number>,
|
|
20
20
|
* getCurrentSpanBounds: (vatID: string) => { startPos: number, endPos: number, hash: string, incarnation: number },
|
|
21
|
-
* stopUsingTranscript: (vatID: string) => void
|
|
21
|
+
* stopUsingTranscript: (vatID: string) => Promise<void>,
|
|
22
22
|
* deleteVatTranscripts: (vatID: string, budget?: number) => { done: boolean, cleanups: number },
|
|
23
23
|
* addItem: (vatID: string, item: string) => void,
|
|
24
24
|
* readSpan: (vatID: string, startPos?: number) => IterableIterator<string>,
|
|
@@ -61,14 +61,15 @@ function insistTranscriptPosition(position) {
|
|
|
61
61
|
* @param {() => void} ensureTxn
|
|
62
62
|
* @param {(key: string, value: string | undefined ) => void} noteExport
|
|
63
63
|
* @param {object} [options]
|
|
64
|
-
* @param {boolean
|
|
64
|
+
* @param {boolean} [options.keepTranscripts]
|
|
65
|
+
* @param {(spanName: string, entries: ReturnType<exportSpan>) => Promise<void>} [options.archiveTranscript]
|
|
65
66
|
* @returns { TranscriptStore & TranscriptStoreInternal & TranscriptStoreDebug }
|
|
66
67
|
*/
|
|
67
68
|
export function makeTranscriptStore(
|
|
68
69
|
db,
|
|
69
70
|
ensureTxn,
|
|
70
71
|
noteExport = () => {},
|
|
71
|
-
{ keepTranscripts = true } = {},
|
|
72
|
+
{ keepTranscripts = true, archiveTranscript } = {},
|
|
72
73
|
) {
|
|
73
74
|
db.exec(`
|
|
74
75
|
CREATE TABLE IF NOT EXISTS transcriptItems (
|
|
@@ -246,6 +247,102 @@ export function makeTranscriptStore(
|
|
|
246
247
|
return bounds;
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
const sqlGetSpanEndPos = db.prepare(`
|
|
251
|
+
SELECT endPos
|
|
252
|
+
FROM transcriptSpans
|
|
253
|
+
WHERE vatID = ? AND startPos = ?
|
|
254
|
+
`);
|
|
255
|
+
sqlGetSpanEndPos.pluck(true);
|
|
256
|
+
|
|
257
|
+
const sqlReadSpanItems = db.prepare(`
|
|
258
|
+
SELECT item
|
|
259
|
+
FROM transcriptItems
|
|
260
|
+
WHERE vatID = ? AND ? <= position AND position < ?
|
|
261
|
+
ORDER BY position
|
|
262
|
+
`);
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read the items in a transcript span
|
|
266
|
+
*
|
|
267
|
+
* @param {string} vatID The vat whose transcript is being read
|
|
268
|
+
* @param {number} [startPos] A start position identifying the span to be
|
|
269
|
+
* read; defaults to the current span, whatever it is
|
|
270
|
+
*
|
|
271
|
+
* @returns {IterableIterator<string>} An iterator over the items in the indicated span
|
|
272
|
+
*/
|
|
273
|
+
function readSpan(vatID, startPos) {
|
|
274
|
+
/** @type {number | undefined} */
|
|
275
|
+
let endPos;
|
|
276
|
+
if (startPos === undefined) {
|
|
277
|
+
({ startPos, endPos } = getCurrentSpanBounds(vatID));
|
|
278
|
+
} else {
|
|
279
|
+
insistTranscriptPosition(startPos);
|
|
280
|
+
endPos = sqlGetSpanEndPos.get(vatID, startPos);
|
|
281
|
+
if (typeof endPos !== 'number') {
|
|
282
|
+
throw Fail`no transcript span for ${q(vatID)} at ${q(startPos)}`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
|
|
286
|
+
const expectedCount = endPos - startPos;
|
|
287
|
+
|
|
288
|
+
function* reader() {
|
|
289
|
+
let count = 0;
|
|
290
|
+
for (const { item } of sqlReadSpanItems.iterate(
|
|
291
|
+
vatID,
|
|
292
|
+
startPos,
|
|
293
|
+
endPos,
|
|
294
|
+
)) {
|
|
295
|
+
yield item;
|
|
296
|
+
count += 1;
|
|
297
|
+
}
|
|
298
|
+
count === expectedCount ||
|
|
299
|
+
Fail`read ${q(count)} transcript entries (expected ${q(
|
|
300
|
+
expectedCount,
|
|
301
|
+
)})`;
|
|
302
|
+
}
|
|
303
|
+
harden(reader);
|
|
304
|
+
|
|
305
|
+
if (startPos === endPos) {
|
|
306
|
+
return empty();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return reader();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const sqlGetSpanIsCurrent = db.prepare(`
|
|
313
|
+
SELECT isCurrent
|
|
314
|
+
FROM transcriptSpans
|
|
315
|
+
WHERE vatID = ? AND startPos = ?
|
|
316
|
+
`);
|
|
317
|
+
sqlGetSpanIsCurrent.pluck(true);
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Read a transcript span and return it as a stream of data suitable for
|
|
321
|
+
* export to another store. Transcript items are terminated by newlines.
|
|
322
|
+
*
|
|
323
|
+
* Transcript span artifact names should be strings of the form:
|
|
324
|
+
* `transcript.${vatID}.${startPos}.${endPos}`
|
|
325
|
+
*
|
|
326
|
+
* @param {string} name The name of the transcript artifact to be read
|
|
327
|
+
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
328
|
+
* @yields {Uint8Array}
|
|
329
|
+
*/
|
|
330
|
+
async function* exportSpan(name) {
|
|
331
|
+
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
332
|
+
const parts = name.split('.');
|
|
333
|
+
const [type, vatID, pos] = parts;
|
|
334
|
+
// prettier-ignore
|
|
335
|
+
(parts.length === 4 && type === 'transcript') ||
|
|
336
|
+
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
|
|
337
|
+
const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
|
|
338
|
+
isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
|
|
339
|
+
const startPos = Number(pos);
|
|
340
|
+
for (const entry of readSpan(vatID, startPos)) {
|
|
341
|
+
yield Buffer.from(`${entry}\n`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
harden(exportSpan);
|
|
345
|
+
|
|
249
346
|
const sqlEndCurrentSpan = db.prepare(`
|
|
250
347
|
UPDATE transcriptSpans
|
|
251
348
|
SET isCurrent = null
|
|
@@ -267,7 +364,7 @@ export function makeTranscriptStore(
|
|
|
267
364
|
* @param {string} vatID
|
|
268
365
|
* @param {ReturnType<getCurrentSpanBounds>} bounds
|
|
269
366
|
*/
|
|
270
|
-
function closeSpan(vatID, bounds) {
|
|
367
|
+
async function closeSpan(vatID, bounds) {
|
|
271
368
|
ensureTxn();
|
|
272
369
|
const { startPos, endPos, hash, incarnation } = bounds;
|
|
273
370
|
const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
|
|
@@ -278,6 +375,12 @@ export function makeTranscriptStore(
|
|
|
278
375
|
// and change its DB row to isCurrent=null
|
|
279
376
|
sqlEndCurrentSpan.run(vatID);
|
|
280
377
|
|
|
378
|
+
await null;
|
|
379
|
+
if (archiveTranscript) {
|
|
380
|
+
const spanName = spanArtifactName(rec);
|
|
381
|
+
const entries = exportSpan(spanName);
|
|
382
|
+
await archiveTranscript(spanName, entries);
|
|
383
|
+
}
|
|
281
384
|
if (!keepTranscripts) {
|
|
282
385
|
// Delete items of the previously-current span.
|
|
283
386
|
// There may still be items associated with even older spans, but we leave
|
|
@@ -289,13 +392,13 @@ export function makeTranscriptStore(
|
|
|
289
392
|
}
|
|
290
393
|
}
|
|
291
394
|
|
|
292
|
-
function doSpanRollover(vatID, isNewIncarnation) {
|
|
395
|
+
async function doSpanRollover(vatID, isNewIncarnation) {
|
|
293
396
|
ensureTxn();
|
|
294
397
|
const bounds = getCurrentSpanBounds(vatID);
|
|
295
398
|
const { endPos, incarnation } = bounds;
|
|
296
399
|
|
|
297
400
|
// deal with the now-old span
|
|
298
|
-
closeSpan(vatID, bounds);
|
|
401
|
+
await closeSpan(vatID, bounds);
|
|
299
402
|
|
|
300
403
|
// create a new (empty) DB row, with isCurrent=1
|
|
301
404
|
const newSpanIncarnation = isNewIncarnation ? incarnation + 1 : incarnation;
|
|
@@ -322,9 +425,9 @@ export function makeTranscriptStore(
|
|
|
322
425
|
* @param {string} vatID The vat whose transcript is to rollover to a new
|
|
323
426
|
* span.
|
|
324
427
|
*
|
|
325
|
-
* @returns {number} the new incarnation number
|
|
428
|
+
* @returns {Promise<number>} the new incarnation number
|
|
326
429
|
*/
|
|
327
|
-
function rolloverIncarnation(vatID) {
|
|
430
|
+
async function rolloverIncarnation(vatID) {
|
|
328
431
|
return doSpanRollover(vatID, true);
|
|
329
432
|
}
|
|
330
433
|
|
|
@@ -335,9 +438,9 @@ export function makeTranscriptStore(
|
|
|
335
438
|
* @param {string} vatID The vat whose transcript is to rollover to a new
|
|
336
439
|
* span.
|
|
337
440
|
*
|
|
338
|
-
* @returns {number} the incarnation number
|
|
441
|
+
* @returns {Promise<number>} the incarnation number
|
|
339
442
|
*/
|
|
340
|
-
function rolloverSpan(vatID) {
|
|
443
|
+
async function rolloverSpan(vatID) {
|
|
341
444
|
return doSpanRollover(vatID, false);
|
|
342
445
|
}
|
|
343
446
|
|
|
@@ -387,13 +490,13 @@ export function makeTranscriptStore(
|
|
|
387
490
|
*
|
|
388
491
|
* @param {string} vatID The vat being terminated/deleted.
|
|
389
492
|
*/
|
|
390
|
-
function stopUsingTranscript(vatID) {
|
|
493
|
+
async function stopUsingTranscript(vatID) {
|
|
391
494
|
ensureTxn();
|
|
392
495
|
const bounds = sqlGetCurrentSpanBounds.get(vatID);
|
|
393
496
|
if (!bounds) return;
|
|
394
497
|
|
|
395
498
|
// deal with the now-old span
|
|
396
|
-
closeSpan(vatID, bounds);
|
|
499
|
+
await closeSpan(vatID, bounds);
|
|
397
500
|
|
|
398
501
|
// remove the transcript.${vatID}.current record
|
|
399
502
|
noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
|
|
@@ -584,102 +687,6 @@ export function makeTranscriptStore(
|
|
|
584
687
|
}
|
|
585
688
|
harden(getArtifactNames);
|
|
586
689
|
|
|
587
|
-
const sqlGetSpanEndPos = db.prepare(`
|
|
588
|
-
SELECT endPos
|
|
589
|
-
FROM transcriptSpans
|
|
590
|
-
WHERE vatID = ? AND startPos = ?
|
|
591
|
-
`);
|
|
592
|
-
sqlGetSpanEndPos.pluck(true);
|
|
593
|
-
|
|
594
|
-
const sqlReadSpanItems = db.prepare(`
|
|
595
|
-
SELECT item
|
|
596
|
-
FROM transcriptItems
|
|
597
|
-
WHERE vatID = ? AND ? <= position AND position < ?
|
|
598
|
-
ORDER BY position
|
|
599
|
-
`);
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Read the items in a transcript span
|
|
603
|
-
*
|
|
604
|
-
* @param {string} vatID The vat whose transcript is being read
|
|
605
|
-
* @param {number} [startPos] A start position identifying the span to be
|
|
606
|
-
* read; defaults to the current span, whatever it is
|
|
607
|
-
*
|
|
608
|
-
* @returns {IterableIterator<string>} An iterator over the items in the indicated span
|
|
609
|
-
*/
|
|
610
|
-
function readSpan(vatID, startPos) {
|
|
611
|
-
/** @type {number | undefined} */
|
|
612
|
-
let endPos;
|
|
613
|
-
if (startPos === undefined) {
|
|
614
|
-
({ startPos, endPos } = getCurrentSpanBounds(vatID));
|
|
615
|
-
} else {
|
|
616
|
-
insistTranscriptPosition(startPos);
|
|
617
|
-
endPos = sqlGetSpanEndPos.get(vatID, startPos);
|
|
618
|
-
if (typeof endPos !== 'number') {
|
|
619
|
-
throw Fail`no transcript span for ${q(vatID)} at ${q(startPos)}`;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
|
|
623
|
-
const expectedCount = endPos - startPos;
|
|
624
|
-
|
|
625
|
-
function* reader() {
|
|
626
|
-
let count = 0;
|
|
627
|
-
for (const { item } of sqlReadSpanItems.iterate(
|
|
628
|
-
vatID,
|
|
629
|
-
startPos,
|
|
630
|
-
endPos,
|
|
631
|
-
)) {
|
|
632
|
-
yield item;
|
|
633
|
-
count += 1;
|
|
634
|
-
}
|
|
635
|
-
count === expectedCount ||
|
|
636
|
-
Fail`read ${q(count)} transcript entries (expected ${q(
|
|
637
|
-
expectedCount,
|
|
638
|
-
)})`;
|
|
639
|
-
}
|
|
640
|
-
harden(reader);
|
|
641
|
-
|
|
642
|
-
if (startPos === endPos) {
|
|
643
|
-
return empty();
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
return reader();
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const sqlGetSpanIsCurrent = db.prepare(`
|
|
650
|
-
SELECT isCurrent
|
|
651
|
-
FROM transcriptSpans
|
|
652
|
-
WHERE vatID = ? AND startPos = ?
|
|
653
|
-
`);
|
|
654
|
-
sqlGetSpanIsCurrent.pluck(true);
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Read a transcript span and return it as a stream of data suitable for
|
|
658
|
-
* export to another store. Transcript items are terminated by newlines.
|
|
659
|
-
*
|
|
660
|
-
* Transcript span artifact names should be strings of the form:
|
|
661
|
-
* `transcript.${vatID}.${startPos}.${endPos}`
|
|
662
|
-
*
|
|
663
|
-
* @param {string} name The name of the transcript artifact to be read
|
|
664
|
-
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
665
|
-
* @yields {Uint8Array}
|
|
666
|
-
*/
|
|
667
|
-
async function* exportSpan(name) {
|
|
668
|
-
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
669
|
-
const parts = name.split('.');
|
|
670
|
-
const [type, vatID, pos] = parts;
|
|
671
|
-
// prettier-ignore
|
|
672
|
-
(parts.length === 4 && type === 'transcript') ||
|
|
673
|
-
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
|
|
674
|
-
const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
|
|
675
|
-
isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
|
|
676
|
-
const startPos = Number(pos);
|
|
677
|
-
for (const entry of readSpan(vatID, startPos)) {
|
|
678
|
-
yield Buffer.from(`${entry}\n`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
harden(exportSpan);
|
|
682
|
-
|
|
683
690
|
const sqlAddItem = db.prepare(`
|
|
684
691
|
INSERT INTO transcriptItems (vatID, item, position, incarnation)
|
|
685
692
|
VALUES (?, ?, ?, ?)
|
package/test/deletion.test.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import test from 'ava';
|
|
3
|
-
import path from 'path';
|
|
4
3
|
|
|
5
4
|
import { Buffer } from 'node:buffer';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import zlib from 'node:zlib';
|
|
6
8
|
import sqlite3 from 'better-sqlite3';
|
|
9
|
+
import tmp from 'tmp';
|
|
7
10
|
import { arrayIsLike } from '@agoric/internal/tools/ava-assertions.js';
|
|
8
11
|
import { tmpDir } from './util.js';
|
|
9
12
|
import { initSwingStore } from '../src/swingStore.js';
|
|
13
|
+
import { makeArchiveSnapshot, makeArchiveTranscript } from '../src/archiver.js';
|
|
10
14
|
import { makeSwingStoreExporter } from '../src/exporter.js';
|
|
11
15
|
import { importSwingStore } from '../src/importer.js';
|
|
12
16
|
|
|
@@ -95,7 +99,7 @@ test('delete transcripts with export callback', async t => {
|
|
|
95
99
|
transcriptStore.addItem('v1', 'aaa');
|
|
96
100
|
transcriptStore.addItem('v1', 'bbb');
|
|
97
101
|
transcriptStore.addItem('v1', 'ccc');
|
|
98
|
-
transcriptStore.rolloverSpan('v1');
|
|
102
|
+
await transcriptStore.rolloverSpan('v1');
|
|
99
103
|
transcriptStore.addItem('v1', 'ddd');
|
|
100
104
|
transcriptStore.addItem('v1', 'eee');
|
|
101
105
|
transcriptStore.addItem('v1', 'fff');
|
|
@@ -168,7 +172,17 @@ const setupTranscript = async (t, keepTranscripts) => {
|
|
|
168
172
|
};
|
|
169
173
|
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
170
174
|
t.teardown(cleanup);
|
|
171
|
-
const
|
|
175
|
+
const [archiveDir, cleanupArchives] = await tmpDir('archives');
|
|
176
|
+
t.teardown(cleanupArchives);
|
|
177
|
+
const fsPowers = { fs, path, tmp };
|
|
178
|
+
const archiveSnapshot = makeArchiveSnapshot(archiveDir, fsPowers);
|
|
179
|
+
const archiveTranscript = makeArchiveTranscript(archiveDir, fsPowers);
|
|
180
|
+
const store = initSwingStore(dbDir, {
|
|
181
|
+
exportCallback,
|
|
182
|
+
keepTranscripts,
|
|
183
|
+
archiveSnapshot,
|
|
184
|
+
archiveTranscript,
|
|
185
|
+
});
|
|
172
186
|
const { kernelStorage, hostStorage } = store;
|
|
173
187
|
const { transcriptStore } = kernelStorage;
|
|
174
188
|
const { commit } = hostStorage;
|
|
@@ -179,13 +193,13 @@ const setupTranscript = async (t, keepTranscripts) => {
|
|
|
179
193
|
transcriptStore.initTranscript(vatID);
|
|
180
194
|
transcriptStore.addItem(vatID, 'aaa');
|
|
181
195
|
transcriptStore.addItem(vatID, 'bbb');
|
|
182
|
-
transcriptStore.rolloverSpan(vatID);
|
|
196
|
+
await transcriptStore.rolloverSpan(vatID);
|
|
183
197
|
transcriptStore.addItem(vatID, 'ccc');
|
|
184
198
|
transcriptStore.addItem(vatID, 'ddd');
|
|
185
|
-
transcriptStore.rolloverIncarnation(vatID);
|
|
199
|
+
await transcriptStore.rolloverIncarnation(vatID);
|
|
186
200
|
transcriptStore.addItem(vatID, 'eee');
|
|
187
201
|
transcriptStore.addItem(vatID, 'fff');
|
|
188
|
-
transcriptStore.rolloverSpan(vatID);
|
|
202
|
+
await transcriptStore.rolloverSpan(vatID);
|
|
189
203
|
transcriptStore.addItem(vatID, 'ggg');
|
|
190
204
|
transcriptStore.addItem(vatID, 'hhh');
|
|
191
205
|
await commit();
|
|
@@ -193,6 +207,7 @@ const setupTranscript = async (t, keepTranscripts) => {
|
|
|
193
207
|
return {
|
|
194
208
|
db,
|
|
195
209
|
dbDir,
|
|
210
|
+
archiveDir,
|
|
196
211
|
commit,
|
|
197
212
|
transcriptStore,
|
|
198
213
|
exportLog,
|
|
@@ -212,6 +227,7 @@ const execSlowTranscriptDeletion = async (t, { keepTranscripts }) => {
|
|
|
212
227
|
const {
|
|
213
228
|
db,
|
|
214
229
|
dbDir,
|
|
230
|
+
archiveDir,
|
|
215
231
|
commit,
|
|
216
232
|
transcriptStore,
|
|
217
233
|
exportLog,
|
|
@@ -241,12 +257,13 @@ const execSlowTranscriptDeletion = async (t, { keepTranscripts }) => {
|
|
|
241
257
|
'transcript.v1.4': t4,
|
|
242
258
|
'transcript.v1.6': t6,
|
|
243
259
|
};
|
|
244
|
-
const
|
|
245
|
-
'transcript.v1.0.2',
|
|
246
|
-
'transcript.v1.2.4',
|
|
247
|
-
'transcript.v1.4.6',
|
|
248
|
-
'transcript.v1.6.8',
|
|
249
|
-
|
|
260
|
+
const expectedArtifactContents = {
|
|
261
|
+
'transcript.v1.0.2': 'aaa\nbbb\n',
|
|
262
|
+
'transcript.v1.2.4': 'ccc\nddd\n',
|
|
263
|
+
'transcript.v1.4.6': 'eee\nfff\n',
|
|
264
|
+
'transcript.v1.6.8': 'ggg\nhhh\n',
|
|
265
|
+
};
|
|
266
|
+
const expectedArtifactNames = Object.keys(expectedArtifactContents);
|
|
250
267
|
|
|
251
268
|
t.deepEqual(stripHashes(currentExportData), expectedLiveExportData);
|
|
252
269
|
t.is(
|
|
@@ -255,6 +272,17 @@ const execSlowTranscriptDeletion = async (t, { keepTranscripts }) => {
|
|
|
255
272
|
);
|
|
256
273
|
t.is(db.prepare('SELECT COUNT(*) FROM transcriptSpans').pluck().get(), 4);
|
|
257
274
|
|
|
275
|
+
// verify archived transcripts
|
|
276
|
+
t.deepEqual(
|
|
277
|
+
fs.readdirSync(archiveDir),
|
|
278
|
+
expectedArtifactNames.slice(0, -1).map(name => `${name}.gz`),
|
|
279
|
+
);
|
|
280
|
+
for (const name of expectedArtifactNames.slice(0, -1)) {
|
|
281
|
+
const filePath = path.join(archiveDir, `${name}.gz`);
|
|
282
|
+
const contents = zlib.gunzipSync(fs.readFileSync(filePath)).toString();
|
|
283
|
+
t.is(contents, expectedArtifactContents[name], `${filePath} contents`);
|
|
284
|
+
}
|
|
285
|
+
|
|
258
286
|
// an "operational"-mode export should list all spans, but only have
|
|
259
287
|
// artifacts for the current one
|
|
260
288
|
{
|
|
@@ -284,15 +312,24 @@ const execSlowTranscriptDeletion = async (t, { keepTranscripts }) => {
|
|
|
284
312
|
// deletes the .current record (i.e. it transforms .current into a
|
|
285
313
|
// closed record)
|
|
286
314
|
{
|
|
287
|
-
transcriptStore.stopUsingTranscript(vatID);
|
|
315
|
+
await transcriptStore.stopUsingTranscript(vatID);
|
|
288
316
|
await commit();
|
|
289
317
|
t.deepEqual(stripHashes(currentExportData), expectedStoppedExportData);
|
|
290
318
|
exportLog.length = 0;
|
|
291
319
|
// stopUsingTranscript is idempotent
|
|
292
|
-
transcriptStore.stopUsingTranscript(vatID);
|
|
320
|
+
await transcriptStore.stopUsingTranscript(vatID);
|
|
293
321
|
await commit();
|
|
294
322
|
t.deepEqual(exportLog, []);
|
|
295
323
|
}
|
|
324
|
+
t.deepEqual(
|
|
325
|
+
fs.readdirSync(archiveDir),
|
|
326
|
+
expectedArtifactNames.map(name => `${name}.gz`),
|
|
327
|
+
);
|
|
328
|
+
for (const name of expectedArtifactNames) {
|
|
329
|
+
const filePath = path.join(archiveDir, `${name}.gz`);
|
|
330
|
+
const contents = zlib.gunzipSync(fs.readFileSync(filePath)).toString();
|
|
331
|
+
t.is(contents, expectedArtifactContents[name], `${filePath} contents`);
|
|
332
|
+
}
|
|
296
333
|
|
|
297
334
|
// All exports (debug and non-debug) in this "terminated but not
|
|
298
335
|
// deleted" state will still have the export-data keys. Only
|
package/test/export.test.js
CHANGED
|
@@ -47,7 +47,7 @@ const exportTest = test.macro(async (t, mode) => {
|
|
|
47
47
|
// incarnation 0
|
|
48
48
|
ks.transcriptStore.addItem('v1', 'start-worker'); // 0
|
|
49
49
|
ks.transcriptStore.addItem('v1', 'shutdown-worker'); // 1
|
|
50
|
-
ks.transcriptStore.rolloverIncarnation('v1');
|
|
50
|
+
await ks.transcriptStore.rolloverIncarnation('v1');
|
|
51
51
|
const spanHash0 =
|
|
52
52
|
'5bee0f44eca02f23eab03703e84ed2647d5d117fed99e1c30a3b424b7f082ab9';
|
|
53
53
|
|
|
@@ -56,7 +56,7 @@ const exportTest = test.macro(async (t, mode) => {
|
|
|
56
56
|
ks.transcriptStore.addItem('v1', 'delivery1'); // 3
|
|
57
57
|
await ks.snapStore.saveSnapshot('v1', 4, getSnapshotStream(snapshotData));
|
|
58
58
|
ks.transcriptStore.addItem('v1', 'save-snapshot'); // 4
|
|
59
|
-
ks.transcriptStore.rolloverSpan('v1'); // range= 2..5
|
|
59
|
+
await ks.transcriptStore.rolloverSpan('v1'); // range= 2..5
|
|
60
60
|
const spanHash1 =
|
|
61
61
|
'57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
|
|
62
62
|
|
|
@@ -64,7 +64,7 @@ const exportTest = test.macro(async (t, mode) => {
|
|
|
64
64
|
ks.transcriptStore.addItem('v1', 'delivery2'); // 6
|
|
65
65
|
await ks.snapStore.saveSnapshot('v1', 7, getSnapshotStream(snapshotData));
|
|
66
66
|
ks.transcriptStore.addItem('v1', 'save-snapshot'); // 7
|
|
67
|
-
ks.transcriptStore.rolloverSpan('v1'); // range= 5..8
|
|
67
|
+
await ks.transcriptStore.rolloverSpan('v1'); // range= 5..8
|
|
68
68
|
const spanHash2 =
|
|
69
69
|
'1947001e78e01bd1e773feb22b4ffc530447373b9de9274d5d5fbda3f23dbf2b';
|
|
70
70
|
|
|
@@ -251,7 +251,7 @@ test('export omits pruned span artifacts', async t => {
|
|
|
251
251
|
ks.transcriptStore.addItem('v1', 'delivery1'); // 1
|
|
252
252
|
await ks.snapStore.saveSnapshot('v1', 2, getSnapshotStream(snapshotData));
|
|
253
253
|
ks.transcriptStore.addItem('v1', 'save-snapshot'); // 2
|
|
254
|
-
ks.transcriptStore.rolloverSpan('v1'); // range= 0..3
|
|
254
|
+
await ks.transcriptStore.rolloverSpan('v1'); // range= 0..3
|
|
255
255
|
const spanHash1 =
|
|
256
256
|
'57152efdd7fdf75c03371d2b4f1088d5bf3eae7fe643babce527ff81df38998c';
|
|
257
257
|
// rolloverSpan prunes the contents of the old span
|
|
@@ -87,7 +87,7 @@ async function fakeAVatSnapshot(vat, ks) {
|
|
|
87
87
|
await ks.snapStore.saveSnapshot(vat.vatID, vat.endPos, getSnapshotStream());
|
|
88
88
|
ks.transcriptStore.addItem(vat.vatID, 'save-snapshot');
|
|
89
89
|
vat.endPos += 1;
|
|
90
|
-
ks.transcriptStore.rolloverSpan(vat.vatID);
|
|
90
|
+
await ks.transcriptStore.rolloverSpan(vat.vatID);
|
|
91
91
|
ks.transcriptStore.addItem(vat.vatID, 'load-snapshot');
|
|
92
92
|
vat.endPos += 1;
|
|
93
93
|
}
|
package/test/snapstore.test.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import test from 'ava';
|
|
2
3
|
|
|
3
4
|
import { Buffer } from 'node:buffer';
|
|
4
|
-
import
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import zlib from 'node:zlib';
|
|
5
8
|
import sqlite3 from 'better-sqlite3';
|
|
9
|
+
import tmp from 'tmp';
|
|
6
10
|
|
|
7
|
-
import test from 'ava';
|
|
8
11
|
import { makeMeasureSeconds } from '@agoric/internal';
|
|
9
12
|
import { makeSnapStore } from '../src/snapStore.js';
|
|
13
|
+
import { makeArchiveSnapshot } from '../src/archiver.js';
|
|
14
|
+
import { tmpDir } from './util.js';
|
|
10
15
|
|
|
11
16
|
function makeExportLog() {
|
|
12
17
|
const exportLog = [];
|
|
@@ -31,6 +36,10 @@ harden(getSnapshotStream);
|
|
|
31
36
|
test('compress to cache file; closes snapshot stream', async t => {
|
|
32
37
|
const db = sqlite3(':memory:');
|
|
33
38
|
const exportLog = makeExportLog();
|
|
39
|
+
const [archiveDir, cleanupArchives] = await tmpDir('archives');
|
|
40
|
+
t.teardown(cleanupArchives);
|
|
41
|
+
const fsPowers = { fs, path, tmp };
|
|
42
|
+
const archiveSnapshot = makeArchiveSnapshot(archiveDir, fsPowers);
|
|
34
43
|
const store = makeSnapStore(
|
|
35
44
|
db,
|
|
36
45
|
ensureTxn,
|
|
@@ -38,6 +47,7 @@ test('compress to cache file; closes snapshot stream', async t => {
|
|
|
38
47
|
measureSeconds: makeMeasureSeconds(() => 0),
|
|
39
48
|
},
|
|
40
49
|
exportLog.noteExport,
|
|
50
|
+
{ archiveSnapshot },
|
|
41
51
|
);
|
|
42
52
|
|
|
43
53
|
const snapshotStream = getSnapshotStream('abc');
|
|
@@ -50,6 +60,7 @@ test('compress to cache file; closes snapshot stream', async t => {
|
|
|
50
60
|
uncompressedSize: 3,
|
|
51
61
|
compressedSize: 23,
|
|
52
62
|
dbSaveSeconds: 0,
|
|
63
|
+
archiveWriteSeconds: 0,
|
|
53
64
|
compressSeconds: 0,
|
|
54
65
|
});
|
|
55
66
|
const snapshotInfo = store.getSnapshotInfo('fakeVatID');
|
|
@@ -89,6 +100,26 @@ test('compress to cache file; closes snapshot stream', async t => {
|
|
|
89
100
|
['snapshot.fakeVatID.47', JSON.stringify(logInfo)],
|
|
90
101
|
['snapshot.fakeVatID.current', `snapshot.fakeVatID.47`],
|
|
91
102
|
]);
|
|
103
|
+
|
|
104
|
+
// verify disk archive
|
|
105
|
+
t.deepEqual(
|
|
106
|
+
fs.readdirSync(archiveDir),
|
|
107
|
+
['snapshot.fakeVatID.47.gz'],
|
|
108
|
+
'archive must be written to disk',
|
|
109
|
+
);
|
|
110
|
+
const fileContents = fs.readFileSync(
|
|
111
|
+
path.join(archiveDir, 'snapshot.fakeVatID.47.gz'),
|
|
112
|
+
);
|
|
113
|
+
t.is(
|
|
114
|
+
fileContents.length,
|
|
115
|
+
dbInfo.compressedSize,
|
|
116
|
+
'file size must match database compressedSize',
|
|
117
|
+
);
|
|
118
|
+
t.is(
|
|
119
|
+
zlib.gunzipSync(fileContents).toString(),
|
|
120
|
+
'abc',
|
|
121
|
+
'gunzip(fileContents) must match input data',
|
|
122
|
+
);
|
|
92
123
|
});
|
|
93
124
|
|
|
94
125
|
test('snapStore prepare / commit delete is robust', async t => {
|
|
@@ -96,11 +127,17 @@ test('snapStore prepare / commit delete is robust', async t => {
|
|
|
96
127
|
measureSeconds: makeMeasureSeconds(() => 0),
|
|
97
128
|
};
|
|
98
129
|
const db = sqlite3(':memory:');
|
|
130
|
+
const [archiveDir, cleanupArchives] = await tmpDir('archives');
|
|
131
|
+
t.teardown(cleanupArchives);
|
|
132
|
+
const fsPowers = { fs, path, tmp };
|
|
133
|
+
const archiveSnapshot = makeArchiveSnapshot(archiveDir, fsPowers);
|
|
99
134
|
const store = makeSnapStore(db, ensureTxn, io, () => {}, {
|
|
100
135
|
keepSnapshots: true,
|
|
136
|
+
archiveSnapshot,
|
|
101
137
|
});
|
|
102
138
|
|
|
103
139
|
const hashes = [];
|
|
140
|
+
const expectedFiles = [];
|
|
104
141
|
for (let i = 0; i < 5; i += 1) {
|
|
105
142
|
const { hash } = await store.saveSnapshot(
|
|
106
143
|
'fakeVatID2',
|
|
@@ -108,6 +145,7 @@ test('snapStore prepare / commit delete is robust', async t => {
|
|
|
108
145
|
getSnapshotStream(`file ${i}`),
|
|
109
146
|
);
|
|
110
147
|
hashes.push(hash);
|
|
148
|
+
expectedFiles.push(`snapshot.fakeVatID2.${i}.gz`);
|
|
111
149
|
}
|
|
112
150
|
const sqlCountSnapshots = db.prepare(`
|
|
113
151
|
SELECT COUNT(*)
|
|
@@ -116,19 +154,24 @@ test('snapStore prepare / commit delete is robust', async t => {
|
|
|
116
154
|
sqlCountSnapshots.pluck(true);
|
|
117
155
|
|
|
118
156
|
t.is(sqlCountSnapshots.get(), 5);
|
|
157
|
+
t.deepEqual(fs.readdirSync(archiveDir), expectedFiles);
|
|
119
158
|
|
|
120
159
|
store.deleteSnapshotByHash('fakeVatID2', hashes[2]);
|
|
121
160
|
t.is(sqlCountSnapshots.get(), 4);
|
|
161
|
+
t.deepEqual(fs.readdirSync(archiveDir), expectedFiles);
|
|
122
162
|
|
|
123
163
|
// Restore (re-save) between prepare and commit.
|
|
124
164
|
store.deleteSnapshotByHash('fakeVatID2', hashes[3]);
|
|
125
165
|
await store.saveSnapshot('fakeVatID3', 29, getSnapshotStream(`file 3`));
|
|
166
|
+
expectedFiles.push(`snapshot.fakeVatID3.29.gz`);
|
|
126
167
|
t.true(store.hasHash('fakeVatID3', hashes[3]));
|
|
168
|
+
t.deepEqual(fs.readdirSync(archiveDir), expectedFiles);
|
|
127
169
|
|
|
128
170
|
store.deleteVatSnapshots('fakeVatID2');
|
|
129
171
|
t.is(sqlCountSnapshots.get(), 1);
|
|
130
172
|
store.deleteVatSnapshots('fakeVatID3');
|
|
131
173
|
t.is(sqlCountSnapshots.get(), 0);
|
|
174
|
+
t.deepEqual(fs.readdirSync(archiveDir), expectedFiles);
|
|
132
175
|
|
|
133
176
|
for (let i = 0; i < 5; i += 1) {
|
|
134
177
|
const { hash } = await store.saveSnapshot(
|
|
@@ -137,8 +180,10 @@ test('snapStore prepare / commit delete is robust', async t => {
|
|
|
137
180
|
getSnapshotStream(`file ${i}`),
|
|
138
181
|
);
|
|
139
182
|
hashes.push(hash);
|
|
183
|
+
expectedFiles.push(`snapshot.fakeVatID4.${i}.gz`);
|
|
140
184
|
}
|
|
141
185
|
t.is(sqlCountSnapshots.get(), 5);
|
|
142
186
|
store.deleteAllUnusedSnapshots();
|
|
143
187
|
t.is(sqlCountSnapshots.get(), 1);
|
|
188
|
+
t.deepEqual(fs.readdirSync(archiveDir), expectedFiles);
|
|
144
189
|
});
|
package/test/state.test.js
CHANGED
|
@@ -181,10 +181,10 @@ const testTranscriptStore = test.macro({
|
|
|
181
181
|
transcriptStore.initTranscript('st1');
|
|
182
182
|
transcriptStore.initTranscript('st2');
|
|
183
183
|
transcriptStore.addItem('st1', 'zeroth');
|
|
184
|
-
transcriptStore.rolloverSpan('st1');
|
|
184
|
+
await transcriptStore.rolloverSpan('st1');
|
|
185
185
|
transcriptStore.addItem('st1', 'first');
|
|
186
186
|
transcriptStore.addItem('st1', 'second');
|
|
187
|
-
transcriptStore.rolloverSpan('st1');
|
|
187
|
+
await transcriptStore.rolloverSpan('st1');
|
|
188
188
|
transcriptStore.addItem('st1', 'third');
|
|
189
189
|
transcriptStore.addItem('st2', 'oneth');
|
|
190
190
|
transcriptStore.addItem('st1', 'fourth');
|