@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.
@@ -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
- `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`).
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-bf1e645.0+bf1e645",
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-bf1e645.0+bf1e645",
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": "bf1e6453eee701f3e465120a03877cf44d458cc6"
54
+ "gitHead": "07f6885c560333981d6564581b2a00735a6c428c"
55
55
  }
@@ -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, and reports information about the process, including
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
- const cleanup = [];
209
- return aggregateTryFinally(
210
- async () => {
211
- const hashStream = createHash('sha256');
212
- const gzip = createGzip();
213
- let compressedSize = 0;
214
- let uncompressedSize = 0;
215
-
216
- const { duration: compressSeconds, result: compressedSnapshot } =
217
- await measureSeconds(async () => {
218
- const snapReader = Readable.from(snapshotStream);
219
- cleanup.push(
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
- const hash = hashStream.digest('hex');
239
-
240
- const { duration: dbSaveSeconds } = await measureSeconds(async () => {
241
- ensureTxn();
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
- return harden({
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 { traceFile, keepSnapshots, keepTranscripts } = options;
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(