@agoric/swingset-liveslots 0.10.3-u16.0 → 0.10.3-u17.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 +17 -18
- package/src/boyd-gc.js +598 -0
- package/src/cache.js +1 -1
- package/src/capdata.js +1 -1
- package/src/collectionManager.js +65 -21
- package/src/facetiousness.js +1 -1
- package/src/liveslots.js +83 -202
- package/src/message.js +1 -1
- package/src/parseVatSlots.js +1 -1
- package/src/vatDataTypes.d.ts +7 -2
- package/src/virtualObjectManager.js +1 -6
- package/src/virtualReferences.js +83 -25
- package/src/watchedPromises.js +7 -2
- package/test/clear-collection.test.js +586 -0
- package/test/collections.test.js +41 -0
- package/test/dropped-weakset-9939.test.js +80 -0
- package/test/dummyMeterControl.js +1 -1
- package/test/durabilityChecks.test.js +1 -1
- package/test/gc-before-finalizer.test.js +230 -0
- package/test/handled-promises.test.js +266 -50
- package/test/liveslots-helpers.js +6 -1
- package/test/liveslots-mock-gc.test.js +99 -0
- package/test/liveslots-real-gc.test.js +18 -2
- package/test/liveslots.test.js +1 -1
- package/test/vat-environment.test.js +65 -0
- package/test/vat-util.js +1 -1
- package/test/virtual-objects/rep-tostring.test.js +1 -2
- package/test/vpid-liveslots.test.js +102 -1
- package/test/weakset-dropped-remotable.test.js +50 -0
- package/tools/fakeVirtualSupport.js +1 -3
- package/tools/setup-vat-data.js +10 -3
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
|
|
3
|
+
import { Far } from '@endo/marshal';
|
|
4
|
+
import { kser, kslot } from '@agoric/kmarshal';
|
|
5
|
+
import { makeLiveSlots } from '../src/liveslots.js';
|
|
6
|
+
import { parseVatSlot } from '../src/parseVatSlots.js';
|
|
7
|
+
import { buildSyscall } from './liveslots-helpers.js';
|
|
8
|
+
import { makeMessage, makeStartVat, makeBringOutYourDead } from './util.js';
|
|
9
|
+
import { makeMockGC } from './mock-gc.js';
|
|
10
|
+
|
|
11
|
+
const getPrefixedKeys = (map, prefix) => {
|
|
12
|
+
const keys = [];
|
|
13
|
+
for (const key of map.keys()) {
|
|
14
|
+
if (key.startsWith(prefix)) {
|
|
15
|
+
keys.push(key.substring(prefix.length));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return keys;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const collectionMetaKeys = new Set([
|
|
22
|
+
'|nextOrdinal',
|
|
23
|
+
'|entryCount',
|
|
24
|
+
'|schemata',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const scanCollection = (kvStore, collectionID) => {
|
|
28
|
+
const collectionPrefix = `vc.${collectionID}.`;
|
|
29
|
+
const ordinalAssignments = [];
|
|
30
|
+
const entries = [];
|
|
31
|
+
const metaKeys = [];
|
|
32
|
+
let totalKeys = 0;
|
|
33
|
+
for (const key of getPrefixedKeys(kvStore, collectionPrefix)) {
|
|
34
|
+
totalKeys += 1;
|
|
35
|
+
if (key.startsWith('|')) {
|
|
36
|
+
if (collectionMetaKeys.has(key)) {
|
|
37
|
+
metaKeys.push(key);
|
|
38
|
+
} else {
|
|
39
|
+
ordinalAssignments.push(key);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
entries.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const keyVrefs = [];
|
|
46
|
+
const refcounts = {};
|
|
47
|
+
for (const ordinalKey of ordinalAssignments) {
|
|
48
|
+
const vref = ordinalKey.substring(1);
|
|
49
|
+
keyVrefs.push(vref);
|
|
50
|
+
const rcKey = `vom.rc.${vref}`;
|
|
51
|
+
refcounts[vref] = kvStore.get(rcKey);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
totalKeys,
|
|
55
|
+
metaKeys,
|
|
56
|
+
ordinalAssignments,
|
|
57
|
+
entries,
|
|
58
|
+
keyVrefs,
|
|
59
|
+
refcounts,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const GC = ['dropImports', 'retireImports', 'retireExports'];
|
|
64
|
+
|
|
65
|
+
const doTest = async (t, mode) => {
|
|
66
|
+
assert(['strong-clear', 'strong-delete', 'weak-delete'].includes(mode));
|
|
67
|
+
const kvStore = new Map();
|
|
68
|
+
const { syscall, log } = buildSyscall({ kvStore });
|
|
69
|
+
const gcTools = makeMockGC();
|
|
70
|
+
const COUNT = 5;
|
|
71
|
+
|
|
72
|
+
// we'll either call holder.clear() to exercise manual clearing, or
|
|
73
|
+
// gcTools.kill(holder) to exercise the collection itself being
|
|
74
|
+
// deleted
|
|
75
|
+
|
|
76
|
+
let holder;
|
|
77
|
+
|
|
78
|
+
function build(vatPowers) {
|
|
79
|
+
const { defineKind, makeScalarBigSetStore } = vatPowers.VatData;
|
|
80
|
+
const make = defineKind('target', () => ({}), {});
|
|
81
|
+
holder = makeScalarBigSetStore('holder');
|
|
82
|
+
const root = Far('root', {
|
|
83
|
+
create() {
|
|
84
|
+
for (let i = 0; i < COUNT; i += 1) {
|
|
85
|
+
// vrefs are all `o+v10/${N}`, N=1..10
|
|
86
|
+
const target = make();
|
|
87
|
+
holder.add(target);
|
|
88
|
+
// we immediately delete the presence, but the finalizers
|
|
89
|
+
// won't run until gcTools.flushAllFRs()
|
|
90
|
+
gcTools.kill(target);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
clear() {
|
|
94
|
+
holder.clear();
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return root;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
|
|
101
|
+
buildRootObject: build,
|
|
102
|
+
}));
|
|
103
|
+
const { dispatch, testHooks } = ls;
|
|
104
|
+
const { valToSlot } = testHooks;
|
|
105
|
+
await dispatch(makeStartVat(kser()));
|
|
106
|
+
log.length = 0;
|
|
107
|
+
|
|
108
|
+
const rootA = 'o+0';
|
|
109
|
+
|
|
110
|
+
await dispatch(makeMessage(rootA, 'create', []));
|
|
111
|
+
log.length = 0;
|
|
112
|
+
|
|
113
|
+
const holderVref = valToSlot.get(holder);
|
|
114
|
+
const collectionID = Number(parseVatSlot(holderVref).subid);
|
|
115
|
+
const populated = scanCollection(kvStore, collectionID);
|
|
116
|
+
t.is(populated.ordinalAssignments.length, COUNT);
|
|
117
|
+
t.is(populated.entries.length, COUNT);
|
|
118
|
+
t.is(populated.keyVrefs.length, COUNT);
|
|
119
|
+
t.true(populated.keyVrefs.every(vref => populated.refcounts[vref] === '1'));
|
|
120
|
+
|
|
121
|
+
// Collect the representatives, leaving only the virtual-data
|
|
122
|
+
// pillar. This BOYD finds non-zero virtual-data refcounts for all
|
|
123
|
+
// five VOs, so they are not deleted.
|
|
124
|
+
gcTools.flushAllFRs();
|
|
125
|
+
const boyd1 = await dispatch(makeBringOutYourDead());
|
|
126
|
+
t.is(boyd1.possiblyDeadSet, 0);
|
|
127
|
+
t.is(boyd1.possiblyRetiredSet, 0);
|
|
128
|
+
log.length = 0;
|
|
129
|
+
t.is(scanCollection(kvStore, collectionID).totalKeys, populated.totalKeys);
|
|
130
|
+
|
|
131
|
+
if (mode === 'strong-clear') {
|
|
132
|
+
// clearing the collections should delete both the data key and the
|
|
133
|
+
// ordinal key for each entry, but it won't delete the values, that
|
|
134
|
+
// is deferred until BOYD. The metadata is retained, because the
|
|
135
|
+
// collection has been cleared, not deleted.
|
|
136
|
+
await dispatch(makeMessage(rootA, 'clear', []));
|
|
137
|
+
} else if (mode === 'strong-delete') {
|
|
138
|
+
gcTools.kill(holder);
|
|
139
|
+
gcTools.flushAllFRs();
|
|
140
|
+
await dispatch(makeBringOutYourDead());
|
|
141
|
+
// that should clear everything, both the holder and the referenced
|
|
142
|
+
// targets
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const scan2 = scanCollection(kvStore, collectionID);
|
|
146
|
+
|
|
147
|
+
// all entries should be gone
|
|
148
|
+
t.is(scan2.ordinalAssignments.length, 0);
|
|
149
|
+
t.is(scan2.entries.length, 0);
|
|
150
|
+
t.is(scan2.keyVrefs.length, 0);
|
|
151
|
+
|
|
152
|
+
if (mode === 'strong-clear') {
|
|
153
|
+
// but the collection itself is still present
|
|
154
|
+
t.is(scan2.metaKeys.length, populated.metaKeys.length);
|
|
155
|
+
for (const vref of populated.keyVrefs) {
|
|
156
|
+
const rcKey = `vom.rc.${vref}`;
|
|
157
|
+
const rc = kvStore.get(rcKey);
|
|
158
|
+
// the target refcounts should be zero (= undefined)
|
|
159
|
+
t.is(rc, undefined);
|
|
160
|
+
// but the data should still be present
|
|
161
|
+
const dataKey = `vom.${vref}`;
|
|
162
|
+
const data = kvStore.get(dataKey);
|
|
163
|
+
t.is(data, '{}');
|
|
164
|
+
}
|
|
165
|
+
// and we need one more BOYD to notice the zero refcounts and
|
|
166
|
+
// delete the data
|
|
167
|
+
await dispatch(makeBringOutYourDead());
|
|
168
|
+
} else if (mode === 'strong-delete') {
|
|
169
|
+
// the collection should be gone
|
|
170
|
+
t.is(scan2.metaKeys.length, 0);
|
|
171
|
+
t.is(scan2.totalKeys, 0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// all the targets should be collected now
|
|
175
|
+
for (const vref of populated.keyVrefs) {
|
|
176
|
+
const rcKey = `vom.rc.${vref}`;
|
|
177
|
+
const rc = kvStore.get(rcKey);
|
|
178
|
+
// the target refcounts should be zero (= undefined)
|
|
179
|
+
t.is(rc, undefined);
|
|
180
|
+
const dataKey = `vom.${vref}`;
|
|
181
|
+
const data = kvStore.get(dataKey);
|
|
182
|
+
t.is(data, undefined);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// none of the Presences were exported, so no GC syscalls
|
|
186
|
+
const gcCalls1 = log.filter(l => GC.includes(l.type));
|
|
187
|
+
t.deepEqual(gcCalls1, []);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// When a virtual collection's keys are the only reference to a
|
|
191
|
+
// virtual object, collection.clear() should let them be deleted. Bug
|
|
192
|
+
// #8756 caused the keys to be retained by mistake.
|
|
193
|
+
|
|
194
|
+
test('collection.clear() deletes keys', async t => {
|
|
195
|
+
await doTest(t, 'strong-clear');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Allowing GC to delete a strong collection should delete/release the
|
|
199
|
+
// keys too
|
|
200
|
+
|
|
201
|
+
test('deleting a strong collection will delete the keys', async t => {
|
|
202
|
+
await doTest(t, 'strong-delete');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Allowing GC to delete a weak collection should retire the keys, and
|
|
206
|
+
// delete/release the contents.
|
|
207
|
+
|
|
208
|
+
test('deleting a weak collection will retire the keys', async t => {
|
|
209
|
+
const kvStore = new Map();
|
|
210
|
+
const { syscall, log } = buildSyscall({ kvStore });
|
|
211
|
+
const gcTools = makeMockGC();
|
|
212
|
+
const COUNT = 5;
|
|
213
|
+
const allVrefs = [];
|
|
214
|
+
const allKslots = [];
|
|
215
|
+
for (let i = 0; i < COUNT; i += 1) {
|
|
216
|
+
const vref = `o-${i + 1}`;
|
|
217
|
+
allVrefs.push(vref);
|
|
218
|
+
allKslots.push(kslot(vref, 'imported'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let recognizer;
|
|
222
|
+
|
|
223
|
+
// Import a bunch of Presences and hold them in a weakset. Drop the
|
|
224
|
+
// imports, but retain recognition, until we drop the weakset, which
|
|
225
|
+
// should delete the collection and notify the kernel that we aren't
|
|
226
|
+
// recognizing the keys (syscall.retireImports)
|
|
227
|
+
function build(vatPowers) {
|
|
228
|
+
const { makeScalarBigWeakSetStore } = vatPowers.VatData;
|
|
229
|
+
recognizer = makeScalarBigWeakSetStore('recognizer');
|
|
230
|
+
const root = Far('root', {
|
|
231
|
+
create(presences) {
|
|
232
|
+
for (const p of presences) {
|
|
233
|
+
recognizer.add(p);
|
|
234
|
+
// we immediately delete the presence, but the finalizers
|
|
235
|
+
// won't run until gcTools.flushAllFRs()
|
|
236
|
+
gcTools.kill(p);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
return root;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
|
|
244
|
+
buildRootObject: build,
|
|
245
|
+
}));
|
|
246
|
+
const { dispatch, testHooks } = ls;
|
|
247
|
+
const { valToSlot } = testHooks;
|
|
248
|
+
await dispatch(makeStartVat(kser()));
|
|
249
|
+
log.length = 0;
|
|
250
|
+
|
|
251
|
+
const rootA = 'o+0';
|
|
252
|
+
|
|
253
|
+
await dispatch(makeMessage(rootA, 'create', [allKslots]));
|
|
254
|
+
log.length = 0;
|
|
255
|
+
|
|
256
|
+
const recognizerVref = valToSlot.get(recognizer);
|
|
257
|
+
const collectionID = Number(parseVatSlot(recognizerVref).subid);
|
|
258
|
+
|
|
259
|
+
// all the Presences should be recognized by the collection, but not
|
|
260
|
+
// referenced
|
|
261
|
+
const populated = scanCollection(kvStore, collectionID);
|
|
262
|
+
t.is(populated.ordinalAssignments.length, COUNT);
|
|
263
|
+
t.is(populated.entries.length, COUNT);
|
|
264
|
+
t.is(populated.keyVrefs.length, COUNT);
|
|
265
|
+
t.true(
|
|
266
|
+
populated.keyVrefs.every(vref => populated.refcounts[vref] === undefined),
|
|
267
|
+
);
|
|
268
|
+
// and there should be recognizer (.ir) entries for each vref|collection pair
|
|
269
|
+
t.true(
|
|
270
|
+
populated.keyVrefs.every(vref =>
|
|
271
|
+
kvStore.has(`vom.ir.${vref}|${collectionID}`),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// collect the Presences, which was the only remaining reachability
|
|
276
|
+
// pillar, leaving just the recognizers
|
|
277
|
+
gcTools.flushAllFRs();
|
|
278
|
+
await dispatch(makeBringOutYourDead());
|
|
279
|
+
// no changes to the collection
|
|
280
|
+
t.deepEqual(scanCollection(kvStore, collectionID), populated);
|
|
281
|
+
// but the Presence vrefs should be dropped
|
|
282
|
+
const gcCalls1 = log.filter(l => GC.includes(l.type));
|
|
283
|
+
t.deepEqual(gcCalls1, [{ type: 'dropImports', slots: allVrefs }]);
|
|
284
|
+
log.length = 0;
|
|
285
|
+
|
|
286
|
+
// now free the whole collection
|
|
287
|
+
gcTools.kill(recognizer);
|
|
288
|
+
gcTools.flushAllFRs();
|
|
289
|
+
await dispatch(makeBringOutYourDead());
|
|
290
|
+
|
|
291
|
+
// the collection should be gone
|
|
292
|
+
const scan2 = scanCollection(kvStore, collectionID);
|
|
293
|
+
t.is(scan2.totalKeys, 0);
|
|
294
|
+
|
|
295
|
+
// and the .ir entries
|
|
296
|
+
t.true(
|
|
297
|
+
populated.keyVrefs.every(
|
|
298
|
+
vref => !kvStore.has(`vom.ir.${vref}|${collectionID}`),
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// and the kernel should be notified that we don't care anymore
|
|
303
|
+
const gcCalls2 = log.filter(l => GC.includes(l.type));
|
|
304
|
+
t.deepEqual(gcCalls2, [{ type: 'retireImports', slots: allVrefs }]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Allowing GC to delete a voAwareWeakSet (or Map) should retire the
|
|
308
|
+
// keys, and delete/release the contents.
|
|
309
|
+
|
|
310
|
+
test('deleting a voAwareWeakSet will retire the keys', async t => {
|
|
311
|
+
const kvStore = new Map();
|
|
312
|
+
const { syscall, log } = buildSyscall({ kvStore });
|
|
313
|
+
const gcTools = makeMockGC();
|
|
314
|
+
const COUNT = 5;
|
|
315
|
+
const allVrefs = [];
|
|
316
|
+
const allKslots = [];
|
|
317
|
+
for (let i = 0; i < COUNT; i += 1) {
|
|
318
|
+
const vref = `o-${i + 1}`;
|
|
319
|
+
allVrefs.push(vref);
|
|
320
|
+
allKslots.push(kslot(vref, 'imported'));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let recognizer;
|
|
324
|
+
|
|
325
|
+
// Import a bunch of Presences and hold them in a weakset. Drop the
|
|
326
|
+
// imports, but retain recognition, until we drop the weakset, which
|
|
327
|
+
// should delete the collection and notify the kernel that we aren't
|
|
328
|
+
// recognizing the keys (syscall.retireImports)
|
|
329
|
+
function build(vatPowers) {
|
|
330
|
+
recognizer = new vatPowers.WeakSet();
|
|
331
|
+
const root = Far('root', {
|
|
332
|
+
create(presences) {
|
|
333
|
+
for (const p of presences) {
|
|
334
|
+
recognizer.add(p);
|
|
335
|
+
// we immediately delete the presence, but the finalizers
|
|
336
|
+
// won't run until gcTools.flushAllFRs()
|
|
337
|
+
gcTools.kill(p);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
return root;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
|
|
345
|
+
buildRootObject: build,
|
|
346
|
+
}));
|
|
347
|
+
const { dispatch, testHooks } = ls;
|
|
348
|
+
const { vrefRecognizers } = testHooks;
|
|
349
|
+
await dispatch(makeStartVat(kser()));
|
|
350
|
+
log.length = 0;
|
|
351
|
+
|
|
352
|
+
const rootA = 'o+0';
|
|
353
|
+
|
|
354
|
+
await dispatch(makeMessage(rootA, 'create', [allKslots]));
|
|
355
|
+
log.length = 0;
|
|
356
|
+
|
|
357
|
+
// the WeakSet has no vref, and doesn't store anything like ".ir"
|
|
358
|
+
// entries in vatstore, but we can snoop on its internal
|
|
359
|
+
// tables. vrefRecognizers is a Map, keyed by vref, with an entry
|
|
360
|
+
// for every vref that is tracked by any voAwareWeakMap/Set. The
|
|
361
|
+
// value is a Set of virtualObjectMaps, the internal/hidden Set used
|
|
362
|
+
// by voAwareWeakMap/Sets.
|
|
363
|
+
|
|
364
|
+
const vrefKeys = [...vrefRecognizers.keys()].sort();
|
|
365
|
+
|
|
366
|
+
// we should be tracking all the presences
|
|
367
|
+
t.is(vrefKeys.length, COUNT);
|
|
368
|
+
// each vref should have a single recognizer
|
|
369
|
+
t.true(vrefKeys.every(vref => vrefRecognizers.get(vref).size === 1));
|
|
370
|
+
// that single recognizer should be the virtualObjectMap for our voAwareWeakSet
|
|
371
|
+
const virtualObjectMap = [...vrefRecognizers.get(vrefKeys[0])][0];
|
|
372
|
+
// they should all point to the same one
|
|
373
|
+
t.true(
|
|
374
|
+
vrefKeys.every(
|
|
375
|
+
vref => [...vrefRecognizers.get(vref)][0] === virtualObjectMap,
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// collect the Presences, which was the only remaining reachability
|
|
380
|
+
// pillar, leaving just the recognizers
|
|
381
|
+
gcTools.flushAllFRs();
|
|
382
|
+
await dispatch(makeBringOutYourDead());
|
|
383
|
+
// no changes to the collection
|
|
384
|
+
t.is(vrefKeys.length, COUNT);
|
|
385
|
+
t.true(vrefKeys.every(vref => vrefRecognizers.get(vref).size === 1));
|
|
386
|
+
t.true(
|
|
387
|
+
vrefKeys.every(
|
|
388
|
+
vref => [...vrefRecognizers.get(vref)][0] === virtualObjectMap,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// but the Presence vrefs should be dropped
|
|
393
|
+
const gcCalls1 = log.filter(l => GC.includes(l.type));
|
|
394
|
+
t.deepEqual(gcCalls1, [{ type: 'dropImports', slots: allVrefs }]);
|
|
395
|
+
log.length = 0;
|
|
396
|
+
|
|
397
|
+
// now free the whole collection
|
|
398
|
+
gcTools.kill(recognizer);
|
|
399
|
+
gcTools.flushAllFRs();
|
|
400
|
+
await dispatch(makeBringOutYourDead());
|
|
401
|
+
|
|
402
|
+
// the collection should be gone
|
|
403
|
+
t.is(vrefRecognizers.size, 0);
|
|
404
|
+
|
|
405
|
+
// and the kernel should be notified that we don't care anymore
|
|
406
|
+
const gcCalls2 = log.filter(l => GC.includes(l.type));
|
|
407
|
+
t.deepEqual(gcCalls2, [{ type: 'retireImports', slots: allVrefs }]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// explore remediation/leftover problems from bugs #7355, #8756, #9956
|
|
411
|
+
// where the DB has corrupted data leftover from before they were fixed
|
|
412
|
+
|
|
413
|
+
test('missing recognition record during delete', async t => {
|
|
414
|
+
const kvStore = new Map();
|
|
415
|
+
const { syscall, log } = buildSyscall({ kvStore });
|
|
416
|
+
const gcTools = makeMockGC();
|
|
417
|
+
|
|
418
|
+
let recognizer;
|
|
419
|
+
let target;
|
|
420
|
+
|
|
421
|
+
// liveslots didn't always add "vom.ir." recognition-records for
|
|
422
|
+
// Remotable-style keys, nor remove them when the key was
|
|
423
|
+
// deleted. So a kernel which adds a key, upgrades to the current
|
|
424
|
+
// (fixed) version, then attempts to delete the key, will not see
|
|
425
|
+
// the record it is expecting. Make sure this doesn't cause
|
|
426
|
+
// problems.
|
|
427
|
+
|
|
428
|
+
function build(vatPowers) {
|
|
429
|
+
const { makeScalarBigWeakSetStore } = vatPowers.VatData;
|
|
430
|
+
recognizer = makeScalarBigWeakSetStore('recognizer');
|
|
431
|
+
target = Far('target', {});
|
|
432
|
+
const root = Far('root', {
|
|
433
|
+
store() {
|
|
434
|
+
recognizer.add(target);
|
|
435
|
+
},
|
|
436
|
+
delete() {
|
|
437
|
+
recognizer.delete(target);
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
return root;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
|
|
444
|
+
buildRootObject: build,
|
|
445
|
+
}));
|
|
446
|
+
const { dispatch, testHooks } = ls;
|
|
447
|
+
const { valToSlot } = testHooks;
|
|
448
|
+
await dispatch(makeStartVat(kser()));
|
|
449
|
+
log.length = 0;
|
|
450
|
+
|
|
451
|
+
const rootA = 'o+0';
|
|
452
|
+
|
|
453
|
+
await dispatch(makeMessage(rootA, 'store'));
|
|
454
|
+
log.length = 0;
|
|
455
|
+
|
|
456
|
+
const targetVref = valToSlot.get(target);
|
|
457
|
+
const recognizerVref = valToSlot.get(recognizer);
|
|
458
|
+
const collectionID = Number(parseVatSlot(recognizerVref).subid);
|
|
459
|
+
const ordinalAssignmentKey = `vc.${collectionID}.|${targetVref}`;
|
|
460
|
+
const ordinalNumber = kvStore.get(ordinalAssignmentKey);
|
|
461
|
+
t.is(ordinalNumber, '1');
|
|
462
|
+
const dataKey = `vc.${collectionID}.r0000000001:${targetVref}`;
|
|
463
|
+
const value = kvStore.get(dataKey);
|
|
464
|
+
t.deepEqual(JSON.parse(value), { body: '#null', slots: [] });
|
|
465
|
+
|
|
466
|
+
// the correct recognition record key
|
|
467
|
+
const rrKey = `vom.ir.${targetVref}|${collectionID}`;
|
|
468
|
+
|
|
469
|
+
// our fixed version creates one
|
|
470
|
+
t.is(kvStore.get(rrKey), '1');
|
|
471
|
+
|
|
472
|
+
// now simulate data from the broken version, by deleting the
|
|
473
|
+
// recognition record
|
|
474
|
+
kvStore.delete(rrKey);
|
|
475
|
+
|
|
476
|
+
// check that deleting the same Remotable doesn't break
|
|
477
|
+
await dispatch(makeMessage(rootA, 'delete'));
|
|
478
|
+
t.false(kvStore.has(ordinalAssignmentKey));
|
|
479
|
+
t.false(kvStore.has(dataKey));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// This test is marked as failing because we do not have any
|
|
483
|
+
// remediation code for #8756. Collections which were cleared before
|
|
484
|
+
// the fix will be corrupted, such that the old keys appear to still
|
|
485
|
+
// be present, even after the fix has been applied. This test
|
|
486
|
+
// demonstrates that we can still *not* handle the following sequence:
|
|
487
|
+
//
|
|
488
|
+
// * (in old version, without fix for #8756):
|
|
489
|
+
// * const c = makeScalarBigMapStore();
|
|
490
|
+
// * const key = Far(); // or any remotable
|
|
491
|
+
// * c.add(key, 'value');
|
|
492
|
+
// * c.clear();
|
|
493
|
+
// * (then in new version, with fix)
|
|
494
|
+
// * assert.equal(c.has(key), false);
|
|
495
|
+
// * c.init(key, 'new value');
|
|
496
|
+
|
|
497
|
+
test.failing('leftover ordinal-assignment record during init', async t => {
|
|
498
|
+
const kvStore = new Map();
|
|
499
|
+
const { syscall, log } = buildSyscall({ kvStore });
|
|
500
|
+
const gcTools = makeMockGC();
|
|
501
|
+
|
|
502
|
+
let store;
|
|
503
|
+
let target;
|
|
504
|
+
/** @type {any} */
|
|
505
|
+
let result;
|
|
506
|
+
|
|
507
|
+
// liveslots didn't always remove the "vc.${collectionID}.|${vref}"
|
|
508
|
+
// ordinal-assignment records when clearing or deleting a
|
|
509
|
+
// collection. So a kernel which adds a key, upgrades to the current
|
|
510
|
+
// (fixed) version, then clears the collection, will have a leftover
|
|
511
|
+
// record. See if this will cause problems when iterating keys or
|
|
512
|
+
// re-adding the same key later.
|
|
513
|
+
|
|
514
|
+
function build(vatPowers) {
|
|
515
|
+
const { makeScalarBigMapStore } = vatPowers.VatData;
|
|
516
|
+
store = makeScalarBigMapStore('store');
|
|
517
|
+
target = Far('target', {});
|
|
518
|
+
const root = Far('root', {
|
|
519
|
+
store() {
|
|
520
|
+
try {
|
|
521
|
+
store.init(target, 123);
|
|
522
|
+
result = 'ok';
|
|
523
|
+
} catch (e) {
|
|
524
|
+
result = e;
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
clear() {
|
|
528
|
+
store.clear();
|
|
529
|
+
},
|
|
530
|
+
has() {
|
|
531
|
+
result = store.has(target);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
return root;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({
|
|
538
|
+
buildRootObject: build,
|
|
539
|
+
}));
|
|
540
|
+
const { dispatch, testHooks } = ls;
|
|
541
|
+
const { valToSlot } = testHooks;
|
|
542
|
+
await dispatch(makeStartVat(kser()));
|
|
543
|
+
log.length = 0;
|
|
544
|
+
|
|
545
|
+
const rootA = 'o+0';
|
|
546
|
+
|
|
547
|
+
result = undefined;
|
|
548
|
+
await dispatch(makeMessage(rootA, 'store'));
|
|
549
|
+
t.is(result, 'ok');
|
|
550
|
+
|
|
551
|
+
const targetVref = valToSlot.get(target);
|
|
552
|
+
const storeVref = valToSlot.get(store);
|
|
553
|
+
const collectionID = Number(parseVatSlot(storeVref).subid);
|
|
554
|
+
const ordinalAssignmentKey = `vc.${collectionID}.|${targetVref}`;
|
|
555
|
+
const ordinalNumber = kvStore.get(ordinalAssignmentKey);
|
|
556
|
+
t.is(ordinalNumber, '1');
|
|
557
|
+
const dataKey = `vc.${collectionID}.r0000000001:${targetVref}`;
|
|
558
|
+
const value = kvStore.get(dataKey);
|
|
559
|
+
t.deepEqual(JSON.parse(value), { body: '#123', slots: [] });
|
|
560
|
+
|
|
561
|
+
result = undefined;
|
|
562
|
+
await dispatch(makeMessage(rootA, 'clear'));
|
|
563
|
+
|
|
564
|
+
// now simulate data from the broken version, by restoring the
|
|
565
|
+
// ordinal-assignment record, as if the code failed to delete it
|
|
566
|
+
|
|
567
|
+
kvStore.set(ordinalAssignmentKey, '1');
|
|
568
|
+
|
|
569
|
+
// problem 1: store.has() should report "false", but incorrectly
|
|
570
|
+
// returns "true"
|
|
571
|
+
|
|
572
|
+
result = undefined;
|
|
573
|
+
await dispatch(makeMessage(rootA, 'has'));
|
|
574
|
+
t.is(result, false);
|
|
575
|
+
|
|
576
|
+
// problem 2: store.init() to re-add the old key should succeed, but
|
|
577
|
+
// incorrectly fails (because the store thinks the key is already
|
|
578
|
+
// present)
|
|
579
|
+
|
|
580
|
+
result = undefined;
|
|
581
|
+
await dispatch(makeMessage(rootA, 'store'));
|
|
582
|
+
t.is(result, 'ok');
|
|
583
|
+
|
|
584
|
+
// other likely problems: store.keys() will report the old key,
|
|
585
|
+
// store.get(oldkey) will probably crash
|
|
586
|
+
});
|
package/test/collections.test.js
CHANGED
|
@@ -145,6 +145,14 @@ function exerciseMapOperations(t, collectionName, testStore) {
|
|
|
145
145
|
`key "[Alleged: something missing]" not found in collection "${collectionName}"`,
|
|
146
146
|
),
|
|
147
147
|
);
|
|
148
|
+
if (collectionName === 'map') {
|
|
149
|
+
// strong map, so we can .clear
|
|
150
|
+
testStore.clear();
|
|
151
|
+
for (const [key, _value] of stuff) {
|
|
152
|
+
t.false(testStore.has(key));
|
|
153
|
+
}
|
|
154
|
+
fillBasicMapStore(testStore);
|
|
155
|
+
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
function exerciseSetOperations(t, collectionName, testStore) {
|
|
@@ -172,6 +180,14 @@ function exerciseSetOperations(t, collectionName, testStore) {
|
|
|
172
180
|
`key "[Alleged: something missing]" not found in collection "${collectionName}"`,
|
|
173
181
|
),
|
|
174
182
|
);
|
|
183
|
+
if (collectionName === 'set') {
|
|
184
|
+
// strong set, so we can .clear
|
|
185
|
+
testStore.clear();
|
|
186
|
+
for (const [key, _value] of stuff) {
|
|
187
|
+
t.false(testStore.has(key));
|
|
188
|
+
}
|
|
189
|
+
fillBasicSetStore(testStore);
|
|
190
|
+
}
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
test('basic map operations', t => {
|
|
@@ -487,6 +503,19 @@ test('map clear', t => {
|
|
|
487
503
|
t.is(testStore.getSize(), 0);
|
|
488
504
|
});
|
|
489
505
|
|
|
506
|
+
test('map clear with pattern', t => {
|
|
507
|
+
const testStore = makeScalarBigMapStore('cmap', { keyShape: M.any() });
|
|
508
|
+
testStore.init('a', 'ax');
|
|
509
|
+
testStore.init('b', 'bx');
|
|
510
|
+
testStore.init('c', 'cx');
|
|
511
|
+
console.log(`M is`, M);
|
|
512
|
+
testStore.clear(M.eq('c'));
|
|
513
|
+
t.true(testStore.has('a'));
|
|
514
|
+
t.true(testStore.has('b'));
|
|
515
|
+
t.false(testStore.has('c'));
|
|
516
|
+
t.is(testStore.getSize(), 2);
|
|
517
|
+
});
|
|
518
|
+
|
|
490
519
|
test('set clear', t => {
|
|
491
520
|
const testStore = makeScalarBigSetStore('cset', { keyShape: M.any() });
|
|
492
521
|
testStore.add('a');
|
|
@@ -499,6 +528,18 @@ test('set clear', t => {
|
|
|
499
528
|
t.is(testStore.getSize(), 0);
|
|
500
529
|
});
|
|
501
530
|
|
|
531
|
+
test('set clear with pattern', t => {
|
|
532
|
+
const testStore = makeScalarBigSetStore('cset', { keyShape: M.any() });
|
|
533
|
+
testStore.add('a');
|
|
534
|
+
testStore.add('b');
|
|
535
|
+
testStore.add('c');
|
|
536
|
+
testStore.clear(M.eq('c'));
|
|
537
|
+
t.true(testStore.has('a'));
|
|
538
|
+
t.true(testStore.has('b'));
|
|
539
|
+
t.false(testStore.has('c'));
|
|
540
|
+
t.is(testStore.getSize(), 2);
|
|
541
|
+
});
|
|
542
|
+
|
|
502
543
|
test('map fail on concurrent modification', t => {
|
|
503
544
|
const primeMap = makeScalarBigMapStore('fmap', {
|
|
504
545
|
keyShape: M.number(),
|