Skip to content

Commit e723855

Browse files
committed
fix: changes based on review comments
- make crank buffer key iteration work correctly in the face of data changes - get rid of nugatory blockBuffer
1 parent 63c7d97 commit e723855

File tree

5 files changed

+123
-274
lines changed

5 files changed

+123
-274
lines changed

packages/SwingSet/src/blockBuffer.js

-87
This file was deleted.

packages/SwingSet/src/hostStorage.js

-123
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,16 @@
11
// @ts-check
22
import { initSwingStore, openSwingStore } from '@agoric/swing-store';
3-
import { assert, details as X } from '@agoric/assert';
43

54
/*
65
The "Storage API" is a set of functions { has, getKeys, get, set, delete } that
76
work on string keys and accept string values. A lot of kernel-side code
87
expects to get an object which implements the Storage API, which is usually
98
associated with a write-back buffer wrapper.
109
11-
The "HostDB API" is a different set of functions { has, getKeys, get,
12-
applyBatch } which the host is expected to provide to the Controller in the
13-
config object. This API allows SwingSet to deliver batches of changes to the
14-
host-side storage medium.
15-
16-
buildHostDBInMemory creates hostDB objects for testing and casual hosts that
17-
can afford to hold all state in RAM. They must arrange to call getState() at
18-
the end of each block and save the resulting string to disk.
19-
2010
A more sophisticated host will build a hostDB that writes changes to disk
2111
directly.
2212
*/
2313

