@agoric/swing-store 0.9.2-dev-dde639f.0.dde639f → 0.9.2-dev-63f4816.0.63f4816
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 +5 -3
- package/src/archiver.js +18 -4
- package/src/transcriptStore.js +35 -7
- package/test/transcriptStore.test.js +178 -0
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-63f4816.0.63f4816",
|
|
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-
|
|
24
|
+
"@agoric/internal": "0.3.3-dev-63f4816.0.63f4816",
|
|
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": "
|
|
56
|
+
"gitHead": "63f481671ced3c1174fa533194d28f64b0e36c50"
|
|
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(),
|
package/src/transcriptStore.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
391
|
+
let archiveResult = Promise.resolve();
|
|
382
392
|
if (archiveTranscript) {
|
|
383
393
|
const spanName = spanArtifactName(rec);
|
|
384
|
-
const entries =
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
);
|