@agoric/swing-store 0.9.2-dev-bf1e645.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 +167 -145
- package/test/deletion.test.js +182 -131
- 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(
|