@agoric/swingset-vat 0.32.3-dev-0eaf7cc.0 → 0.32.3-dev-1f816f9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swingset-vat",
3
- "version": "0.32.3-dev-0eaf7cc.0+0eaf7cc",
3
+ "version": "0.32.3-dev-1f816f9.0+1f816f9",
4
4
  "description": "Vat/Container Launcher",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,16 +27,16 @@
27
27
  "@types/yargs-parser": "^21.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@agoric/internal": "0.3.3-dev-0eaf7cc.0+0eaf7cc",
31
- "@agoric/kmarshal": "0.1.1-dev-0eaf7cc.0+0eaf7cc",
32
- "@agoric/store": "0.9.3-dev-0eaf7cc.0+0eaf7cc",
33
- "@agoric/swing-store": "0.9.2-dev-0eaf7cc.0+0eaf7cc",
34
- "@agoric/swingset-liveslots": "0.10.3-dev-0eaf7cc.0+0eaf7cc",
35
- "@agoric/swingset-xsnap-supervisor": "0.10.3-dev-0eaf7cc.0+0eaf7cc",
36
- "@agoric/time": "0.3.3-dev-0eaf7cc.0+0eaf7cc",
37
- "@agoric/vat-data": "0.5.3-dev-0eaf7cc.0+0eaf7cc",
38
- "@agoric/xsnap": "0.14.3-dev-0eaf7cc.0+0eaf7cc",
39
- "@agoric/xsnap-lockdown": "0.14.1-dev-0eaf7cc.0+0eaf7cc",
30
+ "@agoric/internal": "0.3.3-dev-1f816f9.0+1f816f9",
31
+ "@agoric/kmarshal": "0.1.1-dev-1f816f9.0+1f816f9",
32
+ "@agoric/store": "0.9.3-dev-1f816f9.0+1f816f9",
33
+ "@agoric/swing-store": "0.9.2-dev-1f816f9.0+1f816f9",
34
+ "@agoric/swingset-liveslots": "0.10.3-dev-1f816f9.0+1f816f9",
35
+ "@agoric/swingset-xsnap-supervisor": "0.10.3-dev-1f816f9.0+1f816f9",
36
+ "@agoric/time": "0.3.3-dev-1f816f9.0+1f816f9",
37
+ "@agoric/vat-data": "0.5.3-dev-1f816f9.0+1f816f9",
38
+ "@agoric/xsnap": "0.14.3-dev-1f816f9.0+1f816f9",
39
+ "@agoric/xsnap-lockdown": "0.14.1-dev-1f816f9.0+1f816f9",
40
40
  "@endo/base64": "^1.0.6",
41
41
  "@endo/bundle-source": "^3.3.0",
42
42
  "@endo/captp": "^4.2.2",
@@ -102,5 +102,5 @@
102
102
  "typeCoverage": {
103
103
  "atLeast": 75.1
104
104
  },
105
- "gitHead": "0eaf7cc8151b4e1966f3f33e43362d3d021457aa"
105
+ "gitHead": "1f816f92e8b4f79c43df9608d7a7b443a3ceca20"
106
106
  }
@@ -196,6 +196,14 @@ export const upgradeSwingset = kernelStorage => {
196
196
  version = 1;
197
197
  }
198
198
 
