@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.
@@ -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
- * @typedef { import('./swingStore').SwingStoreExporter } SwingStoreExporter
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, includeHistorical: boolean) => AsyncIterableIterator<Uint8Array>
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, includeHistorical) {
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 {SwingStoreExporter} exporter Exporter from which to get the span data
524
- * @param {object} info Metadata describing the span
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 importSpan(name, exporter, info) {
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
- // prettier-ignore
543
- info.endPos === endPos ||
544
- Fail`artifact name says endPos ${q(endPos)}, metadata says ${q(info.endPos)}`;
545
- const artifactChunks = exporter.getArtifact(name);
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, info.incarnation);
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 available`;
558
- info.hash === hash ||
559
- Fail`artifact ${name} hash is ${q(hash)}, metadata says ${q(info.hash)}`;
560
- sqlWriteSpan.run(
561
- info.vatID,
562
- info.startPos,
563
- info.endPos,
564
- info.hash,
565
- info.isCurrent ? 1 : null,
566
- info.incarnation,
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
@@ -0,0 +1,6 @@
1
+ // Types for the public API
2
+
3
+ // Everything this "exports" actually comes from the neighboring types.d.ts file
4
+
5
+ // Ensure this is a module.
6
+ export {};
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('./swingStore').AnyIterable<Uint8Array>} inStream
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
+ }
@@ -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
- importSwingStore,
9
- initSwingStore,
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: () => assert.fail('import should not query all names'),
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: () => assert.fail('import should not query all names'),
141
+ async *getArtifactNames() {
142
+ yield* [nameA];
143
+ },
142
144
  close: async () => undefined,
143
145
  };
144
146
  await t.throwsAsync(async () => importSwingStore(exporter), {