@agoric/swing-store 0.9.2-dev-2f092c3.0 → 0.9.2-dev-aa10ecd.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/data-export.md +20 -4
- package/docs/swingstore.md +52 -0
- package/package.json +8 -5
- package/src/assertComplete.js +22 -0
- package/src/bundleStore.js +136 -47
- package/src/exporter.js +175 -0
- package/src/importer.js +121 -0
- package/src/index.js +11 -0
- package/src/internal.js +14 -0
- package/src/kvStore.js +172 -0
- package/src/repairMetadata.js +65 -0
- package/src/snapStore.js +160 -33
- package/src/snapStoreIO.js +8 -0
- package/src/swingStore.js +52 -587
- package/src/transcriptStore.js +170 -37
- package/src/types.d.ts +14 -0
- package/src/types.js +6 -0
- package/src/util.js +7 -1
- package/test/test-bundles.js +9 -7
- package/test/test-export.js +308 -0
- package/test/test-exportImport.js +36 -54
- package/test/test-import.js +476 -0
- package/test/test-repair-metadata.js +131 -0
- package/test/util.js +26 -0
package/src/transcriptStore.js
CHANGED
|
@@ -6,8 +6,11 @@ import BufferLineTransform from '@agoric/internal/src/node/buffer-line-transform
|
|
|
6
6
|
import { createSHA256 } from './hasher.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @
|
|
10
|
-
*
|
|
9
|
+
* @template T
|
|
10
|
+
* @typedef { IterableIterator<T> | AsyncIterableIterator<T> } AnyIterableIterator<T>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
11
14
|
* @typedef {{
|
|
12
15
|
* initTranscript: (vatID: string) => void,
|
|
13
16
|
* rolloverSpan: (vatID: string) => number,
|
|
@@ -19,10 +22,13 @@ import { createSHA256 } from './hasher.js';
|
|
|
19
22
|
* }} TranscriptStore
|
|
20
23
|
*
|
|
21
24
|
* @typedef {{
|
|
22
|
-
* exportSpan: (name: string
|
|
23
|
-
* importSpan: (artifactName: string, exporter: SwingStoreExporter, artifactMetadata: Map) => Promise<void>,
|
|
25
|
+
* exportSpan: (name: string) => AsyncIterableIterator<Uint8Array>
|
|
24
26
|
* getExportRecords: (includeHistorical: boolean) => IterableIterator<readonly [key: string, value: string]>,
|
|
25
27
|
* getArtifactNames: (includeHistorical: boolean) => AsyncIterableIterator<string>,
|
|
28
|
+
* importTranscriptSpanRecord: (key: string, value: string) => void,
|
|
29
|
+
* populateTranscriptSpan: (name: string, makeChunkIterator: () => AnyIterableIterator<Uint8Array>, options: { includeHistorical: boolean }) => Promise<void>,
|
|
30
|
+
* assertComplete: (level: 'operational') => void,
|
|
31
|
+
* repairTranscriptSpanRecord: (key: string, value: string) => void,
|
|
26
32
|
* readFullVatTranscript: (vatID: string) => Iterable<{position: number, item: string}>
|
|
27
33
|
* }} TranscriptStoreInternal
|
|
28
34
|
*
|
|
@@ -82,7 +88,7 @@ export function makeTranscriptStore(
|
|
|
82
88
|
//
|
|
83
89
|
// The transcriptItems associated with historical spans may or may not exist,
|
|
84
90
|
// depending on pruning. However, the items associated with the current span
|
|
85
|
-
// must always be present
|
|
91
|
+
// must always be present.
|
|
86
92
|
|
|
87
93
|
db.exec(`
|
|
88
94
|
CREATE TABLE IF NOT EXISTS transcriptSpans (
|
|
@@ -377,6 +383,17 @@ export function makeTranscriptStore(
|
|
|
377
383
|
}
|
|
378
384
|
}
|
|
379
385
|
|
|
386
|
+
// 'position' is not recycled across incarnations, so strictly
|
|
387
|
+
// speaking this query doesn't need to filter on 'incarnation = ?',
|
|
388
|
+
// but this will catch problems like items with incorrect or missing
|
|
389
|
+
// incarnation values
|
|
390
|
+
|
|
391
|
+
const sqlCountPopulatedSpanItems = db.prepare(`
|
|
392
|
+
SELECT COUNT(*) FROM transcriptItems
|
|
393
|
+
WHERE vatID = ? AND incarnation = ? AND position >= ? AND position < ?
|
|
394
|
+
`);
|
|
395
|
+
sqlCountPopulatedSpanItems.pluck();
|
|
396
|
+
|
|
380
397
|
/**
|
|
381
398
|
* Obtain artifact names for spans contained in this store.
|
|
382
399
|
*
|
|
@@ -392,6 +409,13 @@ export function makeTranscriptStore(
|
|
|
392
409
|
? sqlGetAllSpanMetadata
|
|
393
410
|
: sqlGetCurrentSpanMetadata;
|
|
394
411
|
for (const rec of sql.iterate()) {
|
|
412
|
+
const { vatID, incarnation, startPos, endPos } = rec;
|
|
413
|
+
if (
|
|
414
|
+
!sqlCountPopulatedSpanItems.get(vatID, incarnation, startPos, endPos)
|
|
415
|
+
) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
395
419
|
yield spanArtifactName(rec);
|
|
396
420
|
}
|
|
397
421
|
}
|
|
@@ -432,15 +456,22 @@ export function makeTranscriptStore(
|
|
|
432
456
|
}
|
|
433
457
|
}
|
|
434
458
|
startPos <= endPos || Fail`${q(startPos)} <= ${q(endPos)}}`;
|
|
459
|
+
const expectedCount = endPos - startPos;
|
|
435
460
|
|
|
436
461
|
function* reader() {
|
|
462
|
+
let count = 0;
|
|
437
463
|
for (const { item } of sqlReadSpanItems.iterate(
|
|
438
464
|
vatID,
|
|
439
465
|
startPos,
|
|
440
466
|
endPos,
|
|
441
467
|
)) {
|
|
442
468
|
yield item;
|
|
469
|
+
count += 1;
|
|
443
470
|
}
|
|
471
|
+
count === expectedCount ||
|
|
472
|
+
Fail`read ${q(count)} transcript entries (expected ${q(
|
|
473
|
+
expectedCount,
|
|
474
|
+
)})`;
|
|
444
475
|
}
|
|
445
476
|
|
|
446
477
|
if (startPos === endPos) {
|
|
@@ -465,12 +496,10 @@ export function makeTranscriptStore(
|
|
|
465
496
|
* `transcript.${vatID}.${startPos}.${endPos}`
|
|
466
497
|
*
|
|
467
498
|
* @param {string} name The name of the transcript artifact to be read
|
|
468
|
-
* @param {boolean} includeHistorical If true, allow non-current spans to be fetched
|
|
469
|
-
*
|
|
470
499
|
* @returns {AsyncIterableIterator<Uint8Array>}
|
|
471
500
|
* @yields {Uint8Array}
|
|
472
501
|
*/
|
|
473
|
-
async function* exportSpan(name
|
|
502
|
+
async function* exportSpan(name) {
|
|
474
503
|
typeof name === 'string' || Fail`artifact name must be a string`;
|
|
475
504
|
const parts = name.split('.');
|
|
476
505
|
const [type, vatID, pos] = parts;
|
|
@@ -479,9 +508,6 @@ export function makeTranscriptStore(
|
|
|
479
508
|
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw ${q(name)}`;
|
|
480
509
|
const isCurrent = sqlGetSpanIsCurrent.get(vatID, pos);
|
|
481
510
|
isCurrent !== undefined || Fail`transcript span ${q(name)} not available`;
|
|
482
|
-
isCurrent ||
|
|
483
|
-
includeHistorical ||
|
|
484
|
-
Fail`transcript span ${q(name)} not available`;
|
|
485
511
|
const startPos = Number(pos);
|
|
486
512
|
for (const entry of readSpan(vatID, startPos)) {
|
|
487
513
|
yield Buffer.from(`${entry}\n`);
|
|
@@ -516,33 +542,84 @@ export function makeTranscriptStore(
|
|
|
516
542
|
noteExport(spanMetadataKey(rec), JSON.stringify(rec));
|
|
517
543
|
};
|
|
518
544
|
|
|
545
|
+
function importTranscriptSpanRecord(key, value) {
|
|
546
|
+
ensureTxn();
|
|
547
|
+
const [tag, keyVatID, keyStartPos] = key.split('.');
|
|
548
|
+
assert.equal(tag, 'transcript');
|
|
549
|
+
const metadata = JSON.parse(value);
|
|
550
|
+
if (key.endsWith('.current') !== Boolean(metadata.isCurrent)) {
|
|
551
|
+
throw Fail`transcript key ${key} mismatches metadata ${metadata}`;
|
|
552
|
+
}
|
|
553
|
+
const { vatID, startPos, endPos, hash, isCurrent, incarnation } = metadata;
|
|
554
|
+
vatID || Fail`transcript metadata missing vatID: ${metadata}`;
|
|
555
|
+
startPos !== undefined ||
|
|
556
|
+
Fail`transcript metadata missing startPos: ${metadata}`;
|
|
557
|
+
endPos !== undefined ||
|
|
558
|
+
Fail`transcript metadata missing endPos: ${metadata}`;
|
|
559
|
+
hash || Fail`transcript metadata missing hash: ${metadata}`;
|
|
560
|
+
isCurrent !== undefined ||
|
|
561
|
+
Fail`transcript metadata missing isCurrent: ${metadata}`;
|
|
562
|
+
incarnation !== undefined ||
|
|
563
|
+
Fail`transcript metadata missing incarnation: ${metadata}`;
|
|
564
|
+
if (keyStartPos !== 'current') {
|
|
565
|
+
if (Number(keyStartPos) !== startPos) {
|
|
566
|
+
Fail`transcript key ${key} mismatches metadata ${metadata}`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
keyVatID === vatID ||
|
|
570
|
+
Fail`transcript key ${key} mismatches metadata ${metadata}`;
|
|
571
|
+
|
|
572
|
+
// sqlWriteSpan is an INSERT, so the PRIMARY KEY (vatID, position)
|
|
573
|
+
// constraint will catch broken export-data errors like trying to
|
|
574
|
+
// add two different versions of the same span (e.g. one holding
|
|
575
|
+
// items 4..8, a second holding 4..9)
|
|
576
|
+
|
|
577
|
+
sqlWriteSpan.run(
|
|
578
|
+
vatID,
|
|
579
|
+
startPos,
|
|
580
|
+
endPos,
|
|
581
|
+
hash,
|
|
582
|
+
isCurrent ? 1 : null,
|
|
583
|
+
incarnation,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const sqlGetSpanMetadataFor = db.prepare(`
|
|
588
|
+
SELECT hash, isCurrent, incarnation, endPos
|
|
589
|
+
FROM transcriptSpans
|
|
590
|
+
WHERE vatID = ? AND startPos = ?
|
|
591
|
+
`);
|
|
592
|
+
|
|
519
593
|
/**
|
|
520
594
|
* Import a transcript span from another store.
|
|
521
595
|
*
|
|
522
596
|
* @param {string} name Artifact Name of the transcript span
|
|
523
|
-
* @param {
|
|
524
|
-
* @param {object}
|
|
597
|
+
* @param {() => AnyIterableIterator<Uint8Array>} makeChunkIterator get an iterator of transcript byte chunks
|
|
598
|
+
* @param {object} options
|
|
599
|
+
* @param {boolean} options.includeHistorical
|
|
525
600
|
*
|
|
526
601
|
* @returns {Promise<void>}
|
|
527
602
|
*/
|
|
528
|
-
async function
|
|
603
|
+
async function populateTranscriptSpan(name, makeChunkIterator, options) {
|
|
604
|
+
ensureTxn();
|
|
605
|
+
const { includeHistorical } = options;
|
|
529
606
|
const parts = name.split('.');
|
|
530
607
|
const [type, vatID, rawStartPos, rawEndPos] = parts;
|
|
531
608
|
// prettier-ignore
|
|
532
609
|
parts.length === 4 && type === 'transcript' ||
|
|
533
610
|
Fail`expected artifact name of the form 'transcript.{vatID}.{startPos}.{endPos}', saw '${q(name)}'`;
|
|
534
|
-
// prettier-ignore
|
|
535
|
-
info.vatID === vatID ||
|
|
536
|
-
Fail`artifact name says vatID ${q(vatID)}, metadata says ${q(info.vatID)}`;
|
|
537
611
|
const startPos = Number(rawStartPos);
|
|
538
|
-
// prettier-ignore
|
|
539
|
-
info.startPos === startPos ||
|
|
540
|
-
Fail`artifact name says startPos ${q(startPos)}, metadata says ${q(info.startPos)}`;
|
|
541
612
|
const endPos = Number(rawEndPos);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
613
|
+
|
|
614
|
+
const metadata =
|
|
615
|
+
sqlGetSpanMetadataFor.get(vatID, startPos) ||
|
|
616
|
+
Fail`no metadata for transcript span ${name}`;
|
|
617
|
+
if (!metadata.isCurrent && !includeHistorical) {
|
|
618
|
+
return; // ignore old spans
|
|
619
|
+
}
|
|
620
|
+
assert.equal(metadata.endPos, endPos);
|
|
621
|
+
|
|
622
|
+
const artifactChunks = await makeChunkIterator();
|
|
546
623
|
const inStream = Readable.from(artifactChunks);
|
|
547
624
|
const lineTransform = new BufferLineTransform();
|
|
548
625
|
const lineStream = inStream.pipe(lineTransform).setEncoding('utf8');
|
|
@@ -550,21 +627,73 @@ export function makeTranscriptStore(
|
|
|
550
627
|
let pos = startPos;
|
|
551
628
|
for await (const line of lineStream) {
|
|
552
629
|
const item = line.trimEnd();
|
|
553
|
-
sqlAddItem.run(vatID, item, pos,
|
|
630
|
+
sqlAddItem.run(vatID, item, pos, metadata.incarnation);
|
|
554
631
|
hash = updateSpanHash(hash, item);
|
|
555
632
|
pos += 1;
|
|
556
633
|
}
|
|
557
|
-
pos === endPos || Fail`artifact ${name} is not
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
634
|
+
pos === endPos || Fail`artifact ${name} is not complete`;
|
|
635
|
+
|
|
636
|
+
// validate against the previously-established metadata
|
|
637
|
+
|
|
638
|
+
// prettier-ignore
|
|
639
|
+
metadata.hash === hash ||
|
|
640
|
+
Fail`artifact ${name} hash is ${q(hash)}, metadata says ${q(metadata.hash)}`;
|
|
641
|
+
|
|
642
|
+
// If that passes, the not-yet-committed data is good. If it
|
|
643
|
+
// fails, the thrown error will flunk the import and inhibit a
|
|
644
|
+
// commit. So we're done.
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function repairTranscriptSpanRecord(key, value) {
|
|
648
|
+
ensureTxn();
|
|
649
|
+
const [tag, keyVatID, keyStartPos] = key.split('.');
|
|
650
|
+
assert.equal(tag, 'transcript');
|
|
651
|
+
const metadata = JSON.parse(value);
|
|
652
|
+
const { vatID, startPos, endPos, hash, isCurrent, incarnation } = metadata;
|
|
653
|
+
assert.equal(keyVatID, vatID);
|
|
654
|
+
if (keyStartPos !== 'current') {
|
|
655
|
+
if (Number(keyStartPos) !== startPos) {
|
|
656
|
+
Fail`transcript key ${key} mismatches metadata ${metadata}`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const existing = sqlGetSpanMetadataFor.get(vatID, startPos);
|
|
661
|
+
if (existing) {
|
|
662
|
+
if (
|
|
663
|
+
Boolean(existing.isCurrent) !== Boolean(isCurrent) ||
|
|
664
|
+
existing.hash !== hash ||
|
|
665
|
+
existing.incarnation !== incarnation ||
|
|
666
|
+
existing.endPos !== endPos
|
|
667
|
+
) {
|
|
668
|
+
throw Fail`repairTranscriptSpanRecord metadata mismatch: ${existing} vs ${metadata}`;
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
sqlWriteSpan.run(
|
|
672
|
+
vatID,
|
|
673
|
+
startPos,
|
|
674
|
+
endPos,
|
|
675
|
+
hash,
|
|
676
|
+
isCurrent ? 1 : null,
|
|
677
|
+
incarnation,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function assertComplete(level) {
|
|
683
|
+
assert.equal(level, 'operational'); // for now
|
|
684
|
+
// every 'isCurrent' transcript span must have all items
|
|
685
|
+
for (const rec of sqlGetCurrentSpanMetadata.iterate()) {
|
|
686
|
+
const { vatID, startPos, endPos, incarnation } = rec;
|
|
687
|
+
const count = sqlCountPopulatedSpanItems.get(
|
|
688
|
+
vatID,
|
|
689
|
+
incarnation,
|
|
690
|
+
startPos,
|
|
691
|
+
endPos,
|
|
692
|
+
);
|
|
693
|
+
if (count !== endPos - startPos) {
|
|
694
|
+
throw Fail`incomplete current transcript span: ${count} items, ${rec}`;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
568
697
|
}
|
|
569
698
|
|
|
570
699
|
return harden({
|
|
@@ -577,10 +706,14 @@ export function makeTranscriptStore(
|
|
|
577
706
|
deleteVatTranscripts,
|
|
578
707
|
|
|
579
708
|
exportSpan,
|
|
580
|
-
importSpan,
|
|
581
709
|
getExportRecords,
|
|
582
710
|
getArtifactNames,
|
|
583
711
|
|
|
712
|
+
importTranscriptSpanRecord,
|
|
713
|
+
populateTranscriptSpan,
|
|
714
|
+
assertComplete,
|
|
715
|
+
repairTranscriptSpanRecord,
|
|
716
|
+
|
|
584
717
|
dumpTranscripts,
|
|
585
718
|
readFullVatTranscript,
|
|
586
719
|
});
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
SwingStore,
|
|
3
|
+
SwingStoreKernelStorage,
|
|
4
|
+
SwingStoreHostStorage,
|
|
5
|
+
} from './src/swingStore.js';
|
|
6
|
+
export type { KVStore } from './src/kvStore.js';
|
|
7
|
+
export type { BundleStore } from './src/bundleStore.js';
|
|
8
|
+
export type {
|
|
9
|
+
SnapStore,
|
|
10
|
+
SnapshotResult,
|
|
11
|
+
SnapshotInfo,
|
|
12
|
+
} from './src/snapStore.js';
|
|
13
|
+
export type { TranscriptStore } from './src/transcriptStore.js';
|
|
14
|
+
export type { SwingStoreExporter, ExportMode } from './src/exporter.js';
|
package/src/types.js
ADDED
package/src/util.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { Buffer } from 'buffer';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -5,7 +6,7 @@ import { Buffer } from 'buffer';
|
|
|
5
6
|
* 'stream/consumers' package, which unfortunately only exists in newer versions
|
|
6
7
|
* of Node.
|
|
7
8
|
*
|
|
8
|
-
* @param {import('./
|
|
9
|
+
* @param {import('./exporter').AnyIterable<Uint8Array>} inStream
|
|
9
10
|
*/
|
|
10
11
|
export const buffer = async inStream => {
|
|
11
12
|
const chunks = [];
|
|
@@ -14,3 +15,8 @@ export const buffer = async inStream => {
|
|
|
14
15
|
}
|
|
15
16
|
return Buffer.concat(chunks);
|
|
16
17
|
};
|
|
18
|
+
|
|
19
|
+
export function dbFileInDirectory(dirPath) {
|
|
20
|
+
const filePath = path.resolve(dirPath, 'swingstore.sqlite');
|
|
21
|
+
return filePath;
|
|
22
|
+
}
|
package/test/test-bundles.js
CHANGED
|
@@ -4,11 +4,9 @@ import test from 'ava';
|
|
|
4
4
|
import tmp from 'tmp';
|
|
5
5
|
import { Buffer } from 'buffer';
|
|
6
6
|
import { createSHA256 } from '../src/hasher.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
makeSwingStoreExporter,
|
|
11
|
-
} from '../src/swingStore.js';
|
|
7
|
+
import { initSwingStore } from '../src/swingStore.js';
|
|
8
|
+
import { makeSwingStoreExporter } from '../src/exporter.js';
|
|
9
|
+
import { importSwingStore } from '../src/importer.js';
|
|
12
10
|
import { buffer } from '../src/util.js';
|
|
13
11
|
|
|
14
12
|
function makeB0ID(bundle) {
|
|
@@ -116,7 +114,9 @@ test('b0 import', async t => {
|
|
|
116
114
|
t.is(name, nameA);
|
|
117
115
|
yield Buffer.from(JSON.stringify(b0A));
|
|
118
116
|
},
|
|
119
|
-
getArtifactNames
|
|
117
|
+
async *getArtifactNames() {
|
|
118
|
+
yield* [nameA];
|
|
119
|
+
},
|
|
120
120
|
close: async () => undefined,
|
|
121
121
|
};
|
|
122
122
|
const { kernelStorage } = await importSwingStore(exporter);
|
|
@@ -138,7 +138,9 @@ test('b0 bad import', async t => {
|
|
|
138
138
|
t.is(name, nameA);
|
|
139
139
|
yield Buffer.from(JSON.stringify(b0Abogus));
|
|
140
140
|
},
|
|
141
|
-
getArtifactNames
|
|
141
|
+
async *getArtifactNames() {
|
|
142
|
+
yield* [nameA];
|
|
143
|
+
},
|
|
142
144
|
close: async () => undefined,
|
|
143
145
|
};
|
|
144
146
|
await t.throwsAsync(async () => importSwingStore(exporter), {
|