@agoric/swingset-liveslots 0.10.3-dev-ee2126b.0 → 0.10.3-dev-c7eca9b.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 +5 -5
- package/src/liveslots.js +56 -28
- package/src/watchedPromises.js +6 -0
- package/test/handled-promises.test.js +265 -49
- package/test/liveslots-helpers.js +6 -1
- package/test/vpid-liveslots.test.js +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/swingset-liveslots",
|
|
3
|
-
"version": "0.10.3-dev-
|
|
3
|
+
"version": "0.10.3-dev-c7eca9b.0+c7eca9b",
|
|
4
4
|
"description": "SwingSet ocap support layer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"lint:eslint": "eslint ."
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@agoric/internal": "0.3.3-dev-
|
|
21
|
-
"@agoric/store": "0.9.3-dev-
|
|
20
|
+
"@agoric/internal": "0.3.3-dev-c7eca9b.0+c7eca9b",
|
|
21
|
+
"@agoric/store": "0.9.3-dev-c7eca9b.0+c7eca9b",
|
|
22
22
|
"@endo/env-options": "^1.1.4",
|
|
23
23
|
"@endo/errors": "^1.2.2",
|
|
24
24
|
"@endo/eventual-send": "^1.2.2",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@endo/promise-kit": "^1.1.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@agoric/kmarshal": "0.1.1-dev-
|
|
35
|
+
"@agoric/kmarshal": "0.1.1-dev-c7eca9b.0+c7eca9b",
|
|
36
36
|
"ava": "^5.3.0"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
@@ -68,5 +68,5 @@
|
|
|
68
68
|
"typeCoverage": {
|
|
69
69
|
"atLeast": 75.35
|
|
70
70
|
},
|
|
71
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "c7eca9bba1bb4ca361c716570ef2ee4bc496beb0"
|
|
72
72
|
}
|
package/src/liveslots.js
CHANGED
|
@@ -752,32 +752,43 @@ function build(
|
|
|
752
752
|
if (facet !== undefined) {
|
|
753
753
|
result = vrm.getFacet(id, val, facet);
|
|
754
754
|
}
|
|
755
|
-
} else {
|
|
755
|
+
} else if (type === 'object') {
|
|
756
|
+
// Note: an abandonned (e.g. by an upgrade) exported ephemeral or virtual
|
|
757
|
+
// object would appear as an import if re-introduced. In the future we
|
|
758
|
+
// may need to change that if we want to keep recognizing such references
|
|
759
|
+
// In that case we'd need to create an imported presence for these
|
|
760
|
+
// unknown vrefs allocated by the vat.
|
|
761
|
+
// See https://github.com/Agoric/agoric-sdk/issues/9746
|
|
756
762
|
!allocatedByVat || Fail`I don't remember allocating ${slot}`;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
val = makeDeviceNode(slot, iface);
|
|
777
|
-
importedDevices.add(val);
|
|
763
|
+
// this is a new import value
|
|
764
|
+
val = makeImportedPresence(slot, iface);
|
|
765
|
+
} else if (type === 'promise') {
|
|
766
|
+
// We unconditionally create a promise record, even if the promise looks
|
|
767
|
+
// like it was allocated by us. This can happen when re-importing a
|
|
768
|
+
// promise created by the previous incarnation. We may or may not have
|
|
769
|
+
// been the decider of the promise. If we were, the kernel will be
|
|
770
|
+
// rejecting the promise on our behalf. We may have previously been
|
|
771
|
+
// subscribed to that promise, but subscription is idempotent.
|
|
772
|
+
const pRec = makePipelinablePromise(slot);
|
|
773
|
+
importedVPIDs.set(slot, pRec);
|
|
774
|
+
val = pRec.promise;
|
|
775
|
+
// ideally we'd wait until .then is called on p before subscribing,
|
|
776
|
+
// but the current Promise API doesn't give us a way to discover
|
|
777
|
+
// this, so we must subscribe right away. If we were using Vows or
|
|
778
|
+
// some other then-able, we could just hook then() to notify us.
|
|
779
|
+
if (importedPromises) {
|
|
780
|
+
// leave the subscribe() up to dispatch.notify()
|
|
781
|
+
importedPromises.add(slot);
|
|
778
782
|
} else {
|
|
779
|
-
|
|
783
|
+
// probably in dispatch.deliver(), so subscribe now
|
|
784
|
+
syscall.subscribe(slot);
|
|
780
785
|
}
|
|
786
|
+
} else if (type === 'device') {
|
|
787
|
+
!allocatedByVat || Fail`unexpected device ${slot} allocated by vat`;
|
|
788
|
+
val = makeDeviceNode(slot, iface);
|
|
789
|
+
importedDevices.add(val);
|
|
790
|
+
} else {
|
|
791
|
+
Fail`unrecognized slot type '${type}'`;
|
|
781
792
|
}
|
|
782
793
|
registerValue(baseRef, val, facet !== undefined);
|
|
783
794
|
if (!result) {
|
|
@@ -790,7 +801,25 @@ function build(
|
|
|
790
801
|
meterControl.assertNotMetered();
|
|
791
802
|
const { type } = parseVatSlot(slot);
|
|
792
803
|
type === 'promise' || Fail`revivePromise called on non-promise ${slot}`;
|
|
793
|
-
|
|
804
|
+
const val = getValForSlot(slot);
|
|
805
|
+
if (val) {
|
|
806
|
+
// revivePromise is only called by loadWatchedPromiseTable, which runs
|
|
807
|
+
// after buildRootObject(), which is given the deserialized vatParameters.
|
|
808
|
+
// The only way revivePromise() might encounter a pre-existing vpid is if
|
|
809
|
+
// these vatParameters include a promise that the previous incarnation
|
|
810
|
+
// watched, but that `buildRootObject` in the new incarnation didn't
|
|
811
|
+
// explicitly watch again. This can be either a previously imported
|
|
812
|
+
// promise, or a promise the previous incarnation exported, regardless of
|
|
813
|
+
// who the decider now is.
|
|
814
|
+
//
|
|
815
|
+
// In that case convertSlotToVal() has already deserialized the vpid, but
|
|
816
|
+
// since `buildRootObject` didn't explicitely call watchPromise on it, no
|
|
817
|
+
// registration exists so loadWatchedPromiseTable attempts to revive the
|
|
818
|
+
// promise.
|
|
819
|
+
return val;
|
|
820
|
+
}
|
|
821
|
+
// NOTE: it is important that this code not do anything *more*
|
|
822
|
+
// than what convertSlotToVal(vpid) would do
|
|
794
823
|
const pRec = makePipelinablePromise(slot);
|
|
795
824
|
importedVPIDs.set(slot, pRec);
|
|
796
825
|
const p = pRec.promise;
|
|
@@ -1593,12 +1622,10 @@ function build(
|
|
|
1593
1622
|
} else if (delivery[0] === 'stopVat') {
|
|
1594
1623
|
return meterControl.runWithoutMeteringAsync(() => stopVat(delivery[1]));
|
|
1595
1624
|
} else {
|
|
1596
|
-
let complete = false;
|
|
1597
1625
|
// Start user code running, record any internal liveslots errors. We do
|
|
1598
1626
|
// *not* directly wait for the userspace function to complete, nor for
|
|
1599
1627
|
// any promise it returns to fire.
|
|
1600
1628
|
const p = Promise.resolve(delivery).then(unmeteredDispatch);
|
|
1601
|
-
void p.finally(() => (complete = true));
|
|
1602
1629
|
|
|
1603
1630
|
// Instead, we wait for userspace to become idle by draining the
|
|
1604
1631
|
// promise queue. We clean up and then examine/return 'p' so any
|
|
@@ -1609,10 +1636,11 @@ function build(
|
|
|
1609
1636
|
return gcTools.waitUntilQuiescent().then(() => {
|
|
1610
1637
|
afterDispatchActions();
|
|
1611
1638
|
// eslint-disable-next-line prefer-promise-reject-errors
|
|
1612
|
-
return
|
|
1639
|
+
return Promise.race([p, Promise.reject('buildRootObject unresolved')]);
|
|
1613
1640
|
// the only delivery that pays attention to a user-provided
|
|
1614
1641
|
// Promise is startVat, so the error message is specialized to
|
|
1615
|
-
// the only user problem that could cause
|
|
1642
|
+
// the only user problem that could cause the promise to not be
|
|
1643
|
+
// settled.
|
|
1616
1644
|
});
|
|
1617
1645
|
}
|
|
1618
1646
|
}
|
package/src/watchedPromises.js
CHANGED
|
@@ -139,6 +139,12 @@ export function makeWatchedPromiseManager({
|
|
|
139
139
|
*/
|
|
140
140
|
function loadWatchedPromiseTable(revivePromise) {
|
|
141
141
|
for (const vpid of watchedPromiseTable.keys()) {
|
|
142
|
+
if (promiseRegistrations.has(vpid)) {
|
|
143
|
+
// We're only interested in reconnecting the promises from the previous
|
|
144
|
+
// incarnation. Any promise watched during buildRootObject would have
|
|
145
|
+
// already created a registration.
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
142
148
|
const p = revivePromise(vpid);
|
|
143
149
|
promiseRegistrations.init(vpid, p);
|
|
144
150
|
pseudoThen(p, vpid);
|
|
@@ -84,7 +84,7 @@ const makeExoUtils = VatData => {
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
// cf. packages/SwingSet/test/vat-durable-promise-watcher.js
|
|
87
|
-
const buildPromiseWatcherRootObject = (vatPowers,
|
|
87
|
+
const buildPromiseWatcherRootObject = (vatPowers, vatParameters, baggage) => {
|
|
88
88
|
const { VatData } = vatPowers;
|
|
89
89
|
const { watchPromise } = VatData;
|
|
90
90
|
const { prepareExo } = makeExoUtils(VatData);
|
|
@@ -93,37 +93,62 @@ const buildPromiseWatcherRootObject = (vatPowers, _vatParameters, baggage) => {
|
|
|
93
93
|
onFulfilled: M.call(M.any(), M.string()).returns(),
|
|
94
94
|
onRejected: M.call(M.any(), M.string()).returns(),
|
|
95
95
|
});
|
|
96
|
+
const watchResolutions = new Map();
|
|
96
97
|
const watcher = prepareExo(
|
|
97
98
|
baggage,
|
|
99
|
+
// No longer ignoring, but the name is set in stone in `kvStoreDataV1`
|
|
98
100
|
'DurablePromiseIgnorer',
|
|
99
101
|
PromiseWatcherI,
|
|
100
102
|
{
|
|
101
|
-
onFulfilled(
|
|
102
|
-
|
|
103
|
+
onFulfilled(value, name) {
|
|
104
|
+
watchResolutions.set(name, { status: 'fulfilled', value });
|
|
105
|
+
},
|
|
106
|
+
onRejected(reason, name) {
|
|
107
|
+
watchResolutions.set(name, { status: 'rejected', reason });
|
|
108
|
+
},
|
|
103
109
|
},
|
|
104
110
|
);
|
|
105
111
|
|
|
106
|
-
const
|
|
112
|
+
const knownPromises = new Map();
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
const root = Far('root', {
|
|
115
|
+
getPromise: name => {
|
|
116
|
+
const promise = knownPromises.get(name);
|
|
117
|
+
promise || Fail`promise doesn't exists: ${name}`;
|
|
118
|
+
return { promise };
|
|
119
|
+
},
|
|
120
|
+
importPromise: (name, promise) => {
|
|
121
|
+
!knownPromises.has(name) || Fail`promise already exists: ${name}`;
|
|
122
|
+
knownPromises.set(name, promise);
|
|
123
|
+
return `imported promise: ${name}`;
|
|
124
|
+
},
|
|
110
125
|
createLocalPromise: (name, fulfillment, rejection) => {
|
|
111
|
-
!
|
|
126
|
+
!knownPromises.has(name) || Fail`promise already exists: ${name}`;
|
|
112
127
|
const { promise, resolve, reject } = makePromiseKit();
|
|
113
128
|
if (fulfillment !== undefined) {
|
|
114
129
|
resolve(fulfillment);
|
|
115
130
|
} else if (rejection !== undefined) {
|
|
116
131
|
reject(rejection);
|
|
117
132
|
}
|
|
118
|
-
|
|
133
|
+
knownPromises.set(name, promise);
|
|
119
134
|
return `created local promise: ${name}`;
|
|
120
135
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
watchPromise(
|
|
124
|
-
return `watched
|
|
136
|
+
watchPromise: name => {
|
|
137
|
+
knownPromises.has(name) || Fail`promise not found: ${name}`;
|
|
138
|
+
watchPromise(knownPromises.get(name), watcher, name);
|
|
139
|
+
return `watched promise: ${name}`;
|
|
140
|
+
},
|
|
141
|
+
getWatchResolution: name => {
|
|
142
|
+
return watchResolutions.get(name);
|
|
125
143
|
},
|
|
126
144
|
});
|
|
145
|
+
|
|
146
|
+
const startOperations = vatParameters?.startOperations || [];
|
|
147
|
+
for (const [method, ...args] of startOperations) {
|
|
148
|
+
root[method](...args);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return root;
|
|
127
152
|
};
|
|
128
153
|
const kvStoreDataV1 = Object.entries({
|
|
129
154
|
baggageID: 'o+d6/1',
|
|
@@ -176,23 +201,55 @@ const kvStoreDataV1VpidsToKeep = ['p-8'];
|
|
|
176
201
|
const kvStoreDataV1KeysToKeep = ['vc.4.sp-8'];
|
|
177
202
|
|
|
178
203
|
test('past-incarnation watched promises', async t => {
|
|
204
|
+
const S = 'settlement';
|
|
205
|
+
// Anchor promise counters upon which the other assertions depend.
|
|
206
|
+
const firstPImport = 9;
|
|
207
|
+
// cf. src/liveslots.js:initialIDCounters
|
|
208
|
+
const firstPExport = 5;
|
|
209
|
+
|
|
210
|
+
const startImportedP = firstPImport - 2;
|
|
211
|
+
|
|
212
|
+
const v1StartOperations = [
|
|
213
|
+
['createLocalPromise', 'start orphaned'],
|
|
214
|
+
['watchPromise', 'start orphaned'],
|
|
215
|
+
['createLocalPromise', 'start fulfilled', S],
|
|
216
|
+
['watchPromise', 'start fulfilled'],
|
|
217
|
+
['importPromise', 'start imported', kslot(`p-${startImportedP}`)],
|
|
218
|
+
['watchPromise', 'start imported'],
|
|
219
|
+
];
|
|
179
220
|
const kvStore = new Map();
|
|
180
|
-
let {
|
|
221
|
+
let {
|
|
222
|
+
v,
|
|
223
|
+
dispatch,
|
|
224
|
+
dispatchMessage: rawDispatch,
|
|
225
|
+
} = await setupTestLiveslots(
|
|
181
226
|
t,
|
|
182
227
|
buildPromiseWatcherRootObject,
|
|
183
228
|
'durable-promise-watcher',
|
|
184
|
-
{
|
|
229
|
+
{
|
|
230
|
+
kvStore,
|
|
231
|
+
nextPromiseImportNumber: firstPImport,
|
|
232
|
+
vatParameters: { startOperations: v1StartOperations },
|
|
233
|
+
},
|
|
185
234
|
);
|
|
186
235
|
let vatLogs = v.log;
|
|
187
236
|
|
|
188
|
-
// Anchor promise counters upon which the other assertions depend.
|
|
189
|
-
const firstPImport = 1;
|
|
190
|
-
// cf. src/liveslots.js:initialIDCounters
|
|
191
|
-
const firstPExport = 5;
|
|
192
237
|
let lastPImport = firstPImport - 1;
|
|
193
238
|
let lastPExport = firstPExport - 1;
|
|
239
|
+
let dispatches = 0;
|
|
240
|
+
const exportedPromises = new Set();
|
|
241
|
+
const v1OrphanedPExports = [];
|
|
194
242
|
const nextPImport = () => (lastPImport += 1);
|
|
195
243
|
const nextPExport = () => (lastPExport += 1);
|
|
244
|
+
/** @type {typeof rawDispatch} */
|
|
245
|
+
const dispatchMessage = (...args) => {
|
|
246
|
+
dispatches += 1;
|
|
247
|
+
return rawDispatch(...args);
|
|
248
|
+
};
|
|
249
|
+
const recordExportedPromise = name => {
|
|
250
|
+
exportedPromises.add(name);
|
|
251
|
+
return name;
|
|
252
|
+
};
|
|
196
253
|
// Ignore vatstore syscalls.
|
|
197
254
|
const getDispatchLogs = () =>
|
|
198
255
|
vatLogs.splice(0).filter(m => !m.type.startsWith('vatstore'));
|
|
@@ -208,18 +265,55 @@ test('past-incarnation watched promises', async t => {
|
|
|
208
265
|
type: 'subscribe',
|
|
209
266
|
target: vpid,
|
|
210
267
|
});
|
|
211
|
-
|
|
212
|
-
|
|
268
|
+
|
|
269
|
+
// startVat logs
|
|
270
|
+
v1OrphanedPExports.push(nextPExport());
|
|
271
|
+
recordExportedPromise('start orphaned');
|
|
272
|
+
v1OrphanedPExports.push(nextPExport());
|
|
273
|
+
recordExportedPromise('start fulfilled');
|
|
213
274
|
t.deepEqual(getDispatchLogs(), [
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
subscribeMessage(`p-${startImportedP}`),
|
|
276
|
+
subscribeMessage(`p+${lastPExport - 1}`),
|
|
277
|
+
subscribeMessage(`p+${lastPExport}`),
|
|
278
|
+
fulfillmentMessage(`p+${lastPExport}`, S),
|
|
279
|
+
]);
|
|
280
|
+
await dispatchMessage('createLocalPromise', 'exported', S);
|
|
281
|
+
t.deepEqual(getDispatchLogs(), [
|
|
282
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: exported'),
|
|
283
|
+
]);
|
|
284
|
+
await dispatchMessage('getPromise', recordExportedPromise('exported'));
|
|
285
|
+
t.deepEqual(getDispatchLogs(), [
|
|
286
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
287
|
+
promise: kslot(`p+${nextPExport()}`),
|
|
288
|
+
}),
|
|
289
|
+
fulfillmentMessage(`p+${lastPExport}`, S),
|
|
290
|
+
]);
|
|
291
|
+
const importedP = firstPImport - 1;
|
|
292
|
+
await dispatchMessage('importPromise', 'imported', kslot(`p-${importedP}`));
|
|
293
|
+
t.deepEqual(getDispatchLogs(), [
|
|
294
|
+
subscribeMessage(`p-${importedP}`),
|
|
295
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'imported promise: imported'),
|
|
216
296
|
]);
|
|
217
|
-
|
|
218
|
-
const S = 'settlement';
|
|
219
297
|
await dispatchMessage('createLocalPromise', 'orphaned');
|
|
220
298
|
t.deepEqual(getDispatchLogs(), [
|
|
221
299
|
fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: orphaned'),
|
|
222
300
|
]);
|
|
301
|
+
await dispatchMessage('createLocalPromise', 'orphaned exported');
|
|
302
|
+
await dispatchMessage(
|
|
303
|
+
'getPromise',
|
|
304
|
+
recordExportedPromise('orphaned exported'),
|
|
305
|
+
);
|
|
306
|
+
const orphanedExportedP = nextPExport();
|
|
307
|
+
v1OrphanedPExports.push(orphanedExportedP);
|
|
308
|
+
t.deepEqual(getDispatchLogs(), [
|
|
309
|
+
fulfillmentMessage(
|
|
310
|
+
`p-${nextPImport()}`,
|
|
311
|
+
'created local promise: orphaned exported',
|
|
312
|
+
),
|
|
313
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
314
|
+
promise: kslot(`p+${orphanedExportedP}`),
|
|
315
|
+
}),
|
|
316
|
+
]);
|
|
223
317
|
await dispatchMessage('createLocalPromise', 'fulfilled', S);
|
|
224
318
|
t.deepEqual(getDispatchLogs(), [
|
|
225
319
|
fulfillmentMessage(
|
|
@@ -233,68 +327,185 @@ test('past-incarnation watched promises', async t => {
|
|
|
233
327
|
]);
|
|
234
328
|
t.is(
|
|
235
329
|
lastPImport - firstPImport + 1,
|
|
236
|
-
|
|
237
|
-
|
|
330
|
+
dispatches,
|
|
331
|
+
`imported ${dispatches} promises (1 per dispatch)`,
|
|
332
|
+
);
|
|
333
|
+
t.is(
|
|
334
|
+
lastPExport - firstPExport + 1,
|
|
335
|
+
exportedPromises.size,
|
|
336
|
+
`exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
|
|
238
337
|
);
|
|
239
|
-
t.is(lastPExport - firstPExport + 1, 1, 'exported 1 promise: first');
|
|
240
338
|
|
|
241
|
-
await dispatchMessage('
|
|
339
|
+
await dispatchMessage('watchPromise', recordExportedPromise('orphaned'));
|
|
340
|
+
v1OrphanedPExports.push(nextPExport());
|
|
242
341
|
t.deepEqual(getDispatchLogs(), [
|
|
243
|
-
subscribeMessage(`p+${
|
|
244
|
-
fulfillmentMessage(`p-${nextPImport()}`, 'watched
|
|
342
|
+
subscribeMessage(`p+${lastPExport}`),
|
|
343
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: orphaned'),
|
|
245
344
|
]);
|
|
246
|
-
await dispatchMessage('
|
|
345
|
+
await dispatchMessage('watchPromise', recordExportedPromise('fulfilled'));
|
|
247
346
|
t.deepEqual(getDispatchLogs(), [
|
|
248
347
|
subscribeMessage(`p+${nextPExport()}`),
|
|
249
|
-
fulfillmentMessage(
|
|
250
|
-
`p-${nextPImport()}`,
|
|
251
|
-
'watched local promise: fulfilled',
|
|
252
|
-
),
|
|
348
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: fulfilled'),
|
|
253
349
|
fulfillmentMessage(`p+${lastPExport}`, S),
|
|
254
350
|
]);
|
|
255
|
-
await dispatchMessage('
|
|
351
|
+
await dispatchMessage('watchPromise', recordExportedPromise('rejected'));
|
|
256
352
|
t.deepEqual(getDispatchLogs(), [
|
|
257
353
|
subscribeMessage(`p+${nextPExport()}`),
|
|
258
|
-
fulfillmentMessage(`p-${nextPImport()}`, 'watched
|
|
354
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: rejected'),
|
|
259
355
|
rejectionMessage(`p+${lastPExport}`, S),
|
|
260
356
|
]);
|
|
357
|
+
await dispatchMessage('watchPromise', 'imported');
|
|
358
|
+
t.deepEqual(getDispatchLogs(), [
|
|
359
|
+
// no subscribe, we already did at import
|
|
360
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: imported'),
|
|
361
|
+
]);
|
|
362
|
+
await dispatchMessage('getWatchResolution', 'fulfilled');
|
|
363
|
+
t.deepEqual(getDispatchLogs(), [
|
|
364
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
365
|
+
status: 'fulfilled',
|
|
366
|
+
value: S,
|
|
367
|
+
}),
|
|
368
|
+
]);
|
|
369
|
+
await dispatchMessage('getWatchResolution', 'rejected');
|
|
370
|
+
t.deepEqual(getDispatchLogs(), [
|
|
371
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
372
|
+
status: 'rejected',
|
|
373
|
+
reason: S,
|
|
374
|
+
}),
|
|
375
|
+
]);
|
|
376
|
+
await dispatchMessage('getWatchResolution', 'start fulfilled');
|
|
377
|
+
t.deepEqual(getDispatchLogs(), [
|
|
378
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
379
|
+
status: 'fulfilled',
|
|
380
|
+
value: S,
|
|
381
|
+
}),
|
|
382
|
+
]);
|
|
383
|
+
|
|
261
384
|
t.is(
|
|
262
385
|
lastPImport - firstPImport + 1,
|
|
263
|
-
|
|
264
|
-
|
|
386
|
+
dispatches,
|
|
387
|
+
`imported ${dispatches} promises (1 per dispatch)`,
|
|
265
388
|
);
|
|
266
389
|
t.is(
|
|
267
390
|
lastPExport - firstPExport + 1,
|
|
268
|
-
|
|
269
|
-
|
|
391
|
+
exportedPromises.size,
|
|
392
|
+
`exported ${exportedPromises.size} promises: ${[...exportedPromises].join(', ')}`,
|
|
270
393
|
);
|
|
271
394
|
|
|
272
395
|
// Simulate upgrade by starting from the non-empty kvStore.
|
|
273
396
|
// t.log(Object.fromEntries([...kvStore.entries()].sort(compareEntriesByKey)));
|
|
274
397
|
const clonedStore = new Map(kvStore);
|
|
275
|
-
|
|
398
|
+
const startImported2P = firstPImport - 3;
|
|
399
|
+
const v2StartOperations = [
|
|
400
|
+
['importPromise', 'start imported 2', kslot(`p-${startImported2P}`)], // import of new promise
|
|
401
|
+
['importPromise', 'imported', kslot(`p-${importedP}`)], // import previously imported and watched promise
|
|
402
|
+
['importPromise', 'orphaned exported', kslot(`p+${orphanedExportedP}`)], // import previously exported but unwatched promise
|
|
403
|
+
['watchPromise', 'orphaned exported'],
|
|
404
|
+
];
|
|
405
|
+
({
|
|
406
|
+
v,
|
|
407
|
+
dispatch,
|
|
408
|
+
dispatchMessage: rawDispatch,
|
|
409
|
+
} = await setupTestLiveslots(
|
|
276
410
|
t,
|
|
277
411
|
buildPromiseWatcherRootObject,
|
|
278
412
|
'durable-promise-watcher-v2',
|
|
279
|
-
{
|
|
413
|
+
{
|
|
414
|
+
kvStore: clonedStore,
|
|
415
|
+
nextPromiseImportNumber: lastPImport + 1,
|
|
416
|
+
vatParameters: { startOperations: v2StartOperations },
|
|
417
|
+
},
|
|
280
418
|
));
|
|
281
419
|
vatLogs = v.log;
|
|
282
420
|
|
|
421
|
+
// startVat logs
|
|
422
|
+
t.deepEqual(getDispatchLogs(), [
|
|
423
|
+
subscribeMessage(`p-${startImported2P}`),
|
|
424
|
+
subscribeMessage(`p-${importedP}`),
|
|
425
|
+
subscribeMessage(`p+${orphanedExportedP}`),
|
|
426
|
+
]);
|
|
283
427
|
// Simulate kernel rejection of promises orphaned by termination/upgrade of their decider vat.
|
|
284
428
|
const expectedDeletions = [...clonedStore.entries()].filter(entry =>
|
|
285
429
|
entry[1].includes('orphaned'),
|
|
286
430
|
);
|
|
287
431
|
t.true(expectedDeletions.length >= 1);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
432
|
+
for (const orphanedPExport of v1OrphanedPExports) {
|
|
433
|
+
await dispatch(
|
|
434
|
+
makeReject(`p+${orphanedPExport}`, kser('tomorrow never came')),
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
await dispatchMessage('getWatchResolution', 'orphaned');
|
|
438
|
+
t.deepEqual(getDispatchLogs(), [
|
|
439
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
440
|
+
status: 'rejected',
|
|
441
|
+
reason: 'tomorrow never came',
|
|
442
|
+
}),
|
|
443
|
+
]);
|
|
444
|
+
await dispatchMessage('getWatchResolution', 'start orphaned');
|
|
445
|
+
t.deepEqual(getDispatchLogs(), [
|
|
446
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
447
|
+
status: 'rejected',
|
|
448
|
+
reason: 'tomorrow never came',
|
|
449
|
+
}),
|
|
450
|
+
]);
|
|
451
|
+
await dispatchMessage('getWatchResolution', 'orphaned exported');
|
|
452
|
+
t.deepEqual(getDispatchLogs(), [
|
|
453
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
454
|
+
status: 'rejected',
|
|
455
|
+
reason: 'tomorrow never came',
|
|
456
|
+
}),
|
|
457
|
+
]);
|
|
291
458
|
for (const [key, value] of expectedDeletions) {
|
|
292
459
|
t.false(clonedStore.has(key), `entry should be removed: ${key}: ${value}`);
|
|
293
460
|
}
|
|
461
|
+
// Simulate resolution of imported promises watched in previous incarnation
|
|
462
|
+
await dispatch(makeResolve(`p-${importedP}`, kser(undefined)));
|
|
463
|
+
await dispatchMessage('getWatchResolution', 'imported');
|
|
464
|
+
t.deepEqual(getDispatchLogs(), [
|
|
465
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
466
|
+
status: 'fulfilled',
|
|
467
|
+
value: undefined,
|
|
468
|
+
}),
|
|
469
|
+
]);
|
|
470
|
+
await dispatch(makeResolve(`p-${startImportedP}`, kser(undefined)));
|
|
471
|
+
await dispatchMessage('getWatchResolution', 'start imported');
|
|
472
|
+
t.deepEqual(getDispatchLogs(), [
|
|
473
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
474
|
+
status: 'fulfilled',
|
|
475
|
+
value: undefined,
|
|
476
|
+
}),
|
|
477
|
+
]);
|
|
478
|
+
await dispatch(makeResolve(`p-${startImported2P}`, kser(undefined)));
|
|
479
|
+
await dispatchMessage('getWatchResolution', 'start imported 2');
|
|
480
|
+
t.deepEqual(getDispatchLogs(), [
|
|
481
|
+
fulfillmentMessage(`p-${nextPImport()}`, undefined),
|
|
482
|
+
]);
|
|
483
|
+
// simulate resolution of imported promise watched after resolution
|
|
484
|
+
await dispatchMessage('watchPromise', 'start imported 2');
|
|
485
|
+
t.deepEqual(getDispatchLogs(), [
|
|
486
|
+
// Promise was previously resolved, so it is re-exported
|
|
487
|
+
subscribeMessage(`p+${nextPExport()}`),
|
|
488
|
+
fulfillmentMessage(
|
|
489
|
+
`p-${nextPImport()}`,
|
|
490
|
+
'watched promise: start imported 2',
|
|
491
|
+
),
|
|
492
|
+
fulfillmentMessage(`p+${lastPExport}`, undefined),
|
|
493
|
+
]);
|
|
494
|
+
await dispatchMessage('getWatchResolution', 'start imported 2');
|
|
495
|
+
t.deepEqual(getDispatchLogs(), [
|
|
496
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
497
|
+
status: 'fulfilled',
|
|
498
|
+
value: undefined,
|
|
499
|
+
}),
|
|
500
|
+
]);
|
|
294
501
|
|
|
295
502
|
// Verify that the data is still in loadable condition.
|
|
296
503
|
const finalClonedStore = new Map(clonedStore);
|
|
297
|
-
({
|
|
504
|
+
({
|
|
505
|
+
v,
|
|
506
|
+
dispatch,
|
|
507
|
+
dispatchMessage: rawDispatch,
|
|
508
|
+
} = await setupTestLiveslots(
|
|
298
509
|
t,
|
|
299
510
|
buildPromiseWatcherRootObject,
|
|
300
511
|
'durable-promise-watcher-final',
|
|
@@ -303,12 +514,17 @@ test('past-incarnation watched promises', async t => {
|
|
|
303
514
|
vatLogs = v.log;
|
|
304
515
|
vatLogs.length = 0;
|
|
305
516
|
await dispatchMessage('createLocalPromise', 'final', S);
|
|
306
|
-
await dispatchMessage('
|
|
517
|
+
await dispatchMessage('watchPromise', 'final');
|
|
518
|
+
await dispatchMessage('getWatchResolution', 'final');
|
|
307
519
|
t.deepEqual(getDispatchLogs(), [
|
|
308
520
|
fulfillmentMessage(`p-${nextPImport()}`, 'created local promise: final'),
|
|
309
521
|
subscribeMessage(`p+${nextPExport()}`),
|
|
310
|
-
fulfillmentMessage(`p-${nextPImport()}`, 'watched
|
|
522
|
+
fulfillmentMessage(`p-${nextPImport()}`, 'watched promise: final'),
|
|
311
523
|
fulfillmentMessage(`p+${lastPExport}`, S),
|
|
524
|
+
fulfillmentMessage(`p-${nextPImport()}`, {
|
|
525
|
+
status: 'fulfilled',
|
|
526
|
+
value: S,
|
|
527
|
+
}),
|
|
312
528
|
]);
|
|
313
529
|
});
|
|
314
530
|
|
|
@@ -131,6 +131,7 @@ export async function makeDispatch(
|
|
|
131
131
|
build,
|
|
132
132
|
vatID = 'vatA',
|
|
133
133
|
liveSlotsOptions = {},
|
|
134
|
+
vatParameters = undefined,
|
|
134
135
|
) {
|
|
135
136
|
const gcTools = harden({
|
|
136
137
|
WeakRef,
|
|
@@ -150,7 +151,7 @@ export async function makeDispatch(
|
|
|
150
151
|
return { buildRootObject: build };
|
|
151
152
|
},
|
|
152
153
|
);
|
|
153
|
-
await dispatch(['startVat', kser()]);
|
|
154
|
+
await dispatch(['startVat', kser(vatParameters)]);
|
|
154
155
|
return { dispatch, testHooks };
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -171,6 +172,7 @@ function makeRPMaker(nextNumber = 1) {
|
|
|
171
172
|
* @param {Map<string, string>} [options.kvStore]
|
|
172
173
|
* @param {number} [options.nextPromiseImportNumber]
|
|
173
174
|
* @param {boolean} [options.skipLogging]
|
|
175
|
+
* @param {any} [options.vatParameters]
|
|
174
176
|
*/
|
|
175
177
|
export async function setupTestLiveslots(
|
|
176
178
|
t,
|
|
@@ -183,6 +185,7 @@ export async function setupTestLiveslots(
|
|
|
183
185
|
kvStore = new Map(),
|
|
184
186
|
nextPromiseImportNumber,
|
|
185
187
|
skipLogging = false,
|
|
188
|
+
vatParameters,
|
|
186
189
|
} = options;
|
|
187
190
|
const { log, syscall, fakestore } = buildSyscall({ skipLogging, kvStore });
|
|
188
191
|
const nextRP = makeRPMaker(nextPromiseImportNumber);
|
|
@@ -190,6 +193,8 @@ export async function setupTestLiveslots(
|
|
|
190
193
|
syscall,
|
|
191
194
|
buildRootObject,
|
|
192
195
|
vatName,
|
|
196
|
+
{},
|
|
197
|
+
vatParameters,
|
|
193
198
|
);
|
|
194
199
|
|
|
195
200
|
async function dispatchMessage(message, ...args) {
|
|
@@ -898,3 +898,104 @@ test('inter-vat circular promise references', async t => {
|
|
|
898
898
|
// });
|
|
899
899
|
// t.deepEqual(log, []);
|
|
900
900
|
});
|
|
901
|
+
|
|
902
|
+
test('accept previously allocated promise', async t => {
|
|
903
|
+
function build(vatPowers, vatParameters) {
|
|
904
|
+
const { target } = vatParameters;
|
|
905
|
+
return Far('root', {
|
|
906
|
+
call() {
|
|
907
|
+
const promise = E(target).foo();
|
|
908
|
+
// wrap to avoid adoption
|
|
909
|
+
return { promise };
|
|
910
|
+
},
|
|
911
|
+
async waitFor(promise2) {
|
|
912
|
+
// if we're in the same incarnation as the `E(target).foo()`, this
|
|
913
|
+
// `promise2` will be the same JS Promise as the `promise` above
|
|
914
|
+
const v = await promise2;
|
|
915
|
+
return v;
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
let log;
|
|
921
|
+
let syscall;
|
|
922
|
+
let dispatch;
|
|
923
|
+
|
|
924
|
+
const kvStore = new Map();
|
|
925
|
+
({ log, syscall } = buildSyscall({ kvStore }));
|
|
926
|
+
|
|
927
|
+
const target = 'o-1';
|
|
928
|
+
({ dispatch } = await makeDispatch(
|
|
929
|
+
syscall,
|
|
930
|
+
build,
|
|
931
|
+
'reimport-promise',
|
|
932
|
+
{},
|
|
933
|
+
{ target: kslot(target) },
|
|
934
|
+
));
|
|
935
|
+
log.length = 0; // assume pre-build vatstore operations are correct
|
|
936
|
+
|
|
937
|
+
const root = 'o+0';
|
|
938
|
+
const callResultP = 'p-1';
|
|
939
|
+
const waitForResultP = 'p-2';
|
|
940
|
+
const expectedP1 = 'p+5';
|
|
941
|
+
|
|
942
|
+
await dispatch(makeMessage(root, 'call', [], callResultP));
|
|
943
|
+
|
|
944
|
+
// The vat should send 'foo', subscribe to the result promise, and resolve with that promise
|
|
945
|
+
t.deepEqual(log.splice(0, 3), [
|
|
946
|
+
{
|
|
947
|
+
type: 'send',
|
|
948
|
+
targetSlot: target,
|
|
949
|
+
methargs: kser(['foo', []]),
|
|
950
|
+
resultSlot: expectedP1,
|
|
951
|
+
},
|
|
952
|
+
{ type: 'subscribe', target: expectedP1 },
|
|
953
|
+
{
|
|
954
|
+
type: 'resolve',
|
|
955
|
+
resolutions: [[callResultP, false, kser({ promise: kslot(expectedP1) })]],
|
|
956
|
+
},
|
|
957
|
+
]);
|
|
958
|
+
matchIDCounterSet(t, log);
|
|
959
|
+
t.deepEqual(log, []);
|
|
960
|
+
|
|
961
|
+
// snapshot the store at this point
|
|
962
|
+
const clonedStore = new Map(kvStore);
|
|
963
|
+
|
|
964
|
+
const verifyPromiseReImport = async shouldSubscribe => {
|
|
965
|
+
await dispatch(
|
|
966
|
+
makeMessage(root, 'waitFor', [kslot(expectedP1)], waitForResultP),
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// The vat will only subscribe if it was upgraded; if the vat still
|
|
970
|
+
// remembers it, receiving the vref will merely look it up in slotToVal and
|
|
971
|
+
// not create a new Promise (and trigger a subscribe)
|
|
972
|
+
if (shouldSubscribe) {
|
|
973
|
+
t.deepEqual(log.shift(), { type: 'subscribe', target: expectedP1 });
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// waitFor will suspend until promise is resolved
|
|
977
|
+
t.deepEqual(log, []);
|
|
978
|
+
|
|
979
|
+
// Resolution propagates the value to the waitFor result
|
|
980
|
+
await dispatch(makeResolve(expectedP1, kser('success')));
|
|
981
|
+
t.deepEqual(log.shift(), {
|
|
982
|
+
type: 'resolve',
|
|
983
|
+
resolutions: [[waitForResultP, false, kser('success')]],
|
|
984
|
+
});
|
|
985
|
+
t.deepEqual(log, []);
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
await verifyPromiseReImport(false);
|
|
989
|
+
({ log, syscall } = buildSyscall({ kvStore: clonedStore }));
|
|
990
|
+
({ dispatch } = await makeDispatch(
|
|
991
|
+
syscall,
|
|
992
|
+
build,
|
|
993
|
+
'reimport-promise-v2',
|
|
994
|
+
{},
|
|
995
|
+
{},
|
|
996
|
+
));
|
|
997
|
+
log.length = 0;
|
|
998
|
+
|
|
999
|
+
// verify this works the same in the restarted vat
|
|
1000
|
+
await verifyPromiseReImport(true);
|
|
1001
|
+
});
|