24-
/**
25-
* Create a new instance of a bare-bones implementation of the HostDB API.
26-
*
27-
* @param {KVStore} kvStore Storage object that the new HostDB object will be based on.
28-
* If omitted, defaults to a new in memory store.
29-
*/
30-
export function buildHostDBInMemory(kvStore) {
31-
if (!kvStore) {
32-
kvStore = initSwingStore(null).kvStore;
33-
}
34-
35-
/**
36-
* Test if the storage contains a value for a given key.
37-
*
38-
* @param {string} key The key that is of interest.
39-
*
40-
* @returns {boolean} true if a value is stored for the key, false if not.
41-
*/
42-
function has(key) {
43-
return kvStore.has(key);
44-
}
45-
46-
/**
47-
* Obtain an iterator over all the keys within a given range.
48-
*
49-
* @param {string} start Start of the key range of interest (inclusive)
50-
* @param {string} end End of the key range of interest (exclusive)
51-
*
52-
* @returns {Iterable<string>} an iterator for the keys from start <= key < end
53-
*/
54-
function getKeys(start, end) {
55-
return kvStore.getKeys(start, end);
56-
}
57-
58-
/**
59-
* Obtain the value stored for a given key.
60-
*
61-
* @param {string} key The key whose value is sought.
62-
*
63-
* @returns {string=} the (string) value for the given key, or undefined if there is no
64-
* such value.
65-
*/
66-
function get(key) {
67-
return kvStore.get(key);
68-
}
69-
70-
/**
71-
* @typedef {{
72-
* op: 'set',
73-
* key: string,
74-
* value: string,
75-
* }} SetOperation
76-
*
77-
* @typedef {{
78-
* op: 'delete',
79-
* key: string,
80-
* }} DeleteOperation
81-
*
82-
* @typedef {{
83-
* op: Exclude<string, 'set' | 'delete'>,
84-
* key: string,
85-
* }} UnknownOperation
86-
*
87-
* @typedef {SetOperation | DeleteOperation} BatchOperation
88-
* x typedef {Exclude<AnyOperation, BatchOperation>} UnknownOperation
89-
*/
90-
91-
/**
92-
* Make an ordered set of changes to the state that is stored. The changes
93-
* are described by a series of change description objects, each of which
94-
* describes a single change. There are currently two forms:
95-
*
96-
* { op: 'set', key: <KEY>, value: <VALUE> }
97-
* or
98-
* { op: 'delete', key: <KEY> }
99-
* which describe a set or delete operation respectively.
100-
*
101-
* @param {Array<BatchOperation>} changes An array of the changes to be
102-
* applied in order.
103-
* @throws {Error} if any of the changes are not well formed.
104-
*/
105-
function applyBatch(changes) {
106-
// TODO: Note that the parameter checking is done incrementally, thus a
107-
// malformed change descriptor later in the list will only be discovered
108-
// after earlier changes have actually been applied, potentially leaving
109-
// the store in an indeterminate state. Problem? I suspect so...
110-
for (const c of changes) {
111-
assert(`${c.op}` === c.op, X`non-string c.op ${c.op}`);
112-
assert(`${c.key}` === c.key, X`non-string c.key ${c.key}`);
113-
switch (c.op) {
114-
case 'set':
115-
assert(`${c.value}` === c.value, X`non-string c.value ${c.value}`);
116-
kvStore.set(c.key, c.value);
117-
break;
118-
case 'delete':
119-
kvStore.delete(c.key);
120-
break;
121-
default:
122-
assert.fail(X`unknown c.op ${/** @type {*} */ (c).op}`);
123-
}
124-
}
125-
}
126-
127-
const hostDB = {
128-
has,
129-
getKeys,
130-
get,
131-
applyBatch,
132-
};
133-
134-
return harden(hostDB);
135-
}
136-
13714
/**
13815
* Helper function to initialize the appropriate storage objects for the kernel
13916
*

packages/SwingSet/src/kernel/kernelSyscall.js

+11
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ export function makeKernelSyscallHandler(tools) {
8484
kernelKeeper.incStat('syscalls');
8585
kernelKeeper.incStat('syscallVatstoreGetAfter');
8686
let nextIter;
87+
// Note that the working key iterator will be invalidated if the parameters
88+
// to `vatstoreGetAfter` don't correspond to the working key iterator's
89+
// belief about what iteration was in progress. In particular,
90+
// `actualKeyPrefix` incorporates the vatID. Additionally, when this
91+
// syscall is used for iteration over a collection `keyPrefix` also
92+
// incorporates the collection ID. This ensures that uncoordinated
93+
// concurrent iterations cannot interfere with each other. If such
94+
// concurrent iterations *do* happen, there will be a modest performance
95+
// cost since the working key iterator will have to be regenerated each
96+
// time, but we expect this to be a rare case since the normal use pattern
97+
// is a single iteration in a loop within a single crank.
8798
if (
8899
workingKeyPrefix === actualKeyPrefix &&
89100
workingPriorKey === actualPriorKey &&

packages/SwingSet/src/kernel/state/storageWrapper.js

+61-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ import { insistStorageAPI } from '../../storageAPI.js';
1313
// that signals that the object could be foreign and thus deserving of
1414
// xenophobia.
1515

16+
/**
17+
* Given two iterators over ordered sequences, produce a new iterator that will
18+
* iterate in order over the merged output of the two iterators.
19+
*
20+
* @param { Iterator } it1
21+
* @param { Iterator } it2
22+
*
23+
* @yields any
24+
*/
25+
function* mergeSortedIterators(it1, it2) {
26+
let v1 = it1.next();
27+
let v2 = it2.next();
28+
while (!v1.done && !v2.done) {
29+
if (v1.value < v2.value) {
30+
const result = v1.value;
31+
v1 = it1.next();
32+
yield result;
33+
} else if (v1.value === v2.value) {
34+
const result = v1.value;
35+
v1 = it1.next();
36+
v2 = it2.next();
37+
yield result;
38+
} else {
39+
const result = v2.value;
40+
v2 = it2.next();
41+
yield result;
42+
}
43+
}
44+
const itrest = v1.done ? it2 : it1;
45+
let v = v1.done ? v2 : v1;
46+
while (!v.done) {
47+
const result = v.value;
48+
v = itrest.next();
49+
yield result;
50+
}
51+
}
52+
1653
/**
1754
* Create and return a crank buffer, which wraps a storage object with logic
1855
* that buffers any mutations until told to commit them.
@@ -40,6 +77,7 @@ export function buildCrankBuffer(
4077
// to avoid confusion, additions and deletions should never share a key
4178
const additions = new Map();
4279
const deletions = new Set();
80+
let liveGeneration = 0n;
4381
resetCrankhash();
4482

4583
const crankBuffer = {
@@ -54,18 +92,30 @@ export function buildCrankBuffer(
5492
},
5593

5694
*getKeys(start, end) {
95+
const generation = liveGeneration;
5796
assert.typeof(start, 'string');
5897
assert.typeof(end, 'string');
59-
const keys = new Set(kvStore.getKeys(start, end));
98+
99+
// find additions within the query range for use during iteration
100+
const added = [];
60101
for (const k of additions.keys()) {
61-
keys.add(k);
62-
}
63-
for (const k of deletions.keys()) {
64-
keys.delete(k);
102+
if ((start === '' || start <= k) && (end === '' || k < end)) {
103+
added.push(k);
104+
}
65105
}
66-
for (const k of Array.from(keys).sort()) {
106+
added.sort();
107+
108+
for (const k of mergeSortedIterators(
109+
added.values(),
110+
kvStore.getKeys(start, end),
111+
)) {
112+
if (liveGeneration > generation) {
113+
assert.fail('store modified during iteration');
114+
}
67115
if ((start === '' || start <= k) && (end === '' || k < end)) {
68-
yield k;
116+
if (!deletions.has(k)) {
117+
yield k;
118+
}
69119
}
70120
}
71121
},
@@ -86,6 +136,9 @@ export function buildCrankBuffer(
86136
assert.typeof(value, 'string');
87137
additions.set(key, value);
88138
deletions.delete(key);
139+
if (!crankBuffer.has(key)) {
140+
liveGeneration += 1n;
141+
}
89142
if (isConsensusKey(key)) {
90143
crankhasher.add('add');
91144
crankhasher.add('\n');
@@ -100,6 +153,7 @@ export function buildCrankBuffer(
100153
assert.typeof(key, 'string');
101154
additions.delete(key);
102155
deletions.add(key);
156+
// liveGeneration += 1n; // XXX can this be made to work? I fear not...
103157
if (isConsensusKey(key)) {
104158
crankhasher.add('delete');
105159
crankhasher.add('\n');

0 commit comments

Comments
 (0)