@agoric/swing-store 0.9.2-dev-0c6ee36.0.0c6ee36 → 0.9.2-dev-f56a063.0.f56a063

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swing-store",
3
- "version": "0.9.2-dev-0c6ee36.0.0c6ee36",
3
+ "version": "0.9.2-dev-f56a063.0.f56a063",
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": "yarn run -T eslint ."
22
22
  },
23
23
  "dependencies": {
24
- "@agoric/internal": "0.3.3-dev-0c6ee36.0.0c6ee36",
24
+ "@agoric/internal": "0.3.3-dev-f56a063.0.f56a063",
25
25
  "@endo/base64": "^1.0.12",
26
26
  "@endo/bundle-source": "^4.1.2",
27
27
  "@endo/check-bundle": "^1.0.17",
@@ -31,6 +31,8 @@
31
31
  },
32
32
  "devDependencies": {
33
33
  "@endo/init": "^1.1.12",
34
+ "@endo/promise-kit": "^1.1.13",
35
+ "@endo/stream": "^1.2.13",
34
36
  "@types/better-sqlite3": "^7.6.13",
35
37
  "ava": "^5.3.0",
36
38
  "c8": "^10.1.3",
@@ -51,5 +53,5 @@
51
53
  "typeCoverage": {
52
54
  "atLeast": 79.33
53
55
  },
54
- "gitHead": "0c6ee3656ebd5fe23027f792516d4d6fa5e8ae06"
56
+ "gitHead": "f56a0631538b0020cc894a564a17356224bfd543"
55
57
  }
package/src/archiver.js CHANGED
@@ -4,9 +4,13 @@ import { promisify } from 'node:util';
4
4
  import { createGzip } from 'node:zlib';
5
5
  import { withDeferredCleanup } from '@agoric/internal';
6
6
 
7
+ /**
8
+ * @import {AnyIterable} from './exporter.js';
9
+ */
10
+
7
11
  const streamFinished = promisify(streamFinishedCallback);
8
12
 
