@agoric/swing-store 0.9.2-upgrade-16-dev-0df76a7.0 → 0.9.2-upgrade-17-dev-e67cd91.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/src/snapStore.js CHANGED
@@ -3,11 +3,8 @@ import { createHash } from 'crypto';
3
3
  import { finished as finishedCallback, PassThrough, Readable } from 'stream';
4
4
  import { promisify } from 'util';
5
5
  import { createGzip, createGunzip } from 'zlib';
6
- import { Fail, q } from '@agoric/assert';
7
- import {
8
- aggregateTryFinally,
9
- PromiseAllOrErrors,
10
- } from '@agoric/internal/src/node/utils.js';
6
+ import { Fail, q } from '@endo/errors';
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
  /**
@@ -39,7 +37,7 @@ import { buffer } from './util.js';
39
37
  * loadSnapshot: (vatID: string) => AsyncIterableIterator<Uint8Array>,
40
38
  * saveSnapshot: (vatID: string, snapPos: number, snapshotStream: AsyncIterable<Uint8Array>) => Promise<SnapshotResult>,
41
39
  * deleteAllUnusedSnapshots: () => void,
42
- * deleteVatSnapshots: (vatID: string) => void,
40
+ * deleteVatSnapshots: (vatID: string, budget?: number) => { done: boolean, cleanups: number },
43
41
  * stopUsingLastSnapshot: (vatID: string) => void,
44
42
  * getSnapshotInfo: (vatID: string) => SnapshotInfo,
45
43
  * }} SnapStore
@@ -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 (
@@ -173,11 +172,13 @@ export function makeSnapStore(
173
172
  `);
174
173
 
175
174
  function stopUsingLastSnapshot(vatID) {
175
+ // idempotent
176
176
  ensureTxn();
177
177
  const oldInfo = sqlGetPriorSnapshotInfo.get(vatID);
178
178
  if (oldInfo) {
179
179
  const rec = snapshotRec(vatID, oldInfo.snapPos, oldInfo.hash, 0);
180
180
  noteExport(snapshotMetadataKey(rec), JSON.stringify(rec));
181
+ noteExport(currentSnapshotMetadataKey(rec), undefined);
181
182
  if (keepSnapshots) {
182
183
  sqlStopUsingLastSnapshot.run(vatID);
183
184
  } else {
@@ -194,7 +195,8 @@ export function makeSnapStore(
194
195
 
195
196
  /**
196
197
  * Generates a new XS heap snapshot, stores a gzipped copy of it into the
197
- * 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
198
200
  * snapshot size and timing metrics.
199
201
  *
200
202
  * @param {string} vatID
@@ -203,74 +205,62 @@ export function makeSnapStore(
203
205
  * @returns {Promise<SnapshotResult>}
204
206
  */
205
207
  async function saveSnapshot(vatID, snapPos, snapshotStream) {
206
- const cleanup = [];
207
- return aggregateTryFinally(
208
- async () => {
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
- cleanup.push(
218
- () =>
219
- new Promise((resolve, reject) =>
220
- snapReader.destroy(
221
- null,
222
- // @ts-expect-error incorrect types
223
- err => (err ? reject(err) : resolve()),
224
- ),
225
- ),
226
- );
227
-
228
- snapReader.on('data', chunk => {
229
- uncompressedSize += chunk.length;
230
- });
231
- snapReader.pipe(hashStream);
232
- const compressedSnapshotData = await buffer(snapReader.pipe(gzip));
233
- await finished(snapReader);
234
- 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;
235
221
  });
236
- const hash = hashStream.digest('hex');
237
-
238
- const { duration: dbSaveSeconds } = await measureSeconds(async () => {
239
- ensureTxn();
240
- stopUsingLastSnapshot(vatID);
241
- compressedSize = compressedSnapshot.length;
242
- sqlSaveSnapshot.run(
243
- vatID,
244
- snapPos,
245
- 1,
246
- hash,
247
- uncompressedSize,
248
- compressedSize,
249
- compressedSnapshot,
250
- );
251
- const rec = snapshotRec(vatID, snapPos, hash, 1);
252
- const exportKey = snapshotMetadataKey(rec);
253
- noteExport(exportKey, JSON.stringify(rec));
254
- noteExport(
255
- currentSnapshotMetadataKey(rec),
256
- snapshotArtifactName(rec),
257
- );
222
+ snapReader.pipe(hashStream);
223
+ const compressedSnapshotData = await buffer(snapReader.pipe(gzip));
224
+ await finished(snapReader);
225
+ return compressedSnapshotData;
258
226
  });
227
+ const hash = hashStream.digest('hex');
228
+ const rec = snapshotRec(vatID, snapPos, hash, 1);
229
+ const exportKey = snapshotMetadataKey(rec);
259
230
 
260
- 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,
261
239
  hash,
262
240
  uncompressedSize,
263
- compressSeconds,
264
- dbSaveSeconds,
265
241
  compressedSize,
266
- });
267
- },
268
- async () => {
269
- await PromiseAllOrErrors(
270
- cleanup.reverse().map(fn => Promise.resolve().then(() => fn())),
242
+ compressedSnapshot,
271
243
  );
272
- },
273
- );
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
+ });
274
264
  }
