@agoric/swingset-liveslots 0.10.3-other-dev-3eb1a1d.0 → 0.10.3-other-dev-fbe72e7.0.fbe72e7
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 +26 -26
- package/src/boyd-gc.d.ts +12 -0
- package/src/boyd-gc.d.ts.map +1 -0
- package/src/cache.d.ts +71 -0
- package/src/cache.d.ts.map +1 -0
- package/src/capdata.d.ts +16 -0
- package/src/capdata.d.ts.map +1 -0
- package/src/capdata.js +16 -8
- package/src/collectionManager.d.ts +47 -0
- package/src/collectionManager.d.ts.map +1 -0
- package/src/collectionManager.js +1 -0
- package/src/facetiousness.d.ts +25 -0
- package/src/facetiousness.d.ts.map +1 -0
- package/src/index.d.ts +4 -0
- package/src/index.d.ts.map +1 -0
- package/src/kdebug.d.ts +7 -0
- package/src/kdebug.d.ts.map +1 -0
- package/src/liveslots.d.ts +42 -0
- package/src/liveslots.d.ts.map +1 -0
- package/src/liveslots.js +6 -4
- package/src/message.d.ts +49 -0
- package/src/message.d.ts.map +1 -0
- package/src/message.js +7 -3
- package/src/parseVatSlots.d.ts +125 -0
- package/src/parseVatSlots.d.ts.map +1 -0
- package/src/types.d.ts +81 -0
- package/src/types.d.ts.map +1 -0
- package/src/types.js +6 -5
- package/src/vatDataTypes.d.ts +170 -0
- package/src/vatDataTypes.d.ts.map +1 -0
- package/src/vatDataTypes.ts +272 -0
- package/src/vatstore-iterators.d.ts +4 -0
- package/src/vatstore-iterators.d.ts.map +1 -0
- package/src/vatstore-usage.md +198 -0
- package/src/virtualObjectManager.d.ts +44 -0
- package/src/virtualObjectManager.d.ts.map +1 -0
- package/src/virtualObjectManager.js +70 -14
- package/src/virtualReferences.d.ts +61 -0
- package/src/virtualReferences.d.ts.map +1 -0
- package/src/vpid-tracking.md +92 -0
- package/src/watchedPromises.d.ts +31 -0
- package/src/watchedPromises.d.ts.map +1 -0
- package/src/watchedPromises.js +19 -12
- package/test/collections.test.js +25 -4
- package/test/dummyMeterControl.d.ts +2 -0
- package/test/dummyMeterControl.d.ts.map +1 -0
- package/test/engine-gc.d.ts +3 -0
- package/test/engine-gc.d.ts.map +1 -0
- package/test/gc-and-finalize.d.ts +5 -0
- package/test/gc-and-finalize.d.ts.map +1 -0
- package/test/gc-helpers.js +2 -2
- package/test/handled-promises.test.js +529 -163
- package/test/initial-vrefs.test.js +12 -18
- package/test/liveslots-helpers.d.ts +64 -0
- package/test/liveslots-helpers.d.ts.map +1 -0
- package/test/liveslots-helpers.js +1 -0
- package/test/liveslots-real-gc.test.js +11 -9
- package/test/liveslots.test.js +3 -3
- package/test/storeGC/lifecycle.test.js +13 -12
- package/test/util.d.ts +25 -0
- package/test/util.d.ts.map +1 -0
- package/test/util.js +2 -2
- package/test/vat-util.d.ts +9 -0
- package/test/vat-util.d.ts.map +1 -0
- package/test/virtual-objects/state-shape.test.js +312 -221
- package/test/virtual-objects/virtualObjectGC.test.js +37 -36
- package/test/virtual-objects/virtualObjectManager.test.js +41 -63
- package/test/vo-test-harness.test.js +13 -9
- package/test/waitUntilQuiescent.d.ts +3 -0
- package/test/waitUntilQuiescent.d.ts.map +1 -0
- package/tools/fakeCollectionManager.d.ts +14 -0
- package/tools/fakeCollectionManager.d.ts.map +1 -0
- package/tools/fakeVirtualObjectManager.d.ts +32 -0
- package/tools/fakeVirtualObjectManager.d.ts.map +1 -0
- package/tools/fakeVirtualSupport.d.ts +278 -0
- package/tools/fakeVirtualSupport.d.ts.map +1 -0
- package/tools/prepare-strict-test-env.d.ts +37 -0
- package/tools/prepare-strict-test-env.d.ts.map +1 -0
- package/tools/prepare-test-env.d.ts +2 -0
- package/tools/prepare-test-env.d.ts.map +1 -0
- package/tools/setup-vat-data.d.ts +9 -0
- package/tools/setup-vat-data.d.ts.map +1 -0
- package/tools/setup-vat-data.js +0 -1
- package/tools/vo-test-harness.d.ts +33 -0
- package/tools/vo-test-harness.d.ts.map +1 -0
- package/tools/vo-test-harness.js +21 -0
|
@@ -7,6 +7,7 @@ import { assertPattern, mustMatch } from '@agoric/store';
|
|
|
7
7
|
import { defendPrototype, defendPrototypeKit } from '@endo/exo/tools.js';
|
|
8
8
|
import { Far, passStyleOf } from '@endo/marshal';
|
|
9
9
|
import { Nat } from '@endo/nat';
|
|
10
|
+
import { kindOf } from '@endo/patterns';
|
|
10
11
|
import { parseVatSlot, makeBaseRef } from './parseVatSlots.js';
|
|
11
12
|
import { enumerateKeysWithPrefix } from './vatstore-iterators.js';
|
|
12
13
|
import { makeCache } from './cache.js';
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
* @import {DefineKindOptions} from '@agoric/swingset-liveslots'
|
|
21
22
|
* @import {ClassContextProvider, KitContextProvider} from '@endo/exo'
|
|
22
23
|
* @import {ToCapData, FromCapData} from '@endo/marshal';
|
|
24
|
+
* @import {Pattern} from '@endo/patterns';
|
|
25
|
+
* @import {SwingSetCapData} from './types.js';
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
28
|
const {
|
|
@@ -241,12 +244,47 @@ const insistDurableCapdata = (vrm, what, capdata, valueFor) => {
|
|
|
241
244
|
}
|
|
242
245
|
};
|
|
243
246
|
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
const isUndefinedPatt = patt =>
|
|
248
|
+
patt === undefined ||
|
|
249
|
+
(kindOf(patt) === 'match:kind' && patt.payload === 'undefined');
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Assert that a new stateShape either matches the old, or only differs in the
|
|
253
|
+
* addition of new clearly-optional top-level fields as conveyed by an
|
|
254
|
+
* [Endo `M.or`]{@link https://endojs.github.io/endo/interfaces/_endo_patterns.PatternMatchers.html#or}
|
|
255
|
+
* Pattern with at least one `undefined` alternative.
|
|
256
|
+
*
|
|
257
|
+
* @param {SwingSetCapData} oldCD
|
|
258
|
+
* @param {SwingSetCapData} newCD
|
|
259
|
+
* @param {{ newShape: Record<string, Pattern>, serialize: ToCapData<string>, unserialize: FromCapData<string> }} powers
|
|
260
|
+
*/
|
|
261
|
+
const insistCompatibleShapeCapData = (
|
|
262
|
+
oldCD,
|
|
263
|
+
newCD,
|
|
264
|
+
{ newShape, serialize, unserialize },
|
|
265
|
+
) => {
|
|
248
266
|
if (oldCD.body !== newCD.body) {
|
|
249
|
-
|
|
267
|
+
// Allow introduction of any clearly-optional new field at top level.
|
|
268
|
+
const oldShape =
|
|
269
|
+
unserialize(oldCD) ||
|
|
270
|
+
Fail`durable Kind stateShape mismatch (no old shape)`;
|
|
271
|
+
passStyleOf(oldShape) === 'copyRecord' ||
|
|
272
|
+
Fail`durable Kind stateShape mismatch (invalid old shape)`;
|
|
273
|
+
assertPattern(oldShape);
|
|
274
|
+
for (const [name, oldPatt] of Object.entries(oldShape)) {
|
|
275
|
+
// Assert presence and CapData shape, but save slots for their own clause.
|
|
276
|
+
(Object.hasOwn(newShape, name) &&
|
|
277
|
+
serialize(newShape[name]).body === serialize(oldPatt).body) ||
|
|
278
|
+
Fail`durable Kind stateShape mismatch (body ${name})`;
|
|
279
|
+
}
|
|
280
|
+
for (const [name, newPatt] of Object.entries(newShape)) {
|
|
281
|
+
if (Object.hasOwn(oldShape, name)) continue;
|
|
282
|
+
kindOf(newPatt) === 'match:or' ||
|
|
283
|
+
Fail`durable Kind stateShape mismatch (body new field ${name})`;
|
|
284
|
+
// @ts-expect-error A "match:or" pattern has a `payload` array of Patterns
|
|
285
|
+
newPatt.payload.some(patt => isUndefinedPatt(patt)) ||
|
|
286
|
+
Fail`durable Kind stateShape mismatch (body ${name})`;
|
|
287
|
+
}
|
|
250
288
|
}
|
|
251
289
|
if (oldCD.slots.length !== newCD.slots.length) {
|
|
252
290
|
Fail`durable Kind stateShape mismatch (slots.length)`;
|
|
@@ -532,7 +570,7 @@ export const makeVirtualObjectManager = (
|
|
|
532
570
|
* tag: string,
|
|
533
571
|
* unfaceted?: boolean,
|
|
534
572
|
* facets?: string[],
|
|
535
|
-
* stateShapeCapData?:
|
|
573
|
+
* stateShapeCapData?: SwingSetCapData
|
|
536
574
|
* }} DurableKindDescriptor
|
|
537
575
|
*/
|
|
538
576
|
|
|
@@ -803,7 +841,11 @@ export const makeVirtualObjectManager = (
|
|
|
803
841
|
|
|
804
842
|
const oldStateShapeSlots = oldShapeCD ? oldShapeCD.slots : [];
|
|
805
843
|
if (oldShapeCD && !allowStateShapeChanges) {
|
|
806
|
-
|
|
844
|
+
insistCompatibleShapeCapData(oldShapeCD, newShapeCD, {
|
|
845
|
+
newShape: /** @type {Record<string, Pattern>} */ (stateShape),
|
|
846
|
+
serialize,
|
|
847
|
+
unserialize,
|
|
848
|
+
});
|
|
807
849
|
}
|
|
808
850
|
const newStateShapeSlots = newShapeCD.slots;
|
|
809
851
|
vrm.updateReferenceCounts(oldStateShapeSlots, newStateShapeSlots);
|
|
@@ -859,9 +901,11 @@ export const makeVirtualObjectManager = (
|
|
|
859
901
|
const baseRef = getBaseRef(this);
|
|
860
902
|
const record = dataCache.get(baseRef);
|
|
861
903
|
assert(record !== undefined);
|
|
862
|
-
const {
|
|
904
|
+
const { capdatas, valueMap } = record;
|
|
863
905
|
if (!valueMap.has(prop)) {
|
|
864
|
-
const value =
|
|
906
|
+
const value = hasOwn(capdatas, prop)
|
|
907
|
+
? harden(unserialize(capdatas[prop]))
|
|
908
|
+
: undefined;
|
|
865
909
|
checkStatePropertyValue(value, prop);
|
|
866
910
|
valueMap.set(prop, value);
|
|
867
911
|
}
|
|
@@ -877,12 +921,19 @@ export const makeVirtualObjectManager = (
|
|
|
877
921
|
}
|
|
878
922
|
const record = dataCache.get(baseRef); // mutable
|
|
879
923
|
assert(record !== undefined);
|
|
880
|
-
const
|
|
924
|
+
const { capdatas, valueMap } = record;
|
|
925
|
+
const oldSlots = hasOwn(capdatas, prop) ? capdatas[prop].slots : [];
|
|
881
926
|
const newSlots = capdata.slots;
|
|
882
927
|
vrm.updateReferenceCounts(oldSlots, newSlots);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
928
|
+
// modify in place, but mark as dirty
|
|
929
|
+
defineProperty(capdatas, prop, {
|
|
930
|
+
value: capdata,
|
|
931
|
+
writable: true,
|
|
932
|
+
enumerable: true,
|
|
933
|
+
configurable: false,
|
|
934
|
+
});
|
|
935
|
+
valueMap.set(prop, value);
|
|
936
|
+
dataCache.set(baseRef, record);
|
|
886
937
|
},
|
|
887
938
|
enumerable: true,
|
|
888
939
|
configurable: false,
|
|
@@ -1073,7 +1124,12 @@ export const makeVirtualObjectManager = (
|
|
|
1073
1124
|
}
|
|
1074
1125
|
// eslint-disable-next-line github/array-foreach
|
|
1075
1126
|
valueCD.slots.forEach(vrm.addReachableVref);
|
|
1076
|
-
capdatas
|
|
1127
|
+
defineProperty(capdatas, prop, {
|
|
1128
|
+
value: valueCD,
|
|
1129
|
+
writable: true,
|
|
1130
|
+
enumerable: true,
|
|
1131
|
+
configurable: false,
|
|
1132
|
+
});
|
|
1077
1133
|
valueMap.set(prop, value);
|
|
1078
1134
|
}
|
|
1079
1135
|
// dataCache contents remain mutable: state setter modifies in-place
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {*} syscall Vat's syscall object, used to access the vatstore operations.
|
|
3
|
+
* @param {(val: object) => string | undefined} getSlotForVal A function that returns the
|
|
4
|
+
* object ID (vref) for a given object, if any. their corresponding export
|
|
5
|
+
* IDs
|
|
6
|
+
* @param {(slot: string) => object} requiredValForSlot A function that
|
|
7
|
+
* converts an object ID (vref) to an object.
|
|
8
|
+
* @param {*} FinalizationRegistry Powerful JavaScript intrinsic normally denied
|
|
9
|
+
* by SES
|
|
10
|
+
* @param {*} WeakRef Powerful JavaScript intrinsic normally denied
|
|
11
|
+
* by SES
|
|
12
|
+
* @param {*} addToPossiblyDeadSet Function to record objects whose deaths
|
|
13
|
+
* should be reinvestigated
|
|
14
|
+
* @param {*} addToPossiblyRetiredSet Function to record dead objects whose
|
|
15
|
+
* retirement should be reinvestigated
|
|
16
|
+
* @param {boolean} relaxDurabilityRules True IFF the associated swingset is
|
|
17
|
+
* running with relaxed durability rules
|
|
18
|
+
*/
|
|
19
|
+
export function makeVirtualReferenceManager(syscall: any, getSlotForVal: (val: object) => string | undefined, requiredValForSlot: (slot: string) => object, FinalizationRegistry: any, WeakRef: any, addToPossiblyDeadSet: any, addToPossiblyRetiredSet: any, relaxDurabilityRules: boolean): {
|
|
20
|
+
registerDroppedCollection: (target: any, descriptor: any) => void;
|
|
21
|
+
isDurable: (vref: string) => boolean;
|
|
22
|
+
isDurableKind: (kindID: string) => boolean;
|
|
23
|
+
registerKind: (kindID: string, reanimator?: (string: any, boolean: any) => object, deleter?: (string: any) => boolean, durable?: boolean) => void;
|
|
24
|
+
rememberFacetNames: (kindID: string, facetNames: string[] | null) => void;
|
|
25
|
+
getFacet: (kindID: any, facets: any, facetIndex: any) => any;
|
|
26
|
+
getFacetNames: (kindID: any) => any;
|
|
27
|
+
reanimate: (baseRef: string) => any;
|
|
28
|
+
addReachableVref: (vref: any) => void;
|
|
29
|
+
removeReachableVref: (vref: any) => boolean;
|
|
30
|
+
updateReferenceCounts: (beforeSlots: any, afterSlots: any) => void;
|
|
31
|
+
getReachablePromiseRefCount: (p: any) => number;
|
|
32
|
+
addRecognizableValue: (value: any, recognizer: string | (Map<string, any> | Set<string>), recognizerIsVirtual?: boolean) => void;
|
|
33
|
+
removeRecognizableVref: (vref: string, recognizer: string | (Map<string, any> | Set<string>), recognizerIsVirtual?: boolean) => void;
|
|
34
|
+
removeRecognizableValue: (value: any, recognizer: string | (Map<string, any> | Set<string>), recognizerIsVirtual?: boolean) => void;
|
|
35
|
+
vrefKey: (value: any) => string | undefined;
|
|
36
|
+
isPresenceReachable: (vref: string) => boolean;
|
|
37
|
+
isVrefRecognizable: (vref: any) => boolean;
|
|
38
|
+
setExportStatus: (vref: any, exportStatus: any) => void;
|
|
39
|
+
isVirtualObjectReachable: (baseRef: string) => boolean;
|
|
40
|
+
deleteVirtualObject: (baseRef: string) => [boolean, string[]];
|
|
41
|
+
ceaseRecognition: (vref: string) => boolean;
|
|
42
|
+
setDeleteCollectionEntry: (fn: any) => void;
|
|
43
|
+
getRetentionStats: () => {
|
|
44
|
+
remotableRefCounts: number;
|
|
45
|
+
vrefRecognizers: number;
|
|
46
|
+
kindInfoTable: number;
|
|
47
|
+
};
|
|
48
|
+
initializeIDCounters: () => void;
|
|
49
|
+
allocateNextID: (name: any) => number;
|
|
50
|
+
flushIDCounters: () => void;
|
|
51
|
+
testHooks: {
|
|
52
|
+
getReachableRefCount: (vref: any) => number | undefined;
|
|
53
|
+
countCollectionsForWeakKey: (vref: any) => number;
|
|
54
|
+
getDroppedCollectionRegistry: () => any;
|
|
55
|
+
remotableRefCounts: Map<any, number>;
|
|
56
|
+
vrefRecognizers: Map<string, Set<Map<string, any> | Set<string>>>;
|
|
57
|
+
kindInfoTable: Map<any, any>;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export type VirtualReferenceManager = ReturnType<typeof makeVirtualReferenceManager>;
|
|
61
|
+
//# sourceMappingURL=virtualReferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtualReferences.d.ts","sourceRoot":"","sources":["virtualReferences.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;GAiBG;AACH,qDAjBW,GAAC,iBACD,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,sBAGnC,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,wBAExB,GAAC,WAED,GAAC,wBAED,GAAC,2BAED,GAAC,wBAED,OAAO;;sBAqQL,MAAM,KAEJ,OAAO;4BAfT,MAAM,KAEJ,OAAO;2BA9CT,MAAM,eACN,CAAC,MAAM,KAAA,EAAE,OAAO,KAAA,KAAK,MAAM,YAC3B,CAAC,MAAM,KAAA,KAAK,OAAO,YACnB,OAAO;iCASP,MAAM,cACN,MAAM,EAAE,GAAC,IAAI;;;yBA8Eb,MAAM;;;;;kCAmPN,GAAC,cACD,MAAM,mCAAW,wBACjB,OAAO;mCA2BP,MAAM,cACN,MAAM,mCAAW,wBACjB,OAAO;qCAgCP,GAAC,cACD,MAAM,mCAAW,wBACjB,OAAO;;gCAvIP,MAAM,KAEJ,OAAO;;;wCAzaT,MAAM,KAEJ,OAAO;mCAoBT,MAAM,KAEJ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;6BAwiBrB,MAAM,KAEJ,OAAO;;;;;;;;;;;;;;;;;;EA+LrB;sCACa,UAAU,CAAC,OAAO,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Promise/VPID Management in Liveslots
|
|
2
|
+
|
|
3
|
+
Kernels and vats communicate about promises by referring to their VPIDs: vat(-centric) promise IDs. These are strings like `p+12` and `p-23`. Like VOIDs (object IDs), the plus/minus sign indicates which side of the boundary allocated the number (`p+12` and `o+12` are allocated by the vat, `p-13` and `o-13` are allocated by the kernel). But where the object ID sign also indicates which side "owns" the object (i.e. where the behavior lives), the promise ID sign is generally irrelevant.
|
|
4
|
+
|
|
5
|
+
Instead, we care about which side holds the resolution authority for a promise (referred to as being its **decider**). This is not indicated by the VPID sign, and in fact is not necessarily static. Liveslots does not currently have any mechanism to allow one promise to be forwarded to another, but if it acquires this some day, then the decider of a promise could shift from kernel to vat to kernel again before it finally gets resolved. And there *are* sequences that allow a vat to receive a reference to a promise in method arguments before receiving a message whose result promise uses that same VPID (e.g., `p1 = p2~.foo(); x~.bar(p1)`, then someone resolves `p2` to `x`. Consider also tildot-free code like `{ promise: p1, resolve: resolveP1 } = makePromiseKit(); p2 = E(p1).push('queued'); await E(observer).push(p2); resolveP1(observer);` with an observer whose `push` returns a prompt response). In such cases, the decider is initially the kernel but that authority is transferred to a vat later.
|
|
6
|
+
|
|
7
|
+
Each Promise starts out in the "unresolved" state, then later transitions irrevocably to the "resolved" state. Liveslots frequently (but not always) pays attention to this transition by calling `then` to attach fulfillment/rejection settlement callbacks. To handle resolution cycles, liveslots remembers the resolution of old promises in a `WeakMap` for as long as the Promise exists. Consequently, for liveslots' purposes, every Promise is either resolved (a callback has fired and liveslots remembers the settlement), or unresolved (liveslots has not yet seen a resolution that settles it).
|
|
8
|
+
|
|
9
|
+
There are roughly four ways that liveslots might become aware of a promise:
|
|
10
|
+
|
|
11
|
+
* serialization: a Promise instance is serialized, either for the arguments of an outbound `syscall.send` or `syscall.resolve`, the argument of `watchPromise()`, or to be stored into virtualized data (e.g. `bigMapStore.set(key, promise)`, or assignment to a property of a virtual object)
|
|
12
|
+
* creation for outbound result: liveslots allocates a VPID for the `result` of an outbound `syscall.send`, and creates a new Promise instance to give back to userspace
|
|
13
|
+
* deserialization: the arguments of an inbound `dispatch.deliver` or `dispatch.notify` are deserialized, and a new Promise instance is created
|
|
14
|
+
* inbound result: the kernel-allocated `result` VPID of an inbound `dispatch.deliver` is associated with the Promise we get back from `HandledPromise.applyMethod`
|
|
15
|
+
|
|
16
|
+
A Promise may be associated with a VPID even though the kernel does not know about it (i.e. the VPID is not in the kernel's c-list for that vat). This can occur when a Promise is stored into virtual data without also being sent to (or received from) the kernel, although note that every Promise associated with a durable promise watcher _is_ sent to the kernel so it can be rejected during vat upgrade. A Promise can also be resolved but still referenced in vdata and forgotten by the kernel (the kernel's knowledge is temporary; it retires VPIDs from c-lists upon `syscall.resolve` or `dispatch.notify` as appropriate). So a VPID might start out stored only in vdata, then get sent to the kernel, then get resolved, leaving it solely in vdata once more.
|
|
17
|
+
|
|
18
|
+
Each unresolved VPID has a decider: either the kernel or a vat. It can remain unresolved for arbitrarily long, but becomes resolved by the first of the following events:
|
|
19
|
+
|
|
20
|
+
* if liveslots learns about local resolution of the corresponding Promise by userspace, then liveslots will perform a `syscall.resolve()` (prompting 'notify' deliveries to other subscribed vats)
|
|
21
|
+
* if liveslots learns about resolution by inbound notify, then liveslots will unregister it as necessary and inform userspace of the resolution
|
|
22
|
+
* if the vat is terminated, the kernel internally rejects all remaining vat-decided KPIDs without involving the vat
|
|
23
|
+
* if the vat is upgraded, each of those terminate-associated rejections is followed by a 'notify' delivery to the new incarnation
|
|
24
|
+
|
|
25
|
+
Liveslots tracks promises in the following data structures:
|
|
26
|
+
|
|
27
|
+
* `slotToVal` / `valToSlot` : these manage *registration*, the mapping from VPID to Promise and vice versa. These also register objects (Presences, Remotables, and Representatives) to/from VOIDs, and device nodes.
|
|
28
|
+
* to support GC of objects, `slotToVal.get(vref)` is a WeakRef, and `valToSlot` is a WeakMap
|
|
29
|
+
* liveslots uses independent strong references to maintain object/promise lifetimes
|
|
30
|
+
* `exportedVPIDs`: a `Map<VPID, Promise>`: all Promises currently known to the kernel and decided by the vat
|
|
31
|
+
* `importedVPIDs`: a `Map<VPID, PromiseKit>`: all Promises currently known to the kernel and decided by the kernel
|
|
32
|
+
* `remotableRefCounts`: a `Map<Object|Promise, Number>`: all Promises (and Remotables) referenced by virtual data
|
|
33
|
+
|
|
34
|
+
The kernel's c-list for a vat contains all VPIDs in `exportedVPIDs` and `importedVPIDs`. The vat is the decider for `exportedVPIDs`, while the kernel is the decider for `importedVPIDs`. For every VPID in `exportedVPIDs`, we've used `then` on the Promise instance to arrange for a `syscall.resolve` when it settles (becomes fulfilled or rejected). For every VPID key of the `importedVPIDs` Map, the corresponding value is a `[resolve, reject]` "**pRec**", so one of the functions can be called during `dispatch.notify`. Every VPID in `slotToVal` is either in `exportedVPIDs` but not `importedVPIDs`, `importedVPIDs` but not `exportedVPIDs`, or neither.
|
|
35
|
+
|
|
36
|
+
If a VPID in `importedVPIDs` is resolved (by the kernel, via `dispatch.notify`), the VPID is removed from `importedVPIDs`. If a VPID in `exportedVPIDs` is resolved (by the vat, i.e. liveslots observes invocation of a previously-added settlement callback), liveslots invokes `syscall.resolve` and removes the VPID from `exportedVPIDs`. The c-list for a vat will not contain a VPID for any resolved promise.
|
|
37
|
+
|
|
38
|
+
The `slotToVal`/`valToSlot` registration must remain until all of the following are true:
|
|
39
|
+
|
|
40
|
+
* the kernel is no longer aware of the VPID
|
|
41
|
+
* the Promise is not present in any virtual data
|
|
42
|
+
* the promise is not being watched by a `promiseWatcher`.
|
|
43
|
+
|
|
44
|
+
If the registration were to be lost while any of the above conditions were still true, a replacement Promise might be created while the original was still around, causing confusion.
|
|
45
|
+
|
|
46
|
+
## Maintaining Strong References
|
|
47
|
+
|
|
48
|
+
Remember that the `slotToVal` registration uses a WeakRef, so being registered there does not keep the Promise object alive.
|
|
49
|
+
|
|
50
|
+
`exportedVPIDs` and `importedVPIDs` keep their Promise alive in their value. vdata keeps it alive through the key of `remotableRefCounts`. `promiseWatcher` uses an internal `ScalarBigMapStore` to keep the Promise alive.
|
|
51
|
+
|
|
52
|
+
## Promise/VPID Management Algorithm
|
|
53
|
+
|
|
54
|
+
* When a Promise is first serialized (it appears in `convertValToSlot`), a VPID is assigned and the VPID/Promise mapping is registered in `valToSlot`/`slotToVal`
|
|
55
|
+
* at this point, there is not yet a strong reference to the Promise
|
|
56
|
+
* When a VPID appears in the serialized arguments of `syscall.send` or `syscall.resolve`:
|
|
57
|
+
* if the VPID already exists in `exportedVPIDs` or `importedVPIDs`: do nothing
|
|
58
|
+
* else: use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve()`
|
|
59
|
+
* When a `followForKernel` settlement callback is executed:
|
|
60
|
+
* do `syscall.resolve()`
|
|
61
|
+
* remove from `exportedVPIDs`
|
|
62
|
+
* if `remotableRefCounts` reports 0 references: unregister from `valToSlot`/`slotToVal`
|
|
63
|
+
* When the kernel delivers a `dispatch.notify`:
|
|
64
|
+
* retrieve the `[resolve, reject]` pRec from `importedVPIDs`
|
|
65
|
+
* invoke the appropriate function with the deserialized argument
|
|
66
|
+
* if `remotableRefCounts` reports 0 references: unregister from `valToSlot`/`slotToVal`
|
|
67
|
+
* When the vdata refcount for a VPID drops to zero:
|
|
68
|
+
* if the VPID still exists in `exportedVPIDs` or `importedVPIDs`: do nothing
|
|
69
|
+
* else: unregister from `valToSlot`/`slotToVal`
|
|
70
|
+
* When a new VPID is deserialized (it appears in `convertSlotToVal`), this must be the arguments of a delivery (not vdata)
|
|
71
|
+
* use `makePipelinablePromise` to create a HandledPromise for the VPID
|
|
72
|
+
* add the Promise and its `resolve`/`reject` pair to `importedVPIDs`
|
|
73
|
+
* register the Promise in `valToSlot`/`slotToVal`
|
|
74
|
+
* use `syscall.subscribe` to request a `dispatch.notify` delivery when the kernel resolves this promise
|
|
75
|
+
* When a VPID appears as the `result` of an outbound `syscall.send`: (_note overlap with the preceding_)
|
|
76
|
+
* use `allocateVPID` to allocate a new VPID
|
|
77
|
+
* use `makePipelinablePromise` to create a HandledPromise for the VPID
|
|
78
|
+
* add the Promise and its `resolve`/`reject` pair to `importedVPIDs`
|
|
79
|
+
* register the Promise in `valToSlot`/`slotToVal`
|
|
80
|
+
* use `syscall.subscribe` to request a `dispatch.notify` delivery when the kernel resolves this promise
|
|
81
|
+
* When a VPID appears as the `result` of an inbound `dispatch.deliver`, the vat is responsible for deciding it:
|
|
82
|
+
* construct a promise `res` to capture the userspace-provided result
|
|
83
|
+
* if the VPID is present in `importedVPIDs`: retrieve the `[resolve, reject]` pRec and use `resolve(res)` to forward eventual settlement of `res` to settlement of the previously-imported promise, then remove the VPID from `importedVPIDs`
|
|
84
|
+
* else: register marshaller association between the VPID and `res`
|
|
85
|
+
* in either case, use `followForKernel` to add the VPID to `exportedVPIDs` and attach `.then(onFulfill, onReject)` callbacks that will map fulfillment/rejection to `syscall.resolve()`
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
If the serialization is for storage in virtual data, the act of storing the VPID will add the Promise to `remotableRefCounts`, which maintains a strong reference for as long as the VPID is held. When it is removed from virtual data (or the object/collection is deleted), the refcount will be decremented. When the refcount drops to zero, we perform the `exportedVPIDs`/`importedVPIDs` check and then maybe unregister the promise.
|
|
89
|
+
|
|
90
|
+
If the serialization is for the arguments of an outbound `syscall.send` or `syscall.resolve` (or `syscall.callNow`, or `syscall.exit`), the VPID will be added to `exportedVPIDs`.
|
|
91
|
+
|
|
92
|
+
If the *un*serialization occurred when processing the arguments of an *in*bound `dispatch.deliver` or `dispatch.notify`, the VPID (and the "promise kit" trio of Promise, `resolve`, and `reject`) will be stored in `importedVPIDs`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template V
|
|
3
|
+
* @template {any[]} [A=unknown[]]
|
|
4
|
+
* @typedef {[watcher: import('./types.js').PromiseWatcher<V, A>, ...args: A]} PromiseWatcherTuple
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* @param {object} options
|
|
8
|
+
* @param {*} options.syscall
|
|
9
|
+
* @param {import('./virtualReferences.js').VirtualReferenceManager} options.vrm
|
|
10
|
+
* @param {import('./virtualObjectManager.js').VirtualObjectManager} options.vom
|
|
11
|
+
* @param {*} options.collectionManager
|
|
12
|
+
* @param {import('@endo/marshal').ConvertValToSlot<any>} options.convertValToSlot
|
|
13
|
+
* @param {import('@endo/marshal').ConvertSlotToVal<any>} options.convertSlotToVal
|
|
14
|
+
* @param {(vref: any) => boolean} options.maybeExportPromise
|
|
15
|
+
*/
|
|
16
|
+
export function makeWatchedPromiseManager({ syscall, vrm, vom, collectionManager, convertValToSlot, convertSlotToVal, maybeExportPromise, }: {
|
|
17
|
+
syscall: any;
|
|
18
|
+
vrm: import("./virtualReferences.js").VirtualReferenceManager;
|
|
19
|
+
vom: import("./virtualObjectManager.js").VirtualObjectManager;
|
|
20
|
+
collectionManager: any;
|
|
21
|
+
convertValToSlot: import("@endo/marshal").ConvertValToSlot<any>;
|
|
22
|
+
convertSlotToVal: import("@endo/marshal").ConvertSlotToVal<any>;
|
|
23
|
+
maybeExportPromise: (vref: any) => boolean;
|
|
24
|
+
}): {
|
|
25
|
+
preparePromiseWatcherTables: () => void;
|
|
26
|
+
loadWatchedPromiseTable: (revivePromise: (vref: any) => Promise<any>) => void;
|
|
27
|
+
providePromiseWatcher: <V, A extends any[]>(kindHandle: import("./vatDataTypes.js").DurableKindHandle, fulfillHandler?: (value: V, ...args: A) => void, rejectHandler?: (reason: any, ...args: A) => void) => import("./types.js").PromiseWatcher<V, A>;
|
|
28
|
+
watchPromise: <P extends Promise<any>, A extends any[]>(p: P, watcher: import("./types.js").PromiseWatcher<Awaited<P>, A>, ...args: A) => void;
|
|
29
|
+
};
|
|
30
|
+
export type PromiseWatcherTuple<V, A extends any[] = unknown[]> = [watcher: import("./types.js").PromiseWatcher<V, A>, ...args: A];
|
|
31
|
+
//# sourceMappingURL=watchedPromises.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchedPromises.d.ts","sourceRoot":"","sources":["watchedPromises.js"],"names":[],"mappings":"AASA;;;;GAIG;AAEH;;;;;;;;;GASG;AACH,6IARG;IAAmB,OAAO,EAAlB,GAAC;IACiE,GAAG,EAArE,OAAO,wBAAwB,EAAE,uBAAuB;IACU,GAAG,EAArE,OAAO,2BAA2B,EAAE,oBAAoB;IAC7C,iBAAiB,EAA5B,GAAC;IACsD,gBAAgB,EAAvE,OAAO,eAAe,EAAE,gBAAgB,CAAC,GAAG,CAAC;IACU,gBAAgB,EAAvE,OAAO,eAAe,EAAE,gBAAgB,CAAC,GAAG,CAAC;IACb,kBAAkB,EAAlD,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO;CAChC;;6CA+GY,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,KACzB,IAAI;4BAiBJ,CAAC,EACO,CAAC,SAAR,GAAG,EAAG,cACT,OAAO,mBAAmB,EAAE,iBAAiB,mBAC7C,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,kBAC9B,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,KAC/B,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;mBAmC3C,CAAC,SAAS,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,GAAG,EAAE,KAAK,CAAC,WAAW,OAAO,YAAY,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,KAAK,IAAI;EAoE3I;gCA3PY,CAAC,EACQ,CAAC,SAAT,GAAG,EAAG,gBACP,CAAC,OAAO,EAAE,OAAO,YAAY,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC"}
|
package/src/watchedPromises.js
CHANGED
|
@@ -36,13 +36,12 @@ export function makeWatchedPromiseManager({
|
|
|
36
36
|
const { defineDurableKind } = vom;
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* this, slotToVal would forget local Promises that aren't exported.
|
|
39
|
+
* Track promises watched in `buildRootObject` so `loadWatchedPromiseTable`
|
|
40
|
+
* can differentiate them from promises watched in a previous incarnation.
|
|
42
41
|
*
|
|
43
|
-
* @type {
|
|
42
|
+
* @type {Set<string> | null}
|
|
44
43
|
*/
|
|
45
|
-
let
|
|
44
|
+
let buildRootObjectWatchedPromiseRefs;
|
|
46
45
|
|
|
47
46
|
/**
|
|
48
47
|
* watched promises by vpid: each entry is an array of watches on the
|
|
@@ -60,7 +59,7 @@ export function makeWatchedPromiseManager({
|
|
|
60
59
|
let promiseWatcherByKindTable;
|
|
61
60
|
|
|
62
61
|
function preparePromiseWatcherTables() {
|
|
63
|
-
|
|
62
|
+
buildRootObjectWatchedPromiseRefs = new Set();
|
|
64
63
|
let watcherTableID = syscall.vatstoreGet('watcherTableID');
|
|
65
64
|
if (watcherTableID) {
|
|
66
65
|
promiseWatcherByKindTable = convertSlotToVal(watcherTableID);
|
|
@@ -105,7 +104,7 @@ export function makeWatchedPromiseManager({
|
|
|
105
104
|
function settle(value, wasFulfilled) {
|
|
106
105
|
const watches = watchedPromiseTable.get(vpid);
|
|
107
106
|
watchedPromiseTable.delete(vpid);
|
|
108
|
-
|
|
107
|
+
buildRootObjectWatchedPromiseRefs?.delete(vpid);
|
|
109
108
|
for (const watch of watches) {
|
|
110
109
|
const [watcher, ...args] = watch;
|
|
111
110
|
void Promise.resolve().then(() => {
|
|
@@ -139,16 +138,16 @@ export function makeWatchedPromiseManager({
|
|
|
139
138
|
*/
|
|
140
139
|
function loadWatchedPromiseTable(revivePromise) {
|
|
141
140
|
for (const vpid of watchedPromiseTable.keys()) {
|
|
142
|
-
if (
|
|
141
|
+
if (buildRootObjectWatchedPromiseRefs?.has(vpid)) {
|
|
143
142
|
// We're only interested in reconnecting the promises from the previous
|
|
144
143
|
// incarnation. Any promise watched during buildRootObject would have
|
|
145
144
|
// already created a registration.
|
|
146
145
|
continue;
|
|
147
146
|
}
|
|
148
147
|
const p = revivePromise(vpid);
|
|
149
|
-
promiseRegistrations.init(vpid, p);
|
|
150
148
|
pseudoThen(p, vpid);
|
|
151
149
|
}
|
|
150
|
+
buildRootObjectWatchedPromiseRefs = null;
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
/**
|
|
@@ -235,13 +234,21 @@ export function makeWatchedPromiseManager({
|
|
|
235
234
|
} else {
|
|
236
235
|
watchedPromiseTable.init(vpid, harden([[watcher, ...args]]));
|
|
237
236
|
|
|
237
|
+
buildRootObjectWatchedPromiseRefs?.add(vpid);
|
|
238
|
+
|
|
239
|
+
// To avoid triggering
|
|
240
|
+
// https://github.com/Agoric/agoric-sdk/issues/10757 and
|
|
241
|
+
// preventing slotToVal cleanup, the `pseudoThen()` should
|
|
242
|
+
// precede `maybeExportPromise()`. This isn't foolproof, but
|
|
243
|
+
// does mitigate in advance of a proper fix. See #10756 for
|
|
244
|
+
// details of this particular mitigation, and #10757 for the
|
|
245
|
+
// deeper bug.
|
|
246
|
+
pseudoThen(p, vpid);
|
|
247
|
+
|
|
238
248
|
// Ensure that this vat's promises are rejected at termination.
|
|
239
249
|
if (maybeExportPromise(vpid)) {
|
|
240
250
|
syscall.subscribe(vpid);
|
|
241
251
|
}
|
|
242
|
-
|
|
243
|
-
promiseRegistrations.init(vpid, p);
|
|
244
|
-
pseudoThen(p, vpid);
|
|
245
252
|
}
|
|
246
253
|
});
|
|
247
254
|
}
|
package/test/collections.test.js
CHANGED
|
@@ -326,7 +326,7 @@ test('constrain map key shape', t => {
|
|
|
326
326
|
t.throws(
|
|
327
327
|
() => noStrings.init('foo', 'string not ok?'),
|
|
328
328
|
m(
|
|
329
|
-
|
|
329
|
+
/^invalid key type for collection "map key no strings": "foo" - Must fail negated pattern: (.*)$/,
|
|
330
330
|
),
|
|
331
331
|
);
|
|
332
332
|
t.is(noStrings.get(47), 'number ok');
|
|
@@ -335,7 +335,14 @@ test('constrain map key shape', t => {
|
|
|
335
335
|
t.throws(
|
|
336
336
|
() => noStrings.get('foo'),
|
|
337
337
|
m(
|
|
338
|
-
|
|
338
|
+
// TODO https://github.com/Agoric/agoric-sdk/issues/11605
|
|
339
|
+
// This is a golden error message test. We're currently in transition
|
|
340
|
+
// from having pattern error messages quote nested patterns using
|
|
341
|
+
// `q` to quoting them using `qp`. In order to tolerate both during
|
|
342
|
+
// this transition at reasonable cost, this golden error message
|
|
343
|
+
// pattern accepts anything in the position of the quoted nested
|
|
344
|
+
// pattern.
|
|
345
|
+
/^invalid key type for collection "map key no strings": "foo" - Must fail negated pattern: (.*)$/,
|
|
339
346
|
),
|
|
340
347
|
);
|
|
341
348
|
|
|
@@ -397,7 +404,14 @@ test('constrain map value shape', t => {
|
|
|
397
404
|
t.throws(
|
|
398
405
|
() => noStrings.init('skey', 'string not ok?'),
|
|
399
406
|
m(
|
|
400
|
-
|
|
407
|
+
// TODO https://github.com/Agoric/agoric-sdk/issues/11605
|
|
408
|
+
// This is a golden error message test. We're currently in transition
|
|
409
|
+
// from having pattern error messages quote nested patterns using
|
|
410
|
+
// `q` to quoting them using `qp`. In order to tolerate both during
|
|
411
|
+
// this transition at reasonable cost, this golden error message
|
|
412
|
+
// pattern accepts anything in the position of the quoted nested
|
|
413
|
+
// pattern.
|
|
414
|
+
/^invalid value type for collection "map value no strings": "string not ok\?" - Must fail negated pattern: (.*)$/,
|
|
401
415
|
),
|
|
402
416
|
);
|
|
403
417
|
t.is(noStrings.get('nkey'), 47);
|
|
@@ -458,7 +472,14 @@ test('constrain set key shape', t => {
|
|
|
458
472
|
t.throws(
|
|
459
473
|
() => noStrings.add('foo?'),
|
|
460
474
|
m(
|
|
461
|
-
|
|
475
|
+
// TODO https://github.com/Agoric/agoric-sdk/issues/11605
|
|
476
|
+
// This is a golden error message test. We're currently in transition
|
|
477
|
+
// from having pattern error messages quote nested patterns using
|
|
478
|
+
// `q` to quoting them using `qp`. In order to tolerate both during
|
|
479
|
+
// this transition at reasonable cost, this golden error message
|
|
480
|
+
// pattern accepts anything in the position of the quoted nested
|
|
481
|
+
// pattern.
|
|
482
|
+
/^invalid key type for collection "no strings set": "foo\?" - Must fail negated pattern: (.*)$/,
|
|
462
483
|
),
|
|
463
484
|
);
|
|
464
485
|
t.truthy(noStrings.has(47));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dummyMeterControl.d.ts","sourceRoot":"","sources":["dummyMeterControl.js"],"names":[],"mappings":"AAEA,gFAmDC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine-gc.d.ts","sourceRoot":"","sources":["engine-gc.js"],"names":[],"mappings":";AAoBA,mCAAwB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gc-and-finalize.d.ts","sourceRoot":"","sources":["gc-and-finalize.js"],"names":[],"mappings":"AAkEA,qEAwBC;AAwBD;YAbsD,OAAO;EAkB5D"}
|
package/test/gc-helpers.js
CHANGED
|
@@ -9,8 +9,8 @@ import { parseVatSlot } from '../src/parseVatSlots.js';
|
|
|
9
9
|
let aWeakMapStore;
|
|
10
10
|
let aWeakSetStore;
|
|
11
11
|
|
|
12
|
-
export const mainHolderIdx =
|
|
13
|
-
export const mainHeldIdx =
|
|
12
|
+
export const mainHolderIdx = 4;
|
|
13
|
+
export const mainHeldIdx = 5;
|
|
14
14
|
|
|
15
15
|
export function buildRootObject(vatPowers) {
|
|
16
16
|
const { VatData } = vatPowers;
|