9
- /*
13
+ /**
10
14
  * @param {string} dirPath
11
15
  * @param {object} powers
12
16
  * @param {Pick<import('fs'), 'createWriteStream' | 'mkdirSync' | 'renameSync'>} powers.fs
@@ -16,6 +20,11 @@ const streamFinished = promisify(streamFinishedCallback);
16
20
  export const makeArchiveSnapshot = (dirPath, powers) => {
17
21
  const { fs, path, tmp } = powers;
18
22
  fs.mkdirSync(dirPath, { recursive: true });
23
+ /**
24
+ * @param {string} name
25
+ * @param {AnyIterable<Uint8Array>} gzData
26
+ * @returns {Promise<void>}
27
+ */
19
28
  const archiveSnapshot = (name, gzData) => {
20
29
  const destPath = path.join(dirPath, `${name}.gz`);
21
30
  return withDeferredCleanup(async addCleanup => {
@@ -29,7 +38,7 @@ export const makeArchiveSnapshot = (dirPath, powers) => {
29
38
  postfix: '.gz.tmp',
30
39
  detachDescriptor: true,
31
40
  });
32
- addCleanup(() => removeCallback());
41
+ addCleanup(async () => removeCallback());
33
42
  const writer = fs.createWriteStream('', { fd, flush: true });
34
43
  const reader = Readable.from(gzData);
35
44
  const destroyReader = promisify(reader.destroy.bind(reader));
@@ -43,7 +52,7 @@ export const makeArchiveSnapshot = (dirPath, powers) => {
43
52
  };
44
53
  harden(makeArchiveSnapshot);
45
54
 
46
- /*
55
+ /**
47
56
  * @param {string} dirPath
48
57
  * @param {object} powers
49
58
  * @param {Pick<import('fs'), 'createWriteStream' | 'mkdirSync' | 'renameSync'>} powers.fs
@@ -53,6 +62,11 @@ harden(makeArchiveSnapshot);
53
62
  export const makeArchiveTranscript = (dirPath, powers) => {
54
63
  const { fs, path, tmp } = powers;
55
64
  fs.mkdirSync(dirPath, { recursive: true });
65
+ /**
66
+ * @param {string} spanName
67
+ * @param {AnyIterable<Uint8Array>} entries
68
+ * @returns {Promise<void>}
69
+ */
56
70
  const archiveTranscript = (spanName, entries) => {
57
71
  const destPath = path.join(dirPath, `${spanName}.gz`);
58
72
  return withDeferredCleanup(async addCleanup => {
@@ -66,7 +80,7 @@ export const makeArchiveTranscript = (dirPath, powers) => {
66
80
  postfix: '.gz.tmp',
67
81
  detachDescriptor: true,
68
82
  });
69
- addCleanup(() => removeCallback());
83
+ addCleanup(async () => removeCallback());
70
84
  await pipeline(
71
85
  entries,
72
86
  createGzip(),
@@ -331,6 +331,16 @@ export function makeTranscriptStore(
331
331
  * @yields {Uint8Array}
332
332
  */
333
333
  async function* exportSpan(name) {
334
+ yield* syncExportSpan(name);
335
+ }
336
+ harden(exportSpan);
337
+
338
+ /**
339
+ * @param {string} name The name of the transcript artifact to be read
340
+ * @returns {IterableIterator<Uint8Array>}
341
+ * @yields {Uint8Array}
342
+ */
343
+ function* syncExportSpan(name) {
334
344
  typeof name === 'string' || Fail`artifact name must be a string`;
335
345
  const parts = name.split('.');
336
346
  const [type, vatID, pos] = parts;
@@ -344,7 +354,6 @@ export function makeTranscriptStore(
344
354
  yield Buffer.from(`${entry}\n`);
345
355
  }
346
356
  }
347
- harden(exportSpan);
348
357
 
349
358
  const sqlEndCurrentSpan = db.prepare(`
350
359
  UPDATE transcriptSpans
@@ -366,8 +375,9 @@ export function makeTranscriptStore(
366
375
  *
367
376
  * @param {string} vatID
368
377
  * @param {ReturnType<getCurrentSpanBounds>} bounds
378
+ * @returns {{deferredTask: Promise<void>}}
369
379
  */
370
- async function closeSpan(vatID, bounds) {
380
+ function closeSpan(vatID, bounds) {
371
381
  ensureTxn();
372
382
  const { startPos, endPos, hash, incarnation } = bounds;
373
383
  const rec = spanRec(vatID, startPos, endPos, hash, false, incarnation);
@@ -378,11 +388,11 @@ export function makeTranscriptStore(
378
388
  // and change its DB row to isCurrent=null
379
389
  sqlEndCurrentSpan.run(vatID);
380
390
 
381
- await null;
391
+ let archiveResult = Promise.resolve();
382
392
  if (archiveTranscript) {
383
393
  const spanName = spanArtifactName(rec);
384
- const entries = exportSpan(spanName);
385
- await archiveTranscript(spanName, entries);
394
+ const entries = [...syncExportSpan(spanName)];
395
+ archiveResult = archiveTranscript(spanName, entries);
386
396
  }
387
397
  if (!keepTranscripts) {
388
398
  // Delete items of the previously-current span.
@@ -393,15 +403,23 @@ export function makeTranscriptStore(
393
403
  // that doesn't include them.
394
404
  sqlDeleteOldItems.run(vatID, startPos, endPos);
395
405
  }
406
+
407
+ return harden({ deferredTask: archiveResult });
396
408
  }
397
409
 
410
+ /**
411
+ * This function guarantees that all store operations are done synchronously.
412
+ *
413
+ * @param {string} vatID
414
+ * @param {boolean} isNewIncarnation
415
+ */
398
416
  async function doSpanRollover(vatID, isNewIncarnation) {
399
417
  ensureTxn();
400
418
  const bounds = getCurrentSpanBounds(vatID);
401
419
  const { endPos, incarnation } = bounds;
402
420
 
403
421
  // deal with the now-old span
404
- await closeSpan(vatID, bounds);
422
+ const { deferredTask } = closeSpan(vatID, bounds);
405
423
 
406
424
  // create a new (empty) DB row, with isCurrent=1
407
425
  const newSpanIncarnation = isNewIncarnation ? incarnation + 1 : incarnation;
@@ -418,6 +436,8 @@ export function makeTranscriptStore(
418
436
  );
419
437
  noteExport(spanMetadataKey(rec), JSON.stringify(rec));
420
438
 
439
+ await deferredTask;
440
+
421
441
  return newSpanIncarnation;
422
442
  }
423
443
 
@@ -425,6 +445,8 @@ export function makeTranscriptStore(
425
445
  * End the current transcript span for a vat and start a new span in a new
426
446
  * incarnation (e.g., after a vat upgrade).
427
447
  *
448
+ * This function guarantees that all store operations are done synchronously.
449
+ *
428
450
  * @param {string} vatID The vat whose transcript is to rollover to a new
429
451
  * span.
430
452
  *
@@ -438,6 +460,8 @@ export function makeTranscriptStore(
438
460
  * End the current transcript span for a vat and start a new span in the
439
461
  * current incarnation (e.g., after a heap snapshot event).
440
462
  *
463
+ * This function guarantees that all store operations are done synchronously.
464
+ *
441
465
  * @param {string} vatID The vat whose transcript is to rollover to a new
442
466
  * span.
443
467
  *
@@ -491,6 +515,8 @@ export function makeTranscriptStore(
491
515
  * Prepare for vat deletion by marking the isCurrent=1 span as not current.
492
516
  * Idempotent.
493
517
  *
518
+ * This function guarantees that all store operations are done synchronously.
519
+ *
494
520
  * @param {string} vatID The vat being terminated/deleted.
495
521
  */
496
522
  async function stopUsingTranscript(vatID) {
@@ -499,10 +525,12 @@ export function makeTranscriptStore(
499
525
  if (!bounds) return;
500
526
 
501
527
  // deal with the now-old span
502
- await closeSpan(vatID, bounds);
528
+ const { deferredTask } = closeSpan(vatID, bounds);
503
529
 
504
530
  // remove the transcript.${vatID}.current record
505
531
  noteExport(spanMetadataKey({ vatID, isCurrent: true }), undefined);
532
+
533
+ await deferredTask;
506
534
  }
507
535
 
508
536
  /**
@@ -0,0 +1,178 @@
1
+ // @ts-check
2
+ import test from 'ava';
3
+
4
+ import sqlite3 from 'better-sqlite3';
5
+ import { makePromiseKit } from '@endo/promise-kit';
6
+ import { makeQueue } from '@endo/stream';
7
+
8
+ import { makeTranscriptStore } from '../src/transcriptStore.js';
9
+
10
+ /**
11
+ * @import {AsyncQueue} from '@endo/stream';
12
+ * @import {PromiseKit} from '@endo/promise-kit';
13
+ */
14
+
15
+ function makeExportLog() {
16
+ const exportLog = [];
17
+ return {
18
+ noteExport(key, value) {
19
+ exportLog.push([key, value]);
20
+ },
21
+ getLog() {
22
+ return exportLog.splice(0);
23
+ },
24
+ };
25
+ }
26
+
27
+ /**
28
+ * @typedef {{name: string; entries: Uint8Array[]}} TranscriptArtifact
29
+ */
30
+
31
+ /**
32
+ * A test kit that consumes the transcript archive entries on demand
33
+ */
34
+ function makeArchiveTranscript() {
35
+ /** @type {AsyncQueue<(transcriptArtifact: TranscriptArtifact) => void>} */
36
+ const queue = makeQueue();
37
+
38
+ return {
39
+ archiveTranscript: async (name, rawEntries) => {
40
+ const resolve = await queue.get();
41
+ // TODO: use Array.fromAsync once we're on Node 22
42
+ const entries = [];
43
+ for await (const entry of rawEntries) {
44
+ entries.push(entry);
45
+ }
46
+ resolve({ name, entries });
47
+ },
48
+ getTranscriptArtifact: () => {
49
+ /** @type {PromiseKit<TranscriptArtifact>} */
50
+ const { resolve, promise } = makePromiseKit();
51
+ queue.put(resolve);
52
+ return promise;
53
+ },
54
+ };
55
+ }
56
+
57
+ function ensureTxn() {}
58
+
59
+ const closeSpanMacro = test.macro(async (t, useArchiver) => {
60
+ const db = sqlite3(':memory:');
61
+ const exportLog = makeExportLog();
62
+ const { archiveTranscript, getTranscriptArtifact } = useArchiver
63
+ ? makeArchiveTranscript()
64
+ : {};
65
+ const store = makeTranscriptStore(db, ensureTxn, exportLog.noteExport, {
66
+ keepTranscripts: false,
67
+ archiveTranscript,
68
+ });
69
+
70
+ const vatID = 'v1';
71
+
72
+ await null;
73
+
74
+ store.initTranscript(vatID);
75
+ store.addItem(vatID, 'foo');
76
+ store.addItem(vatID, 'bar');
77
+
78
+ t.deepEqual(exportLog.getLog(), [
79
+ [
80
+ 'transcript.v1.current',
81
+ '{"vatID":"v1","startPos":0,"endPos":0,"hash":"43e6be43a3a34d60c0ebeb8498b5849b094fc20fc68483a7aeb3624fa10f79f6","isCurrent":1,"incarnation":0}',
82
+ ],
83
+ [
84
+ 'transcript.v1.current',
85
+ '{"vatID":"v1","startPos":0,"endPos":1,"hash":"3a676f1da91fe6da8268853f21e64e3366da45811b830f9d92f39d4c01d69291","isCurrent":1,"incarnation":0}',
86
+ ],
87
+ [
88
+ 'transcript.v1.current',
89
+ '{"vatID":"v1","startPos":0,"endPos":2,"hash":"92bf47a2498333897b80e299e6248c3a9f180a68d9f8ced4d1408956afc48e55","isCurrent":1,"incarnation":0}',
90
+ ],
91
+ ]);
92
+
93
+ const spanRolloverResult = store.rolloverSpan(vatID);
94
+
95
+ t.deepEqual(exportLog.getLog(), [
96
+ [
97
+ 'transcript.v1.0',
98
+ '{"vatID":"v1","startPos":0,"endPos":2,"hash":"92bf47a2498333897b80e299e6248c3a9f180a68d9f8ced4d1408956afc48e55","isCurrent":0,"incarnation":0}',
99
+ ],
100
+ [
101
+ 'transcript.v1.current',
102
+ '{"vatID":"v1","startPos":2,"endPos":2,"hash":"43e6be43a3a34d60c0ebeb8498b5849b094fc20fc68483a7aeb3624fa10f79f6","isCurrent":1,"incarnation":0}',
103
+ ],
104
+ ]);
105
+
106
+ if (useArchiver) {
107
+ t.deepEqual(await getTranscriptArtifact?.(), {
108
+ name: 'transcript.v1.0.2',
109
+ entries: [Buffer.from('foo\n'), Buffer.from('bar\n')],
110
+ });
111
+ }
112
+
113
+ await spanRolloverResult;
114
+
115
+ t.deepEqual(exportLog.getLog(), []);
116
+
117
+ store.addItem(vatID, 'foo');
118
+ const incarnationRolloverResult = store.rolloverIncarnation(vatID);
119
+
120
+ t.deepEqual(exportLog.getLog(), [
121
+ [
122
+ 'transcript.v1.current',
123
+ '{"vatID":"v1","startPos":2,"endPos":3,"hash":"3a676f1da91fe6da8268853f21e64e3366da45811b830f9d92f39d4c01d69291","isCurrent":1,"incarnation":0}',
124
+ ],
125
+ [
126
+ 'transcript.v1.2',
127
+ '{"vatID":"v1","startPos":2,"endPos":3,"hash":"3a676f1da91fe6da8268853f21e64e3366da45811b830f9d92f39d4c01d69291","isCurrent":0,"incarnation":0}',
128
+ ],
129
+ [
130
+ 'transcript.v1.current',
131
+ '{"vatID":"v1","startPos":3,"endPos":3,"hash":"43e6be43a3a34d60c0ebeb8498b5849b094fc20fc68483a7aeb3624fa10f79f6","isCurrent":1,"incarnation":1}',
132
+ ],
133
+ ]);
134
+
135
+ if (useArchiver) {
136
+ t.deepEqual(await getTranscriptArtifact?.(), {
137
+ name: 'transcript.v1.2.3',
138
+ entries: [Buffer.from('foo\n')],
139
+ });
140
+ }
141
+
142
+ await incarnationRolloverResult;
143
+
144
+ t.deepEqual(exportLog.getLog(), []);
145
+
146
+ store.addItem(vatID, 'foo');
147
+ const stopResult = store.stopUsingTranscript(vatID);
148
+
149
+ t.deepEqual(exportLog.getLog(), [
150
+ [
151
+ 'transcript.v1.current',
152
+ '{"vatID":"v1","startPos":3,"endPos":4,"hash":"3a676f1da91fe6da8268853f21e64e3366da45811b830f9d92f39d4c01d69291","isCurrent":1,"incarnation":1}',
153
+ ],
154
+ [
155
+ 'transcript.v1.3',
156
+ '{"vatID":"v1","startPos":3,"endPos":4,"hash":"3a676f1da91fe6da8268853f21e64e3366da45811b830f9d92f39d4c01d69291","isCurrent":0,"incarnation":1}',
157
+ ],
158
+ ['transcript.v1.current', undefined],
159
+ ]);
160
+
161
+ if (useArchiver) {
162
+ t.deepEqual(await getTranscriptArtifact?.(), {
163
+ name: 'transcript.v1.3.4',
164
+ entries: [Buffer.from('foo\n')],
165
+ });
166
+ }
167
+
168
+ await stopResult;
169
+
170
+ t.deepEqual(exportLog.getLog(), []);
171
+ });
172
+
173
+ test('span close operations are sync (with archiver)', closeSpanMacro, true);
174
+ test(
175
+ 'span close operations are sync (without archiver)',
176
+ closeSpanMacro,
177
+ false,
178
+ );