275
265
 
276
266
  const sqlGetSnapshot = db.prepare(`
@@ -354,28 +344,74 @@ export function makeSnapStore(
354
344
  WHERE vatID = ?
355
345
  `);
356
346
 
347
+ const sqlDeleteOneVatSnapshot = db.prepare(`
348
+ DELETE FROM snapshots
349
+ WHERE vatID = ? AND snapPos = ?
350
+ `);
351
+
357
352
  const sqlGetSnapshotList = db.prepare(`
358
353
  SELECT snapPos
359
354
  FROM snapshots
360
355
  WHERE vatID = ?
361
356
  ORDER BY snapPos
362
357
  `);
363
- sqlGetSnapshotList.pluck(true);
358
+
359
+ const sqlGetSnapshotListLimited = db.prepare(`
360
+ SELECT snapPos, inUse
361
+ FROM snapshots
362
+ WHERE vatID = ?
363
+ ORDER BY snapPos DESC
364
+ LIMIT ?
365
+ `);
364
366
 
365
367
  /**
366
- * Delete all snapshots for a given vat (for use when, e.g., a vat is terminated)
368
+ * @param {string} vatID
369
+ * @returns {boolean}
370
+ */
371
+ function hasSnapshots(vatID) {
372
+ // the LIMIT 1 means we aren't really getting all entries
373
+ return sqlGetSnapshotListLimited.all(vatID, 1).length > 0;
374
+ }
375
+
376
+ /**
377
+ * Delete some or all snapshots for a given vat (for use when, e.g.,
378
+ * a vat is terminated)
367
379
  *
368
380
  * @param {string} vatID
381
+ * @param {number} [budget]
382
+ * @returns {{ done: boolean, cleanups: number }}
369
383
  */
370
- function deleteVatSnapshots(vatID) {
384
+ function deleteVatSnapshots(vatID, budget = Infinity) {
371
385
  ensureTxn();
372
- const deletions = sqlGetSnapshotList.all(vatID);
373
- for (const snapPos of deletions) {
386
+ const deleteAll = budget === Infinity;
387
+ assert(deleteAll || budget >= 1, 'budget must be undefined or positive');
388
+ // We can't use .iterate because noteExport can write to the DB,
389
+ // and overlapping queries are not supported.
390
+ const deletions = deleteAll
391
+ ? sqlGetSnapshotList.all(vatID)
392
+ : sqlGetSnapshotListLimited.all(vatID, budget);
393
+ let clearCurrent = deleteAll;
394
+ for (const deletion of deletions) {
395
+ clearCurrent ||= deletion.inUse;
396
+ const { snapPos } = deletion;
374
397
  const exportRec = snapshotRec(vatID, snapPos, undefined);
375
398
  noteExport(snapshotMetadataKey(exportRec), undefined);
399
+ // Budgeted deletion must delete rows one by one,
400
+ // but full deletion is handled all at once after this loop.
401
+ if (!deleteAll) {
402
+ sqlDeleteOneVatSnapshot.run(vatID, snapPos);
403
+ }
404
+ }
405
+ if (deleteAll) {
406
+ sqlDeleteVatSnapshots.run(vatID);
407
+ }
408
+ if (clearCurrent) {
409
+ noteExport(currentSnapshotMetadataKey({ vatID }), undefined);
376
410
  }
377
- noteExport(currentSnapshotMetadataKey({ vatID }), undefined);
378
- sqlDeleteVatSnapshots.run(vatID);
411
+ return {
412
+ done: deleteAll || deletions.length === 0 || !hasSnapshots(vatID),
413
+ cleanups: deletions.length,
414
+ };
379
415
  }
380
416
 
381
417
  const sqlGetSnapshotInfo = db.prepare(`
@@ -452,7 +488,7 @@ export function makeSnapStore(
452
488
  `);
453
489
 
454
490
  /**
455
- * Obtain artifact metadata records for spanshots contained in this store.
491
+ * Obtain artifact metadata records for snapshots contained in this store.
456
492
  *
457
493
  * @param {boolean} includeHistorical If true, include all metadata that is
458
494
  * present in the store regardless of its currency; if false, only include
package/src/swingStore.js CHANGED
@@ -5,7 +5,7 @@ import * as path from 'path';
5
5
 
6
6
  import sqlite3 from 'better-sqlite3';
7
7
 
8
- import { Fail, q } from '@agoric/assert';
8
+ import { Fail, q } from '@endo/errors';
9
9
 
10
10
  import { dbFileInDirectory } from './util.js';
11
11
  import { makeKVStore, getKeyType } from './kvStore.js';
@@ -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(
@@ -554,6 +562,7 @@ export function makeSwingStore(dirPath, forceReset, options = {}) {
554
562
  getCurrentSpanBounds: transcriptStore.getCurrentSpanBounds,
555
563
  addItem: transcriptStore.addItem,
556
564
  readSpan: transcriptStore.readSpan,
565
+ stopUsingTranscript: transcriptStore.stopUsingTranscript,
557
566
  deleteVatTranscripts: transcriptStore.deleteVatTranscripts,
558
567
  };
559
568