Skip to content

Commit

Permalink
feat: support vatstore iteration over explicit key bounds
Browse files Browse the repository at this point in the history
  • Loading branch information
FUDCo committed Dec 4, 2021
1 parent e723855 commit f220dd8
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 52 deletions.
55 changes: 33 additions & 22 deletions packages/SwingSet/src/kernel/kernelSyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ export function makeKernelSyscallHandler(tools) {
return `${vatID}.vs.${key}`;
}

let workingKeyPrefix;
let workingPriorKey;
let workingLowerBound;
let workingUpperBound;
let workingKeyIterator;

function clearVatStoreIteration() {
workingKeyPrefix = undefined;
workingPriorKey = undefined;
workingLowerBound = undefined;
workingUpperBound = undefined;
workingKeyIterator = undefined;
}

Expand All @@ -78,43 +80,52 @@ export function makeKernelSyscallHandler(tools) {
return OKNULL;
}

function vatstoreGetAfter(vatID, keyPrefix, priorKey) {
const actualKeyPrefix = vatstoreKeyKey(vatID, keyPrefix);
function vatstoreGetAfter(vatID, priorKey, lowerBound, upperBound) {
const actualPriorKey = vatstoreKeyKey(vatID, priorKey);
const actualLowerBound = vatstoreKeyKey(vatID, lowerBound);
let actualUpperBound;
if (upperBound) {
actualUpperBound = vatstoreKeyKey(vatID, upperBound);
} else {
const lastChar = String.fromCharCode(
actualLowerBound.slice(-1).charCodeAt(0) + 1,
);
actualUpperBound = `${actualLowerBound.slice(0, -1)}${lastChar}`;
}
kernelKeeper.incStat('syscalls');
kernelKeeper.incStat('syscallVatstoreGetAfter');
let nextIter;
// Note that the working key iterator will be invalidated if the parameters
// to `vatstoreGetAfter` don't correspond to the working key iterator's
// belief about what iteration was in progress. In particular,
// `actualKeyPrefix` incorporates the vatID. Additionally, when this
// syscall is used for iteration over a collection `keyPrefix` also
// incorporates the collection ID. This ensures that uncoordinated
// concurrent iterations cannot interfere with each other. If such
// concurrent iterations *do* happen, there will be a modest performance
// cost since the working key iterator will have to be regenerated each
// time, but we expect this to be a rare case since the normal use pattern
// is a single iteration in a loop within a single crank.
// belief about what iteration was in progress. In particular, the bounds
// incorporate the vatID. Additionally, when this syscall is used for
// iteration over a collection, the bounds also incorporate the collection
// ID. This ensures that uncoordinated concurrent iterations cannot
// interfere with each other. If such concurrent iterations *do* happen,
// there will be a modest performance cost since the working key iterator
// will have to be regenerated each time, but we expect this to be a rare
// case since the normal use pattern is a single iteration in a loop within
// a single crank.
if (
workingKeyPrefix === actualKeyPrefix &&
workingPriorKey === actualPriorKey &&
workingLowerBound === actualLowerBound &&
workingUpperBound === actualUpperBound &&
workingKeyIterator
) {
nextIter = workingKeyIterator.next();
} else {
let startKey;
if (priorKey === '') {
startKey = actualKeyPrefix;
startKey = actualLowerBound;
} else {
startKey = actualPriorKey;
}
assert(startKey.startsWith(actualKeyPrefix));
const lastChar = String.fromCharCode(
actualKeyPrefix.slice(-1).charCodeAt(0) + 1,
);
const endKey = `${actualKeyPrefix.slice(0, -1)}${lastChar}`;
workingKeyPrefix = actualKeyPrefix;
workingKeyIterator = kvStore.getKeys(startKey, endKey);
assert(actualLowerBound <= startKey);
assert(actualLowerBound < actualUpperBound);
assert(startKey < actualUpperBound);
workingLowerBound = actualLowerBound;
workingUpperBound = actualUpperBound;
workingKeyIterator = kvStore.getKeys(startKey, actualUpperBound);
nextIter = workingKeyIterator.next();
if (!nextIter.done && nextIter.value === actualPriorKey) {
nextIter = workingKeyIterator.next();
Expand Down
20 changes: 13 additions & 7 deletions packages/SwingSet/src/kernel/liveSlots.js
Original file line number Diff line number Diff line change
Expand Up @@ -1089,19 +1089,25 @@ function build(
assert.typeof(value, 'string');
syscall.vatstoreSet(`vvs.${key}`, value);
},
getAfter: (keyPrefix, priorKey) => {
insistValidVatstoreKey(keyPrefix);
getAfter: (priorKey, lowerBound, upperBound) => {
if (priorKey !== '') {
insistValidVatstoreKey(priorKey);
assert(
priorKey.startsWith(keyPrefix),
'priorKey must start with keyPrefix',
);
assert(priorKey >= lowerBound, 'priorKey must be >= lowerBound');
priorKey = `vvs.${priorKey}`;
}
insistValidVatstoreKey(lowerBound);
lowerBound = `vvs.${lowerBound}`;
if (upperBound !== undefined) {
insistValidVatstoreKey(upperBound);
assert(upperBound > lowerBound, 'upperBound must be > lowerBound');
}
if (upperBound !== undefined) {
upperBound = `vvs.${upperBound}`;
}
const fetched = syscall.vatstoreGetAfter(
`vvs.${keyPrefix}`,
priorKey,
lowerBound,
upperBound,
);
if (fetched) {
const [key, value] = fetched;
Expand Down
4 changes: 2 additions & 2 deletions packages/SwingSet/src/kernel/vatManager/supervisor-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ function makeSupervisorSyscall(syscallToManager, workerCanBlock) {
doSyscall(['callNow', target, method, args]),
vatstoreGet: key => doSyscall(['vatstoreGet', key]),
vatstoreSet: (key, value) => doSyscall(['vatstoreSet', key, value]),
vatstoreGetAfter: (keyPrefix, priorKey) =>
doSyscall(['vatstoreGetAfter', keyPrefix, priorKey]),
vatstoreGetAfter: (priorKey, lowerBound, upperBound) =>
doSyscall(['vatstoreGetAfter', priorKey, lowerBound, upperBound]),
vatstoreDelete: key => doSyscall(['vatstoreDelete', key]),
};

Expand Down
19 changes: 15 additions & 4 deletions packages/SwingSet/src/kernel/vatTranslator.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,24 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {
return harden(['vatstoreSet', vatID, key, value]);
}

function translateVatstoreGetAfter(keyPrefix, priorKey) {
insistValidVatstoreKey(keyPrefix);
function translateVatstoreGetAfter(priorKey, lowerBound, upperBound) {
if (priorKey !== '') {
insistValidVatstoreKey(priorKey);
}
kdebug(`syscall[${vatID}].vatstoreGetAfter(${keyPrefix}, ${priorKey})`);
return harden(['vatstoreGetAfter', vatID, keyPrefix, priorKey]);
insistValidVatstoreKey(lowerBound);
if (upperBound) {
insistValidVatstoreKey(upperBound);
}
kdebug(
`syscall[${vatID}].vatstoreGetAfter(${priorKey}, ${lowerBound}, ${upperBound})`,
);
return harden([
'vatstoreGetAfter',
vatID,
priorKey,
lowerBound,
upperBound,
]);
}

function translateVatstoreDelete(key) {
Expand Down
7 changes: 5 additions & 2 deletions packages/SwingSet/src/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,12 @@ export function insistVatSyscallObject(vso) {
break;
}
case 'vatstoreGetAfter': {
const [keyPrefix, priorKey] = rest;
assert.typeof(keyPrefix, 'string');
const [priorKey, lowerBound, upperBound] = rest;
assert.typeof(priorKey, 'string');
assert.typeof(lowerBound, 'string');
if (upperBound !== undefined) {
assert.typeof(upperBound, 'string');
}
break;
}
case 'vatstoreDelete': {
Expand Down
10 changes: 8 additions & 2 deletions packages/SwingSet/test/test-vatstore.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ test('vatstore', async t => {
send('store', 'yyy', 'not thisy');
// check that we hit all the 'x.' keys
send('scan', 'x.');
// check that we can scan an explicit key range
send('scanRange', 'x.2', 'x.p');
// check that this works even if the iteration is interrupted
send('scan', 'x.', 3);
// check that interleaved iterations don't interfere
Expand Down Expand Up @@ -102,6 +104,10 @@ test('vatstore', async t => {
' x.3 -> three',
' x.a -> four',
' x.qrz -> five',
'scanRange x.2 x.p:',
' x.2 -> two',
' x.3 -> three',
' x.a -> four',
'scan x. 3:',
' x.1 -> one',
' x.2 -> two',
Expand All @@ -126,8 +132,8 @@ test('vatstore', async t => {
'apiAbuse x.: use prefix as prior key (should work)',
' x.1 -> one',
'apiAbuse x.: use out of range prior key aaax.',
' getAfter(x., aaax.) threw Error: priorKey must start with keyPrefix',
' getAfter(aaax., x.) threw Error: priorKey must be >= lowerBound',
'apiAbuse x.: use invalid key prefix',
' getAfter("ab@%%$#", "") threw Error: invalid vatstore key',
' getAfter("", "ab@%%$#") threw Error: invalid vatstore key',
]);
});
39 changes: 26 additions & 13 deletions packages/SwingSet/test/vat-vatstore.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ export function buildRootObject(vatPowers) {
vatstore.set(key, value);
log(`store ${key} <- "${value}"`);
},
getAfter(keyPrefix, priorKey) {
const result = vatstore.getAfter(keyPrefix, priorKey);
getAfter(priorKey, lowerBound, upperBound) {
const result = vatstore.getAfter(priorKey, lowerBound, upperBound);
if (result) {
const [key, value] = result;
log(`getAfter ${keyPrefix} ${priorKey} -> [${key}, ${value}]`);
log(
`getAfter ${priorKey} ${lowerBound} ${upperBound} -> [${key}, ${value}]`,
);
} else {
log(`getAfter ${keyPrefix} ${priorKey} -> undefined`);
log(`getAfter ${priorKey} ${lowerBound} ${upperBound} -> undefined`);
}
return result;
},
Expand All @@ -40,7 +42,7 @@ export function buildRootObject(vatPowers) {
let fetched;
let count = 0;
// eslint-disable-next-line no-cond-assign
while ((fetched = vatstore.getAfter(prefix, key))) {
while ((fetched = vatstore.getAfter(key, prefix))) {
count += 1;
[key, value] = fetched;
log(` ${key} -> ${value}`);
Expand All @@ -50,6 +52,17 @@ export function buildRootObject(vatPowers) {
}
}
},
scanRange(lower, upper) {
log(`scanRange ${lower} ${upper}:`);
let key = '';
let value;
let fetched;
// eslint-disable-next-line no-cond-assign
while ((fetched = vatstore.getAfter(key, lower, upper))) {
[key, value] = fetched;
log(` ${key} -> ${value}`);
}
},
scanInterleaved(prefix1, prefix2) {
let key1 = '';
let key2 = '';
Expand All @@ -59,14 +72,14 @@ export function buildRootObject(vatPowers) {
let done1 = false;
let done2 = false;
do {
const fetched1 = vatstore.getAfter(prefix1, key1);
const fetched1 = vatstore.getAfter(key1, prefix1);
if (fetched1 && !done1) {
[key1, value1] = fetched1;
log(` 1: ${key1} -> ${value1}`);
} else {
done1 = true;
}
const fetched2 = vatstore.getAfter(prefix2, key2);
const fetched2 = vatstore.getAfter(key2, prefix2);
if (fetched2 && !done2) {
[key2, value2] = fetched2;
log(` 2: ${key2} -> ${value2}`);
Expand All @@ -88,28 +101,28 @@ export function buildRootObject(vatPowers) {
const badPriorKey = `aaa${prefix}`;
log(`apiAbuse ${prefix}: use out of range prior key ${badPriorKey}`);
try {
const fetched2 = vatstore.getAfter(prefix, badPriorKey);
const fetched2 = vatstore.getAfter(badPriorKey, prefix);
if (fetched2) {
const [key, value] = fetched2;
log(` ${key} -> ${value}`);
} else {
log(` getAfter(${prefix}, ${badPriorKey}) returns undefined`);
log(` getAfter(${badPriorKey}, ${prefix}) returns undefined`);
}
} catch (e) {
log(` getAfter(${prefix}, ${badPriorKey}) threw ${e}`);
log(` getAfter(${badPriorKey}, ${prefix}) threw ${e}`);
}

log(`apiAbuse ${prefix}: use invalid key prefix`);
try {
const fetched3 = vatstore.getAfter('ab@%%$#', '');
const fetched3 = vatstore.getAfter('', 'ab@%%$#');
if (fetched3) {
const [key, value] = fetched3;
log(` ${key} -> ${value}`);
} else {
log(` getAfter("ab@%%$#", "") returns undefined`);
log(` getAfter("", "ab@%%$#") returns undefined`);
}
} catch (e) {
log(` getAfter("ab@%%$#", "") threw ${e}`);
log(` getAfter("", "ab@%%$#") threw ${e}`);
}
},
delete(key) {
Expand Down

0 comments on commit f220dd8

Please sign in to comment.