Skip to content

Commit 63c7d97

Browse files
committed
feat: add vatstoreGetAfter syscall to enable iterating vatstore keys
1 parent fbc7c64 commit 63c7d97

File tree

8 files changed

+298
-14
lines changed

8 files changed

+298
-14
lines changed

packages/SwingSet/src/kernel/kernelSyscall.js

+57
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ export function makeKernelSyscallHandler(tools) {
5151
return `${vatID}.vs.${key}`;
5252
}
5353

54+
let workingKeyPrefix;
55+
let workingPriorKey;
56+
let workingKeyIterator;
57+
58+
function clearVatStoreIteration() {
59+
workingKeyPrefix = undefined;
60+
workingPriorKey = undefined;
61+
workingKeyIterator = undefined;
62+
}
63+
5464
function vatstoreGet(vatID, key) {
5565
const actualKey = vatstoreKeyKey(vatID, key);
5666
kernelKeeper.incStat('syscalls');
@@ -64,14 +74,59 @@ export function makeKernelSyscallHandler(tools) {
6474
kernelKeeper.incStat('syscalls');
6575
kernelKeeper.incStat('syscallVatstoreSet');
6676
kvStore.set(actualKey, value);
77+
clearVatStoreIteration();
6778
return OKNULL;
6879
}
6980

81+
function vatstoreGetAfter(vatID, keyPrefix, priorKey) {
82+
const actualKeyPrefix = vatstoreKeyKey(vatID, keyPrefix);
83+
const actualPriorKey = vatstoreKeyKey(vatID, priorKey);
84+
kernelKeeper.incStat('syscalls');
85+
kernelKeeper.incStat('syscallVatstoreGetAfter');
86+
let nextIter;
87+
if (
88+
workingKeyPrefix === actualKeyPrefix &&
89+
workingPriorKey === actualPriorKey &&
90+
workingKeyIterator
91+
) {
92+
nextIter = workingKeyIterator.next();
93+
} else {
94+
let startKey;
95+
if (priorKey === '') {
96+
startKey = actualKeyPrefix;
97+
} else {
98+
startKey = actualPriorKey;
99+
}
100+
assert(startKey.startsWith(actualKeyPrefix));
101+
const lastChar = String.fromCharCode(
102+
actualKeyPrefix.slice(-1).charCodeAt(0) + 1,
103+
);
104+
const endKey = `${actualKeyPrefix.slice(0, -1)}${lastChar}`;
105+
workingKeyPrefix = actualKeyPrefix;
106+
workingKeyIterator = kvStore.getKeys(startKey, endKey);
107+
nextIter = workingKeyIterator.next();
108+
if (!nextIter.done && nextIter.value === actualPriorKey) {
109+
nextIter = workingKeyIterator.next();
110+
}
111+
}
112+
if (nextIter.done) {
113+
clearVatStoreIteration();
114+
return harden(['ok', undefined]);
115+
} else {
116+
const nextKey = nextIter.value;
117+
const resultValue = kvStore.get(nextKey);
118+
workingPriorKey = nextKey;
119+
const resultKey = nextKey.slice(vatID.length + 4); // `${vatID}.vs.`.length
120+
return harden(['ok', [resultKey, resultValue]]);
121+
}
122+
}
123+
70124
function vatstoreDelete(vatID, key) {
71125
const actualKey = vatstoreKeyKey(vatID, key);
72126
kernelKeeper.incStat('syscalls');
73127
kernelKeeper.incStat('syscallVatstoreDelete');
74128
kvStore.delete(actualKey);
129+
clearVatStoreIteration();
75130
return OKNULL;
76131
}
77132

@@ -160,6 +215,8 @@ export function makeKernelSyscallHandler(tools) {
160215
return vatstoreGet(...args);
161216
case 'vatstoreSet':
162217
return vatstoreSet(...args);
218+
case 'vatstoreGetAfter':
219+
return vatstoreGetAfter(...args);
163220
case 'vatstoreDelete':
164221
return vatstoreDelete(...args);
165222
case 'dropImports':

packages/SwingSet/src/kernel/liveSlots.js

+26-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { insistVatType, makeVatSlot, parseVatSlot } from '../parseVatSlots.js';
1212
import { insistCapData } from '../capdata.js';
1313
import { insistMessage } from '../message.js';
1414
import { makeVirtualObjectManager } from './virtualObjectManager.js';
15+
import { insistValidVatstoreKey } from './vatTranslator.js';
1516

1617
const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to force churn for testing
1718

@@ -1080,16 +1081,38 @@ function build(
10801081
if (enableVatstore) {
10811082
vpow.vatstore = harden({
10821083
get: key => {
1083-
assert.typeof(key, 'string');
1084+
insistValidVatstoreKey(key);
10841085
return syscall.vatstoreGet(`vvs.${key}`);
10851086
},
10861087
set: (key, value) => {
1087-
assert.typeof(key, 'string');
1088+
insistValidVatstoreKey(key);
10881089
assert.typeof(value, 'string');
10891090
syscall.vatstoreSet(`vvs.${key}`, value);
10901091
},
1092+
getAfter: (keyPrefix, priorKey) => {
1093+
insistValidVatstoreKey(keyPrefix);
1094+
if (priorKey !== '') {
1095+
insistValidVatstoreKey(priorKey);
1096+
assert(
1097+
priorKey.startsWith(keyPrefix),
1098+
'priorKey must start with keyPrefix',
1099+
);
1100+
priorKey = `vvs.${priorKey}`;
1101+
}
1102+
const fetched = syscall.vatstoreGetAfter(
1103+
`vvs.${keyPrefix}`,
1104+
priorKey,
1105+
);
1106+
if (fetched) {
1107+
const [key, value] = fetched;
1108+
assert(key.startsWith('vvs.'));
1109+
return [key.slice(4), value];
1110+
} else {
1111+
return undefined;
1112+
}
1113+
},
10911114
delete: key => {
1092-
assert.typeof(key, 'string');
1115+
insistValidVatstoreKey(key);
10931116
syscall.vatstoreDelete(`vvs.${key}`);
10941117
},
10951118
});

packages/SwingSet/src/kernel/metrics.js

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export const KERNEL_STATS_SUM_METRICS = [
4040
name: 'swingset_syscall_vatstore_set_total',
4141
description: 'Total number of SwingSet vatstore set kernel calls',
4242
},
43+
{
44+
key: 'syscallVatstoreGetAfter',
45+
name: 'swingset_syscall_vatstore_getAfter_total',
46+
description: 'Total number of SwingSet vatstore getAfter kernel calls',
47+
},
4348
{
4449
key: 'syscallVatstoreDelete',
4550
name: 'swingset_syscall_vatstore_delete_total',

packages/SwingSet/src/kernel/vatManager/supervisor-helper.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,18 @@ function makeSupervisorSyscall(syscallToManager, workerCanBlock) {
117117
doSyscall(['callNow', target, method, args]),
118118
vatstoreGet: key => doSyscall(['vatstoreGet', key]),
119119
vatstoreSet: (key, value) => doSyscall(['vatstoreSet', key, value]),
120+
vatstoreGetAfter: (keyPrefix, priorKey) =>
121+
doSyscall(['vatstoreGetAfter', keyPrefix, priorKey]),
120122
vatstoreDelete: key => doSyscall(['vatstoreDelete', key]),
121123
};
122124

123-
const blocking = ['callNow', 'vatstoreGet', 'vatstoreSet', 'vatstoreDelete'];
125+
const blocking = [
126+
'callNow',
127+
'vatstoreGet',
128+
'vatstoreSet',
129+
'vatstoreGetAfter',
130+
'vatstoreDelete',
131+
];
124132

125133
if (!workerCanBlock) {
126134
for (const name of blocking) {

packages/SwingSet/src/kernel/vatTranslator.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ function makeTranslateKernelDeliveryToVatDelivery(vatID, kernelKeeper) {
152152
return kernelDeliveryToVatDelivery;
153153
}
154154

155+
export function insistValidVatstoreKey(key) {
156+
assert.typeof(key, 'string');
157+
assert(key.match(/^[-\w.+/]+$/), 'invalid vatstore key');
158+
}
159+
155160
/*
156161
* return a function that converts VatSyscall objects into KernelSyscall
157162
* objects
@@ -212,11 +217,6 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {
212217
return harden(['exit', vatID, !!isFailure, kernelInfo]);
213218
}
214219

215-
function insistValidVatstoreKey(key) {
216-
assert.typeof(key, 'string');
217-
assert(key.match(/^[-\w.+/]+$/));
218-
}
219-
220220
function translateVatstoreGet(key) {
221221
insistValidVatstoreKey(key);
222222
kdebug(`syscall[${vatID}].vatstoreGet(${key})`);
@@ -230,6 +230,15 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {
230230
return harden(['vatstoreSet', vatID, key, value]);
231231
}
232232

233+
function translateVatstoreGetAfter(keyPrefix, priorKey) {
234+
insistValidVatstoreKey(keyPrefix);
235+
if (priorKey !== '') {
236+
insistValidVatstoreKey(priorKey);
237+
}
238+
kdebug(`syscall[${vatID}].vatstoreGetAfter(${keyPrefix}, ${priorKey})`);
239+
return harden(['vatstoreGetAfter', vatID, keyPrefix, priorKey]);
240+
}
241+
233242
function translateVatstoreDelete(key) {
234243
insistValidVatstoreKey(key);
235244
kdebug(`syscall[${vatID}].vatstoreDelete(${key})`);
@@ -366,6 +375,8 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) {
366375
return translateVatstoreGet(...args);
367376
case 'vatstoreSet':
368377
return translateVatstoreSet(...args);
378+
case 'vatstoreGetAfter':
379+
return translateVatstoreGetAfter(...args);
369380
case 'vatstoreDelete':
370381
return translateVatstoreDelete(...args);
371382
case 'dropImports':
@@ -418,6 +429,13 @@ function makeTranslateKernelSyscallResultToVatSyscallResult(
418429
} else {
419430
return harden(['ok', undefined]);
420431
}
432+
case 'vatstoreGetAfter':
433+
if (resultData) {
434+
assert(Array.isArray(resultData));
435+
return harden(['ok', resultData]);
436+
} else {
437+
return harden(['ok', undefined]);
438+
}
421439
default:
422440
assert(resultData === null);
423441
return harden(['ok', null]);

packages/SwingSet/src/message.js

+6
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ export function insistVatSyscallObject(vso) {
151151
assert.typeof(data, 'string');
152152
break;
153153
}
154+
case 'vatstoreGetAfter': {
155+
const [keyPrefix, priorKey] = rest;
156+
assert.typeof(keyPrefix, 'string');
157+
assert.typeof(priorKey, 'string');
158+
break;
159+
}
154160
case 'vatstoreDelete': {
155161
const [key] = rest;
156162
assert.typeof(key, 'string');

packages/SwingSet/test/test-vatstore.js

+78-5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,37 @@ test('vatstore', async t => {
4040
send('get', 'zot');
4141
await c.run();
4242

43+
for (const k of hostStorage.kvStore.getKeys('', '')) {
44+
if (k.endsWith('.foo')) {
45+
t.is(k, 'v1.vs.vvs.foo');
46+
}
47+
}
48+
49+
send('store', 'x.1', 'one');
50+
send('store', 'x.2', 'two');
51+
send('store', 'x.3', 'three');
52+
send('store', 'x.a', 'four');
53+
send('store', 'x.qrz', 'five');
54+
send('store', 'xxx', 'not this');
55+
send('store', 'y.1', 'oney');
56+
send('store', 'y.2', 'twoy');
57+
send('store', 'y.3', 'threey');
58+
send('store', 'y.a', 'foury');
59+
send('store', 'y.b', 'fivey');
60+
send('store', 'y.c', 'sixy');
61+
send('store', 'y.d', 'seveny');
62+
send('store', 'yyy', 'not thisy');
63+
// check that we hit all the 'x.' keys
64+
send('scan', 'x.');
65+
// check that this works even if the iteration is interrupted
66+
send('scan', 'x.', 3);
67+
// check that interleaved iterations don't interfere
68+
send('scanInterleaved', 'x.', 'y.');
69+
// check for a successful empty iteration if there's no match
70+
send('scan', 'z.');
71+
// exercise various calls with malformed parameters
72+
send('apiAbuse', 'x.');
73+
await c.run();
4374
t.deepEqual(c.dump().log, [
4475
'get zot -> <undefined>',
4576
'store zot <- "first zot"',
@@ -51,10 +82,52 @@ test('vatstore', async t => {
5182
'delete zot',
5283
'get foo -> "first foo"',
5384
'get zot -> <undefined>',
85+
'store x.1 <- "one"',
86+
'store x.2 <- "two"',
87+
'store x.3 <- "three"',
88+
'store x.a <- "four"',
89+
'store x.qrz <- "five"',
90+
'store xxx <- "not this"',
91+
'store y.1 <- "oney"',
92+
'store y.2 <- "twoy"',
93+
'store y.3 <- "threey"',
94+
'store y.a <- "foury"',
95+
'store y.b <- "fivey"',
96+
'store y.c <- "sixy"',
97+
'store y.d <- "seveny"',
98+
'store yyy <- "not thisy"',
99+
'scan x.:',
100+
' x.1 -> one',
101+
' x.2 -> two',
102+
' x.3 -> three',
103+
' x.a -> four',
104+
' x.qrz -> five',
105+
'scan x. 3:',
106+
' x.1 -> one',
107+
' x.2 -> two',
108+
' x.3 -> three',
109+
' interrupting...',
110+
' x.a -> four',
111+
' x.qrz -> five',
112+
'scanInterleaved x. y.:',
113+
' 1: x.1 -> one',
114+
' 2: y.1 -> oney',
115+
' 1: x.2 -> two',
116+
' 2: y.2 -> twoy',
117+
' 1: x.3 -> three',
118+
' 2: y.3 -> threey',
119+
' 1: x.a -> four',
120+
' 2: y.a -> foury',
121+
' 1: x.qrz -> five',
122+
' 2: y.b -> fivey',
123+
' 2: y.c -> sixy',
124+
' 2: y.d -> seveny',
125+
'scan z.:',
126+
'apiAbuse x.: use prefix as prior key (should work)',
127+
' x.1 -> one',
128+
'apiAbuse x.: use out of range prior key aaax.',
129+
' getAfter(x., aaax.) threw Error: priorKey must start with keyPrefix',
130+
'apiAbuse x.: use invalid key prefix',
131+
' getAfter("ab@%%$#", "") threw Error: invalid vatstore key',
54132
]);
55-
for (const k of hostStorage.kvStore.getKeys('', '')) {
56-
if (k.endsWith('.foo')) {
57-
t.is(k, 'v1.vs.vvs.foo');
58-
}
59-
}
60133
});

0 commit comments

Comments
 (0)