diff --git a/packages/SwingSet/src/kernel/state/kernelKeeper.js b/packages/SwingSet/src/kernel/state/kernelKeeper.js index 2c6057e51897..7908340d7ccd 100644 --- a/packages/SwingSet/src/kernel/state/kernelKeeper.js +++ b/packages/SwingSet/src/kernel/state/kernelKeeper.js @@ -36,6 +36,7 @@ const enableKernelGC = true; // device.names = JSON([names..]) // device.name.$NAME = $deviceID = d$NN // device.nextID = $NN +// meter.nextID = $NN // used to make m$NN // kernelBundle = JSON(bundle) // bundle.$NAME = JSON(bundle) @@ -54,6 +55,9 @@ const enableKernelGC = true; // v$NN.vs.$key = string // v$NN.lastSnapshot = JSON({ snapshotID, startPos }) +// m$NN.remaining = $NN // non-negative remaining capacity (in computrons) +// m$NN.threshold = $NN // notify when .remaining first drops below this + // d$NN.o.nextID = $NN // d$NN.c.$kernelSlot = $deviceSlot = o-$NN/d+$NN/d-$NN // d$NN.c.$deviceSlot = $kernelSlot = ko$NN/kd$NN @@ -90,6 +94,12 @@ export function commaSplit(s) { return s.split(','); } +function insistMeterID(m) { + assert.typeof(m, 'string'); + assert.equal(m[0], 'm'); + Nat(BigInt(m.slice(1))); +} + // we use different starting index values for the various vNN/koNN/kdNN/kpNN // slots, to reduce confusing overlap when looking at debug messages (e.g. // seeing both kp1 and ko1, which are completely unrelated despite having the @@ -106,6 +116,7 @@ const FIRST_OBJECT_ID = 20n; const FIRST_DEVNODE_ID = 30n; const FIRST_PROMISE_ID = 40n; const FIRST_CRANK_NUMBER = 0n; +const FIRST_METER_ID = 1n; /** * @param {KVStorePlus} kvStore @@ -228,6 +239,7 @@ export default function makeKernelKeeper( kvStore.set('ko.nextID', `${FIRST_OBJECT_ID}`); kvStore.set('kd.nextID', `${FIRST_DEVNODE_ID}`); kvStore.set('kp.nextID', `${FIRST_PROMISE_ID}`); + kvStore.set('meter.nextID', `${FIRST_METER_ID}`); kvStore.set('gcActions', '[]'); kvStore.set('runQueue', JSON.stringify([])); kvStore.set('crankNumber', `${FIRST_CRANK_NUMBER}`); @@ -706,6 +718,58 @@ export default function makeKernelKeeper( return msg; } + function allocateMeter(remaining, threshold) { + const nextID = Nat(BigInt(getRequired('meter.nextID'))); + kvStore.set('meter.nextID', `${nextID + 1n}`); + const meterID = `m${nextID}`; + kvStore.set(`${meterID}.remaining`, `${Nat(remaining)}`); + kvStore.set(`${meterID}.threshold`, `${Nat(threshold)}`); + return meterID; + } + + function addMeterRemaining(meterID, delta) { + insistMeterID(meterID); + const remaining = parseInt(getRequired(`${meterID}.remaining`), 10); + kvStore.set(`${meterID}.remaining`, `${Nat(remaining) + Nat(delta)}`); + } + + function setMeterThreshold(meterID, threshold) { + insistMeterID(meterID); + kvStore.set(`${meterID}.threshold`, `${Nat(threshold)}`); + } + + function getMeter(meterID) { + insistMeterID(meterID); + const remaining = parseInt(getRequired(`${meterID}.remaining`), 10); + const threshold = parseInt(getRequired(`${meterID}.threshold`), 10); + return harden({ remaining, threshold }); + } + + function deductMeter(meterID, spent) { + insistMeterID(meterID); + Nat(spent); + const oldRemaining = parseInt(getRequired(`${meterID}.remaining`), 10); + const threshold = parseInt(getRequired(`${meterID}.threshold`), 10); + let underflow = false; + let notify = false; + let remaining = oldRemaining - spent; + if (remaining < 0) { + underflow = true; + remaining = 0; + } + if (remaining < threshold && oldRemaining >= threshold) { + notify = true; + } + kvStore.set(`${meterID}.remaining`, `${Nat(remaining)}`); + return harden({ underflow, notify }); + } + + function deleteMeter(meterID) { + insistMeterID(meterID); + kvStore.delete(`${meterID}.remaining`); + kvStore.delete(`${meterID}.threshold`); + } + function hasVatWithName(name) { return kvStore.has(`vat.name.${name}`); } @@ -1205,6 +1269,13 @@ export default function makeKernelKeeper( getRunQueueLength, getNextMsg, + allocateMeter, + addMeterRemaining, + setMeterThreshold, + getMeter, + deductMeter, + deleteMeter, + hasVatWithName, getVatIDForName, allocateVatIDForNameIfNeeded, diff --git a/packages/SwingSet/test/test-state.js b/packages/SwingSet/test/test-state.js index dc3e74957007..f2b2b81fa55a 100644 --- a/packages/SwingSet/test/test-state.js +++ b/packages/SwingSet/test/test-state.js @@ -287,6 +287,7 @@ test('kernel state', async t => { ['kd.nextID', '30'], ['kp.nextID', '40'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); }); @@ -321,6 +322,7 @@ test('kernelKeeper vat names', async t => { ['vat.name.vatname5', 'v1'], ['vat.name.Frank', 'v2'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); t.deepEqual(k.getStaticVats(), [ ['Frank', 'v2'], @@ -369,6 +371,7 @@ test('kernelKeeper device names', async t => { ['device.name.devicename5', 'd7'], ['device.name.Frank', 'd8'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); t.deepEqual(k.getDevices(), [ ['Frank', 'd8'], @@ -550,6 +553,7 @@ test('kernelKeeper promises', async t => { [`${ko}.owner`, 'v1'], [`${ko}.refCount`, '1,1'], ['kernel.defaultManagerType', 'local'], + ['meter.nextID', '1'], ]); }); @@ -663,3 +667,37 @@ test('XS vatKeeper defaultManagerType', async t => { k.createStartingKernelState('xs-worker'); t.is(k.getDefaultManagerType(), 'xs-worker'); }); + +test('meters', async t => { + const { kvStore, streamStore } = buildKeeperStorageInMemory(); + const k = makeKernelKeeper(kvStore, streamStore); + k.createStartingKernelState('local'); + const m1 = k.allocateMeter(100, 10); + const m2 = k.allocateMeter(200, 150); + t.not(m1, m2); + k.deleteMeter(m2); + t.deepEqual(k.getMeter(m1), { remaining: 100, threshold: 10 }); + t.deepEqual(k.deductMeter(m1, 10), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 10), { underflow: false, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 80, threshold: 10 }); + t.deepEqual(k.deductMeter(m1, 70), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 1), { underflow: false, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 9, threshold: 10 }); + t.deepEqual(k.deductMeter(m1, 1), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 9), { underflow: true, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 0, threshold: 10 }); + t.deepEqual(k.deductMeter(m1, 2), { underflow: true, notify: false }); + t.deepEqual(k.getMeter(m1), { remaining: 0, threshold: 10 }); + k.addMeterRemaining(m1, 50n); + t.deepEqual(k.getMeter(m1), { remaining: 50, threshold: 10 }); + t.deepEqual(k.deductMeter(m1, 30), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 25), { underflow: true, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 0, threshold: 10 }); + + k.addMeterRemaining(m1, 50n); + k.setMeterThreshold(m1, 40); + t.deepEqual(k.getMeter(m1), { remaining: 50, threshold: 40 }); + t.deepEqual(k.deductMeter(m1, 10), { underflow: false, notify: false }); + t.deepEqual(k.deductMeter(m1, 10), { underflow: false, notify: true }); + t.deepEqual(k.getMeter(m1), { remaining: 30, threshold: 40 }); +});