199
+ if (version < 2) {
200
+ // schema v2: add vats.terminated = []
201
+ assert(!kvStore.has('vats.terminated'));
202
+ kvStore.set('vats.terminated', JSON.stringify([]));
203
+ modified = true;
204
+ version = 2;
205
+ }
206
+
199
207
  if (modified) {
200
208
  kvStore.set('version', `${version}`);
201
209
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { assert, Fail } from '@endo/errors';
4
4
  import { isNat } from '@endo/nat';
5
+ import { mustMatch, M } from '@endo/patterns';
5
6
  import { importBundle } from '@endo/import-bundle';
6
7
  import { makeUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js';
7
8
  import { kser, kslot, makeError } from '@agoric/kmarshal';
@@ -275,12 +276,16 @@ export default function buildKernel(
275
276
  // (#9157). The fix will add .critical to CrankResults, populated by a
276
277
  // getOptions query in deliveryCrankResults() or copied from
277
278
  // dynamicOptions in processCreateVat.
278
- critical = kernelKeeper.provideVatKeeper(vatID).getOptions().critical;
279
+ const vatKeeper = kernelKeeper.provideVatKeeper(vatID);
280
+ critical = vatKeeper.getOptions().critical;
279
281
 
280
282
  // Reject all promises decided by the vat, making sure to capture the list
281
283
  // of kpids before that data is deleted.
282
284
  const deadPromises = [...kernelKeeper.enumeratePromisesByDecider(vatID)];
283
- kernelKeeper.cleanupAfterTerminatedVat(vatID);
285
+ // remove vatID from the list of live vats, and mark for deletion
286
+ kernelKeeper.deleteVatID(vatID);
287
+ kernelKeeper.markVatAsTerminated(vatID);
288
+ kernelKeeper.removeVatFromSwingStoreExports(vatID);
284
289
  for (const kpid of deadPromises) {
285
290
  resolveToError(kpid, makeError('vat terminated'), vatID);
286
291
  }
@@ -387,7 +392,8 @@ export default function buildKernel(
387
392
  * abort?: boolean, // changes should be discarded, not committed
388
393
  * consumeMessage?: boolean, // discard the aborted delivery
389
394
  * didDelivery?: VatID, // we made a delivery to a vat, for run policy and save-snapshot
390
- * computrons?: BigInt, // computron count for run policy
395
+ * computrons?: bigint, // computron count for run policy
396
+ * cleanups?: number, // cleanup budget spent
391
397
  * meterID?: string, // deduct those computrons from a meter
392
398
  * measureDirt?: { vatID: VatID, dirt: Dirt }, // dirt counters should increment
393
399
  * terminate?: { vatID: VatID, reject: boolean, info: SwingSetCapData }, // terminate vat, notify vat-admin
@@ -651,16 +657,40 @@ export default function buildKernel(
651
657
  if (!vatWarehouse.lookup(vatID)) {
652
658
  return NO_DELIVERY_CRANK_RESULTS; // can't collect from the dead
653
659
  }
654
- const vatKeeper = kernelKeeper.provideVatKeeper(vatID);
655
660
  /** @type { KernelDeliveryBringOutYourDead } */
656
661
  const kd = harden([type]);
657
662
  const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd);
658
663
  const status = await deliverAndLogToVat(vatID, kd, vd);
659
- vatKeeper.clearReapDirt(); // BOYD zeros out the when-to-BOYD counters
660
664
  // no gcKrefs, BOYD clears them anyways
661
665
  return deliveryCrankResults(vatID, status, false); // no meter, BOYD clears dirt
662
666
  }
663
667
 
668
+ /**
669
+ * Perform a small (budget-limited) amount of dead-vat cleanup work.
670
+ *
671
+ * @param {RunQueueEventCleanupTerminatedVat} message
672
+ * 'message' is the run-queue cleanup action, which includes a vatID and budget.
673
+ * A budget of 'undefined' allows unlimited work. Otherwise, the budget is a Number,
674
+ * and cleanup should not touch more than maybe 5*budget DB rows.
675
+ * @returns {Promise<CrankResults>}
676
+ */
677
+ async function processCleanupTerminatedVat(message) {
678
+ const { vatID, budget } = message;
679
+ const { done, cleanups } = kernelKeeper.cleanupAfterTerminatedVat(
680
+ vatID,
681
+ budget,
682
+ );
683
+ if (done) {
684
+ kernelKeeper.forgetTerminatedVat(vatID);
685
+ kernelSlog.write({ type: 'vat-cleanup-complete', vatID });
686
+ }
687
+ // We don't perform any deliveries here, so there are no computrons to
688
+ // report, but we do tell the runPolicy know how much kernel-side DB
689
+ // work we did, so it can decide how much was too much.
690
+ const computrons = 0n;
691
+ return harden({ computrons, cleanups });
692
+ }
693
+
664
694
  /**
665
695
  * The 'startVat' event is queued by `initializeKernel` for all static vats,
666
696
  * so that we execute their bundle imports and call their `buildRootObject`
@@ -916,7 +946,6 @@ export default function buildKernel(
916
946
  const boydVD = vatWarehouse.kernelDeliveryToVatDelivery(vatID, boydKD);
917
947
  const boydStatus = await deliverAndLogToVat(vatID, boydKD, boydVD);
918
948
  const boydResults = deliveryCrankResults(vatID, boydStatus, false);
919
- vatKeeper.clearReapDirt();
920
949
 
921
950
  // we don't meter bringOutYourDead since no user code is running, but we
922
951
  // still report computrons to the runPolicy
@@ -1172,6 +1201,7 @@ export default function buildKernel(
1172
1201
  * @typedef { import('../types-internal.js').RunQueueEventRetireImports } RunQueueEventRetireImports
1173
1202
  * @typedef { import('../types-internal.js').RunQueueEventNegatedGCAction } RunQueueEventNegatedGCAction
1174
1203
  * @typedef { import('../types-internal.js').RunQueueEventBringOutYourDead } RunQueueEventBringOutYourDead
1204
+ * @typedef { import('../types-internal.js').RunQueueEventCleanupTerminatedVat } RunQueueEventCleanupTerminatedVat
1175
1205
  * @typedef { import('../types-internal.js').RunQueueEvent } RunQueueEvent
1176
1206
  */
1177
1207
 
@@ -1239,6 +1269,8 @@ export default function buildKernel(
1239
1269
  } else if (message.type === 'negated-gc-action') {
1240
1270
  // processGCActionSet pruned some negated actions, but had no GC
1241
1271
  // action to perform. Record the DB changes in their own crank.
1272
+ } else if (message.type === 'cleanup-terminated-vat') {
1273
+ deliverP = processCleanupTerminatedVat(message);
1242
1274
  } else if (gcMessages.includes(message.type)) {
1243
1275
  deliverP = processGCMessage(message);
1244
1276
  } else {
@@ -1289,18 +1321,13 @@ export default function buildKernel(
1289
1321
  const crankResults = await deliverRunQueueEvent(message);
1290
1322
  // { abort/commit, deduct, terminate+notify, consumeMessage }
1291
1323
 
1292
- if (crankResults.didDelivery) {
1293
- if (message.type === 'create-vat') {
1294
- // TODO: create-vat now gets metering, at least for the
1295
- // dispatch.startVat . We should probably tell the policy about
1296
- // the creation too since there's extra overhead (we're
1297
- // launching a new child process, at least, although that
1298
- // sometimes happens randomly because of vat eviction policy
1299
- // which should not affect the in-consensus policyInput)
1300
- policyInput = ['create-vat', {}];
1301
- } else {
1302
- policyInput = ['crank', {}];
1303
- }
1324
+ if (message.type === 'cleanup-terminated-vat') {
1325
+ const { cleanups } = crankResults;
1326
+ assert(cleanups !== undefined);
1327
+ policyInput = ['cleanup', { cleanups }];
1328
+ } else if (crankResults.didDelivery) {
1329
+ const tag = message.type === 'create-vat' ? 'create-vat' : 'crank';
1330
+ policyInput = [tag, {}];
1304
1331
  }
1305
1332
 
1306
1333
  // Deliveries cause syscalls, syscalls might cause errors
@@ -1758,16 +1785,24 @@ export default function buildKernel(
1758
1785
  }
1759
1786
  }
1760
1787
 
1788
+ const allowCleanupShape = M.or(
1789
+ // 'false' will prohibit cleanup
1790
+ false,
1791
+ // otherwise allow cleanup, optionally with a limiting budget
1792
+ M.splitRecord({}, { budget: M.number() }, M.record()),
1793
+ );
1794
+
1761
1795
  /**
1762
1796
  * Pulls the next message from the highest-priority queue and returns it
1763
1797
  * along with a corresponding processor.
1764
1798
  *
1799
+ * @param {RunPolicy} [policy] - a RunPolicy to limit the work being done
1765
1800
  * @returns {{
1766
1801
  * message: RunQueueEvent | undefined,
1767
1802
  * processor: (message: RunQueueEvent) => Promise<PolicyInput>,
1768
1803
  * }}
1769
1804
  */
1770
- function getNextMessageAndProcessor() {
1805
+ function getNextMessageAndProcessor(policy) {
1771
1806
  const acceptanceMessage = kernelKeeper.getNextAcceptanceQueueMsg();
1772
1807
  if (acceptanceMessage) {
1773
1808
  return {
@@ -1775,7 +1810,12 @@ export default function buildKernel(
1775
1810
  processor: processAcceptanceMessage,
1776
1811
  };
1777
1812
  }
1813
+ // Absent specific configuration, allow unlimited cleanup.
1814
+ const allowCleanup = policy?.allowCleanup?.() ?? {};
1815
+ mustMatch(harden(allowCleanup), allowCleanupShape);
1816
+
1778
1817
  const message =
1818
+ kernelKeeper.nextCleanupTerminatedVatAction(allowCleanup) ||
1779
1819
  processGCActionSet(kernelKeeper) ||
1780
1820
  kernelKeeper.nextReapAction() ||
1781
1821
  kernelKeeper.getNextRunQueueMsg();
@@ -1885,7 +1925,7 @@ export default function buildKernel(
1885
1925
  kernelKeeper.startCrank();
1886
1926
  try {
1887
1927
  kernelKeeper.establishCrankSavepoint('start');
1888
- const { processor, message } = getNextMessageAndProcessor();
1928
+ const { processor, message } = getNextMessageAndProcessor(policy);
1889
1929
  if (!message) {
1890
1930
  break;
1891
1931
  }
@@ -1907,6 +1947,13 @@ export default function buildKernel(
1907
1947
  case 'crank-failed':
1908
1948
  policyOutput = policy.crankFailed(policyInput[1]);
1909
1949
  break;
1950
+ case 'cleanup': {
1951
+ // Give the policy a chance to interrupt kernel execution,
1952
+ // but default to continuing.
1953
+ const { didCleanup } = policy;
1954
+ policyOutput = didCleanup ? didCleanup(policyInput[1]) : true;
1955
+ break;
1956
+ }
1910
1957
  case 'none':
1911
1958
  policyOutput = policy.emptyCrank();
1912
1959
  break;
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-use-before-define */
2
2
  import { Nat, isNat } from '@endo/nat';
3
3
  import { assert, Fail } from '@endo/errors';
4
+ import { objectMetaMap } from '@agoric/internal';
4
5
  import {
5
6
  initializeVatState,
6
7
  makeVatKeeper,
@@ -50,7 +51,7 @@ const enableKernelGC = true;
50
51
  export { DEFAULT_REAP_DIRT_THRESHOLD_KEY };
51
52
 
52
53
  // most recent DB schema version
53
- export const CURRENT_SCHEMA_VERSION = 1;
54
+ export const CURRENT_SCHEMA_VERSION = 2;
54
55
 
55
56
  // Kernel state lives in a key-value store supporting key retrieval by
56
57
  // lexicographic range. All keys and values are strings.
@@ -69,14 +70,15 @@ export const CURRENT_SCHEMA_VERSION = 1;
69
70
  // only modified by a call to upgradeSwingset(). See below for
70
71
  // deltas/upgrades from one version to the next.
71
72
  //
72
- // The current ("v1") schema keys/values are:
73
+ // The current ("v2") schema keys/values are:
73
74
  //
74
- // version = '1'
75
+ // version = '2'
75
76
  // vat.names = JSON([names..])
76
77
  // vat.dynamicIDs = JSON([vatIDs..])
77
78
  // vat.name.$NAME = $vatID = v$NN
78
79
  // vat.nextID = $NN
79
80
  // vat.nextUpgradeID = $NN
81
+ // vats.terminated = JSON([vatIDs..])
80
82
  // device.names = JSON([names..])
81
83
  // device.name.$NAME = $deviceID = d$NN
82
84
  // device.nextID = $NN
@@ -172,6 +174,9 @@ export const CURRENT_SCHEMA_VERSION = 1;
172
174
  // * replace `kernel.defaultReapInterval` with `kernel.defaultReapDirtThreshold`
173
175
  // * replace vat's `vNN.reapInterval`/`vNN.reapCountdown` with `vNN.reapDirt`
174
176
  // and a `vNN.reapDirtThreshold` in `vNN.options`
177
+ // v2:
178
+ // * change `version` to `'2'`
179
+ // * add `vats.terminated` with `[]` as initial value
175
180
 
176
181
  export function commaSplit(s) {
177
182
  if (s === '') {
@@ -180,6 +185,11 @@ export function commaSplit(s) {
180
185
  return s.split(',');
181
186
  }
182
187
 
188
+ export function stripPrefix(prefix, str) {
189
+ assert(str.startsWith(prefix), str);
190
+ return str.slice(prefix.length);
191
+ }
192
+
183
193
  function insistMeterID(m) {
184
194
  assert.typeof(m, 'string');
185
195
  assert.equal(m[0], 'm');
@@ -248,6 +258,12 @@ export default function makeKernelKeeper(
248
258
 
249
259
  insistStorageAPI(kvStore);
250
260
 
261
+ // the terminated-vats cache is normally populated from
262
+ // 'vats.terminated', but for initialization purposes we need give
263
+ // it a value here, and then populate it for real if we're dealing
264
+ // with an up-to-date DB
265
+ let terminatedVats = [];
266
+
251
267
  const versionString = kvStore.get('version');
252
268
  const version = Number(versionString || '0');
253
269
  if (expectedVersion === 'uninitialized') {
@@ -261,6 +277,9 @@ export default function makeKernelKeeper(
261
277
  throw Error(
262
278
  `kernel DB is too old: has version v${version}, but expected v${expectedVersion}`,
263
279
  );
280
+ } else {
281
+ // DB is up-to-date, so populate any caches we use
282
+ terminatedVats = JSON.parse(getRequired('vats.terminated'));
264
283
  }
265
284
 
266
285
  /**
@@ -399,6 +418,7 @@ export default function makeKernelKeeper(
399
418
  kvStore.set('vat.dynamicIDs', '[]');
400
419
  kvStore.set('vat.nextID', `${FIRST_VAT_ID}`);
401
420
  kvStore.set('vat.nextUpgradeID', `1`);
421
+ kvStore.set('vats.terminated', '[]');
402
422
  kvStore.set('device.names', '[]');
403
423
  kvStore.set('device.nextID', `${FIRST_DEVICE_ID}`);
404
424
  kvStore.set('ko.nextID', `${FIRST_OBJECT_ID}`);
@@ -679,8 +699,12 @@ export default function makeKernelKeeper(
679
699
  function ownerOfKernelObject(kernelSlot) {
680
700
  insistKernelType('object', kernelSlot);
681
701
  const owner = kvStore.get(`${kernelSlot}.owner`);
682
- if (owner) {
683
- insistVatID(owner);
702
+ if (!owner) {
703
+ return undefined;
704
+ }
705
+ insistVatID(owner);
706
+ if (terminatedVats.includes(owner)) {
707
+ return undefined;
684
708
  }
685
709
  return owner;
686
710
  }
@@ -885,13 +909,70 @@ export default function makeKernelKeeper(
885
909
  kvStore.set(`${kernelSlot}.data.slots`, capdata.slots.join(','));
886
910
  }
887
911
 
888
- function cleanupAfterTerminatedVat(vatID) {
912
+ function removeVatFromSwingStoreExports(vatID) {
913
+ // Delete primary swingstore records for this vat, in preparation
914
+ // for (slow) deletion. After this, swingstore exports will omit
915
+ // this vat. This is called from the kernel's terminateVat, which
916
+ // initiates (but does not complete) deletion.
917
+ snapStore.stopUsingLastSnapshot(vatID);
918
+ transcriptStore.stopUsingTranscript(vatID);
919
+ }
920
+
921
+ /**
922
+ * Perform some cleanup work for a specific (terminated but not
923
+ * fully-deleted) vat, possibly limited by a budget. Returns 'done'
924
+ * (where false means "please call me again", and true means "you
925
+ * can delete the vatID now"), and a count of how much work was done
926
+ * (so the runPolicy can decide when to stop).
927
+ *
928
+ * @param {string} vatID
929
+ * @param {number} [budget]
930
+ * @returns {{ done: boolean, cleanups: number }}
931
+ *
932
+ */
933
+ function cleanupAfterTerminatedVat(vatID, budget = undefined) {
889
934
  insistVatID(vatID);
890
- const vatKeeper = provideVatKeeper(vatID);
891
- const exportPrefix = `${vatID}.c.o+`;
892
- const importPrefix = `${vatID}.c.o-`;
935
+ let cleanups = 0;
936
+ const work = {
937
+ exports: 0,
938
+ imports: 0,
939
+ kv: 0,
940
+ snapshots: 0,
941
+ transcripts: 0,
942
+ };
943
+ let spend = _did => false; // returns "stop now"
944
+ if (budget !== undefined) {
945
+ assert.typeof(budget, 'number');
946
+ spend = (did = 1) => {
947
+ assert(budget !== undefined); // hush TSC
948
+ cleanups += did;
949
+ budget -= did;
950
+ return budget <= 0;
951
+ };
952
+ }
953
+ const logWork = () => {
954
+ const w = objectMetaMap(work, desc => (desc.value ? desc : undefined));
955
+ kernelSlog?.write({ type: 'vat-cleanup', vatID, work: w });
956
+ };
957
+
958
+ // TODO: it would be slightly cheaper to walk all kvStore keys in
959
+ // order, and act on each one according to its category (c-list
960
+ // export, c-list import, vatstore, other), so we use a single
961
+ // enumeratePrefixedKeys() call each time. Until we do that, the
962
+ // last phase of the cleanup (where we've deleted all the exports
963
+ // and imports, and are working on the remaining keys) will waste
964
+ // two DB queries on each call. OTOH, those queries will probably
965
+ // hit the same SQLite index page as the successful one, so it
966
+ // probably won't cause any extra disk IO. So we can defer this
967
+ // optimization for a while. Note: when we implement it, be
968
+ // prepared to encounter the clist entries in eiher order (kref
969
+ // first or vref first), and delete the other one in the same
970
+ // call, so we don't wind up with half an entry.
893
971
 
894
- vatKeeper.deleteSnapshotsAndTranscript();
972
+ const vatKeeper = provideVatKeeper(vatID);
973
+ const clistPrefix = `${vatID}.c.`;
974
+ const exportPrefix = `${clistPrefix}o+`;
975
+ const importPrefix = `${clistPrefix}o-`;
895
976
 
896
977
  // Note: ASCII order is "+,-./", and we rely upon this to split the
897
978
  // keyspace into the various o+NN/o-NN/etc spaces. If we were using a
@@ -915,8 +996,27 @@ export default function makeKernelKeeper(
915
996
  // begin with `vMM.c.o+`. In addition to deleting the c-list entry, we
916
997
  // must also delete the corresponding kernel owner entry for the object,
917
998
  // since the object will no longer be accessible.
999
+ const vref = stripPrefix(clistPrefix, k);
1000
+ assert(vref.startsWith('o+'), vref);
918
1001
  const kref = kvStore.get(k);
919
- orphanKernelObject(kref, vatID);
1002
+ // we must delete the c-list entry, but we don't need to
1003
+ // manipulate refcounts like the way vatKeeper/deleteCListEntry
1004
+ // does, so just delete the keys directly
1005
+ // vatKeeper.deleteCListEntry(kref, vref);
1006
+ kvStore.delete(`${vatID}.c.${kref}`);
1007
+ kvStore.delete(`${vatID}.c.${vref}`);
1008
+ // if this object became unreferenced, processRefcounts will
1009
+ // delete it, so don't be surprised. TODO: this results in an
1010
+ // extra get() for each delete(), see if there's a way to avoid
1011
+ // this. TODO: think carefully about other such races.
1012
+ if (kvStore.has(`${kref}.owner`)) {
1013
+ orphanKernelObject(kref, vatID);
1014
+ }
1015
+ work.exports += 1;
1016
+ if (spend()) {
1017
+ logWork();
1018
+ return { done: false, cleanups };
1019
+ }
920
1020
  }
921
1021
 
922
1022
  // then scan for imported objects, which must be decrefed
@@ -924,9 +1024,14 @@ export default function makeKernelKeeper(
924
1024
  // abandoned imports: delete the clist entry as if the vat did a
925
1025
  // drop+retire
926
1026
  const kref = kvStore.get(k) || Fail`getNextKey ensures get`;
927
- const vref = k.slice(`${vatID}.c.`.length);
1027
+ const vref = stripPrefix(clistPrefix, k);
928
1028
  vatKeeper.deleteCListEntry(kref, vref);
929
1029
  // that will also delete both db keys
1030
+ work.imports += 1;
1031
+ if (spend()) {
1032
+ logWork();
1033
+ return { done: false, cleanups };
1034
+ }
930
1035
  }
931
1036
 
932
1037
  // the caller used enumeratePromisesByDecider() before calling us,
@@ -935,8 +1040,31 @@ export default function makeKernelKeeper(
935
1040
  // now loop back through everything and delete it all
936
1041
  for (const k of enumeratePrefixedKeys(kvStore, `${vatID}.`)) {
937
1042
  kvStore.delete(k);
1043
+ work.kv += 1;
1044
+ if (spend()) {
1045
+ logWork();
1046
+ return { done: false, cleanups };
1047
+ }
1048
+ }
1049
+
1050
+ // this will internally loop through 'budget' deletions
1051
+ const dsc = vatKeeper.deleteSnapshots(budget);
1052
+ work.snapshots += dsc.cleanups;
1053
+ if (spend(dsc.cleanups)) {
1054
+ logWork();
1055
+ return { done: false, cleanups };
938
1056
  }
939
1057
 
1058
+ // same
1059
+ const dts = vatKeeper.deleteTranscripts(budget);
1060
+ work.transcripts += dts.cleanups;
1061
+ // last task, so increment cleanups, but dc.done is authoritative
1062
+ spend(dts.cleanups);
1063
+ logWork();
1064
+ return { done: dts.done, cleanups };
1065
+ }
1066
+
1067
+ function deleteVatID(vatID) {
940
1068
  // TODO: deleting entries from the dynamic vat IDs list requires a linear
941
1069
  // scan of the list; arguably this collection ought to be represented in a
942
1070
  // different way that makes it efficient to remove an entry from it, though
@@ -1221,6 +1349,41 @@ export default function makeKernelKeeper(
1221
1349
  return makeUpgradeID(nextID);
1222
1350
  }
1223
1351
 
1352
+ function markVatAsTerminated(vatID) {
1353
+ if (!terminatedVats.includes(vatID)) {
1354
+ terminatedVats.push(vatID);
1355
+ kvStore.set(`vats.terminated`, JSON.stringify(terminatedVats));
1356
+ }
1357
+ }
1358
+
1359
+ function getFirstTerminatedVat() {
1360
+ if (terminatedVats.length) {
1361
+ return terminatedVats[0];
1362
+ }
1363
+ return undefined;
1364
+ }
1365
+
1366
+ function forgetTerminatedVat(vatID) {
1367
+ terminatedVats = terminatedVats.filter(id => id !== vatID);
1368
+ kvStore.set(`vats.terminated`, JSON.stringify(terminatedVats));
1369
+ }
1370
+
1371
+ function nextCleanupTerminatedVatAction(allowCleanup) {
1372
+ if (!allowCleanup) {
1373
+ return undefined;
1374
+ }
1375
+ // budget === undefined means "unlimited"
1376
+ const { budget } = allowCleanup;
1377
+ // if (getGCActions().size) {
1378
+ // return undefined;
1379
+ // }
1380
+ const vatID = getFirstTerminatedVat();
1381
+ if (vatID) {
1382
+ return { type: 'cleanup-terminated-vat', vatID, budget };
1383
+ }
1384
+ return undefined;
1385
+ }
1386
+
1224
1387
  // As refcounts are decremented, we accumulate a set of krefs for which
1225
1388
  // action might need to be taken:
1226
1389
  // * promises which are now resolved and unreferenced can be deleted
@@ -1413,7 +1576,7 @@ export default function makeKernelKeeper(
1413
1576
 
1414
1577
  function vatIsAlive(vatID) {
1415
1578
  insistVatID(vatID);
1416
- return kvStore.has(`${vatID}.o.nextID`);
1579
+ return kvStore.has(`${vatID}.o.nextID`) && !terminatedVats.includes(vatID);
1417
1580
  }
1418
1581
 
1419
1582
  /**
@@ -1705,11 +1868,18 @@ export default function makeKernelKeeper(
1705
1868
  provideVatKeeper,
1706
1869
  vatIsAlive,
1707
1870
  evictVatKeeper,
1871
+ removeVatFromSwingStoreExports,
1708
1872
  cleanupAfterTerminatedVat,
1709
1873
  addDynamicVatID,
1710
1874
  getDynamicVats,
1711
1875
  getStaticVats,
1712
1876
  getDevices,
1877
+ deleteVatID,
1878
+
1879
+ markVatAsTerminated,
1880
+ getFirstTerminatedVat,
1881
+ forgetTerminatedVat,
1882
+ nextCleanupTerminatedVatAction,
1713
1883
 
1714
1884
  allocateUpgradeID,
1715
1885
 
@@ -675,11 +675,44 @@ export function makeVatKeeper(
675
675
  });
676
676
  }
677
677
 
678
- function deleteSnapshotsAndTranscript() {
679
- if (snapStore) {
680
- snapStore.deleteVatSnapshots(vatID);
678
+ /**
679
+ * Perform some (possibly-limited) cleanup work for a vat. Returns
680
+ * 'done' (where false means "please call me again", and true means
681
+ * "you can delete the vatID now"), and a count of how much work was
682
+ * done (so the runPolicy can decide when to stop).
683
+ *
684
+ * @param {number} [budget]
685
+ * @returns {{ done: boolean, cleanups: number }}
686
+ *
687
+ */
688
+ function deleteSnapshots(budget = undefined) {
689
+ // Each budget=1 allows us to delete one snapshot entry.
690
+ if (!snapStore) {
691
+ return { done: true, cleanups: 0 };
681
692
  }
682
- transcriptStore.deleteVatTranscripts(vatID);
693
+ // initially uses 2+2*budget DB statements, then just 1 when done
694
+ return snapStore.deleteVatSnapshots(vatID, budget);
695
+ }
696
+
697
+ /**
698
+ * Perform some (possibly-limited) cleanup work for a vat. Returns
699
+ * 'done' (where false means "please call me again", and true means
700
+ * "you can delete the vatID now"), and a count of how much work was
701
+ * done (so the runPolicy can decide when to stop).
702
+ *
703
+ * @param {number} [budget]
704
+ * @returns {{ done: boolean, cleanups: number }}
705
+ *
706
+ */
707
+ function deleteTranscripts(budget = undefined) {
708
+ // Each budget=1 allows us to delete one transcript span and any
709
+ // transcript items associated with that span. Some nodes will
710
+ // have historical transcript items, some will not. Using budget=5
711
+ // and snapshotInterval=200 means we delete 5 span records and
712
+ // maybe 1000 span items.
713
+
714
+ // initially uses 2+3*budget DB statements, then just 1 when done
715
+ return transcriptStore.deleteVatTranscripts(vatID, budget);
683
716
  }
684
717
 
685
718
  function beginNewIncarnation() {
@@ -761,7 +794,8 @@ export function makeVatKeeper(
761
794
  dumpState,
762
795
  saveSnapshot,
763
796
  getSnapshotInfo,
764
- deleteSnapshotsAndTranscript,
797
+ deleteSnapshots,
798
+ deleteTranscripts,
765
799
  beginNewIncarnation,
766
800
  });
767
801
  }
@@ -544,6 +544,10 @@ export function makeVatWarehouse({
544
544
  vatKeeper.addToTranscript(getTranscriptEntry(vd, deliveryResult));
545
545
  }
546
546
 
547
+ if (kd[0] === 'bringOutYourDead') {
548
+ vatKeeper.clearReapDirt(); // BOYD zeros out the when-to-BOYD counters
549
+ }
550
+
547
551
  // TODO: if per-vat policy decides it wants a BOYD or heap snapshot,
548
552
  // now is the time to do it, or to ask the kernel to schedule it
549
553
 
@@ -3,6 +3,9 @@ import { assert } from '@endo/errors';
3
3
  export function foreverPolicy() {
4
4
  /** @type { RunPolicy } */
5
5
  return harden({
6
+ allowCleanup() {
7
+ return {}; // unlimited budget
8
+ },
6
9
  vatCreated(_details) {
7
10
  return true;
8
11
  },
@@ -27,6 +30,9 @@ export function crankCounter(
27
30
  let vats = 0;
28
31
  /** @type { RunPolicy } */
29
32
  const policy = harden({
33
+ allowCleanup() {
34
+ return { budget: 100 }; // limited budget
35
+ },
30
36
  vatCreated() {
31
37
  vats += 1;
32
38
  return vats < maxCreateVats;
@@ -47,13 +53,21 @@ export function crankCounter(
47
53
  return policy;
48
54
  }
49
55
 
50
- export function computronCounter(limit) {
56
+ export function computronCounter(limit, options = {}) {
51
57
  assert.typeof(limit, 'bigint');
58
+ const {
59
+ cleanupBudget = 100,
60
+ vatCreatedComputrons = 100_000n, // pretend that's the cost
61
+ crankFailedComputrons = 1_000_000n,
62
+ } = options;
52
63
  let total = 0n;
53
64
  /** @type { RunPolicy } */
54
65
  const policy = harden({
66
+ allowCleanup() {
67
+ return { budget: cleanupBudget }; // limited budget
68
+ },
55
69
  vatCreated() {
56
- total += 100000n; // pretend vat creation takes 100k computrons
70
+ total += vatCreatedComputrons;
57
71
  return total < limit;
58
72
  },
59
73
  crankComplete(details = {}) {
@@ -65,7 +79,7 @@ export function computronCounter(limit) {
65
79
  return total < limit;
66
80
  },
67
81
  crankFailed() {
68
- total += 1000000n; // who knows, 1M is as good as anything
82
+ total += crankFailedComputrons;
69
83
  return total < limit;
70
84
  },
71
85
  emptyCrank() {
@@ -79,6 +93,7 @@ export function wallClockWaiter(seconds) {
79
93
  const timeout = Date.now() + 1000 * seconds;
80
94
  /** @type { RunPolicy } */
81
95
  const policy = harden({
96
+ allowCleanup: () => ({}), // unlimited budget
82
97
  vatCreated: () => Date.now() < timeout,
83
98
  crankComplete: () => Date.now() < timeout,
84
99
  crankFailed: () => Date.now() < timeout,
@@ -86,3 +101,34 @@ export function wallClockWaiter(seconds) {
86
101
  });
87
102
  return policy;
88
103
  }
104
+
105
+ export function noCleanup() {
106
+ /** @type { RunPolicy } */
107
+ const policy = harden({
108
+ allowCleanup: () => false,
109
+ vatCreated: () => true,
110
+ crankComplete: () => true,
111
+ crankFailed: () => true,
112
+ emptyCrank: () => true,
113
+ });
114
+ return policy;
115
+ }
116
+
117
+ export function someCleanup(budget) {
118
+ let once = true;
119
+ /** @type { RunPolicy } */
120
+ const policy = harden({
121
+ allowCleanup: () => {
122
+ if (once) {
123
+ once = false;
124
+ return { budget };
125
+ }
126
+ return false;
127
+ },
128
+ vatCreated: () => true,
129
+ crankComplete: () => true,
130
+ crankFailed: () => true,
131
+ emptyCrank: () => true,
132
+ });
133
+ return policy;
134
+ }
@@ -224,12 +224,17 @@ export {};
224
224
  * @typedef { [tag: 'create-vat', details: PolicyInputDetails ]} PolicyInputCreateVat
225
225
  * @typedef { [tag: 'crank', details: PolicyInputDetails ] } PolicyInputCrankComplete
226
226
  * @typedef { [tag: 'crank-failed', details: PolicyInputDetails ]} PolicyInputCrankFailed
227
- * @typedef { PolicyInputNone | PolicyInputCreateVat | PolicyInputCrankComplete | PolicyInputCrankFailed } PolicyInput
227
+ * @typedef { [tag: 'cleanup', details: { cleanups: number, computrons?: number }] } PolicyInputCleanup
228
+ * @typedef { PolicyInputNone | PolicyInputCreateVat | PolicyInputCrankComplete |
229
+ * PolicyInputCrankFailed | PolicyInputCleanup } PolicyInput
228
230
  * @typedef { boolean } PolicyOutput
229
- * @typedef { { vatCreated: (details: {}) => PolicyOutput,
231
+ * @typedef { {
232
+ * allowCleanup?: () => false | { budget?: number },
233
+ * vatCreated: (details: {}) => PolicyOutput,
230
234
  * crankComplete: (details: { computrons?: bigint }) => PolicyOutput,
231
235
  * crankFailed: (details: {}) => PolicyOutput,
232
236
  * emptyCrank: () => PolicyOutput,
237
+ * didCleanup?: (details: { cleanups: number }) => PolicyOutput,
233
238
  * } } RunPolicy
234
239
  *
235
240
  * @typedef {object} VatWarehousePolicy
@@ -143,10 +143,12 @@ export {};
143
143
  * @typedef { { type: 'retireImports', vatID: VatID, krefs: string[] } } RunQueueEventRetireImports
144
144
  * @typedef { { type: 'negated-gc-action', vatID?: VatID } } RunQueueEventNegatedGCAction
145
145
  * @typedef { { type: 'bringOutYourDead', vatID: VatID } } RunQueueEventBringOutYourDead
146
+ * @typedef { { type: 'cleanup-terminated-vat', vatID: VatID,
147
+ * budget: number | undefined } } RunQueueEventCleanupTerminatedVat
146
148
  * @typedef { RunQueueEventNotify | RunQueueEventSend | RunQueueEventCreateVat |
147
149
  * RunQueueEventUpgradeVat | RunQueueEventChangeVatOptions | RunQueueEventStartVat |
148
150
  * RunQueueEventDropExports | RunQueueEventRetireExports | RunQueueEventRetireImports |
149
- * RunQueueEventNegatedGCAction | RunQueueEventBringOutYourDead
151
+ * RunQueueEventNegatedGCAction | RunQueueEventBringOutYourDead | RunQueueEventCleanupTerminatedVat
150
152
  * } RunQueueEvent
151
153
  */
152
154