-
Notifications
You must be signed in to change notification settings - Fork 214
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(liveslots): collection deletion didn't free key objects
Fix collection deletion. Previously, objects (passables/vrefs) used as collection keys were not de-referenced when the collection was dropped/deleted or unconditionally cleared (`collection.clear()` without keyPatt or valuePatt). Strong collections would leak a reachable-refcount to the key object, and weak collections would leak a recognizable-refcount, leading to the object being kept alive (or kept unretired) forever. In addition, each entry would leak a vatstore key until the collection was finally deleted. The code in `clearInternalFull()` confused dbKeys, encodedKeys, and vrefs, and gave the wrong kind of key to `isEncodedRemotable()`, so the answer was always "no". fixes #8756
- Loading branch information
Showing
2 changed files
with
214 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
packages/swingset-liveslots/test/storeGC/test-clear-collection.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
// @ts-nocheck | ||
import test from 'ava'; | ||
|
||
import { Far } from '@endo/marshal'; | ||
import { kser, kslot } from '@agoric/kmarshal'; | ||
import { makeLiveSlots } from '../../src/liveslots.js'; | ||
import { buildSyscall } from '../liveslots-helpers.js'; | ||
import { makeMessage, makeStartVat, makeBringOutYourDead } from '../util.js'; | ||
import { makeMockGC } from '../mock-gc.js'; | ||
|
||
// When a virtual collection is the only reference to a virtual | ||
// object, collection.clear() should let them be deleted. Bug #8756 | ||
// caused them to be retained by mistake. | ||
|
||
test('collection.clear() deletes contents', async t => { | ||
const { syscall, log } = buildSyscall(); | ||
const gcTools = makeMockGC(); | ||
const COUNT = 5; | ||
|
||
function build(vatPowers) { | ||
const { defineKind, makeScalarBigSetStore } = vatPowers.VatData; | ||
const make = defineKind('target', () => ({}), {}); | ||
const holder = makeScalarBigSetStore('holder'); | ||
const root = Far('root', { | ||
create() { | ||
for (let i = 0; i < COUNT; i += 1) { | ||
// vrefs are all `o+v10/${N}`, N=1..10 | ||
const target = make(); | ||
holder.add(target); | ||
// we immediately delete the presence, but the finalizers | ||
// won't run until gcTools.flushAllFRs() | ||
gcTools.kill(target); | ||
} | ||
}, | ||
clear() { | ||
holder.clear(); | ||
}, | ||
}); | ||
return root; | ||
} | ||
|
||
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({ | ||
buildRootObject: build, | ||
})); | ||
const { dispatch } = ls; | ||
await dispatch(makeStartVat(kser())); | ||
log.length = 0; | ||
|
||
const rootA = 'o+0'; | ||
|
||
await dispatch(makeMessage(rootA, 'create', [])); | ||
log.length = 0; | ||
|
||
// Collect the representatives, leaving only the virtual-data | ||
// pillar. This BOYD finds non-zero virtual-data refcounts for all | ||
// five VOs, so they are not deleted. | ||
gcTools.flushAllFRs(); | ||
const boyd1 = await dispatch(makeBringOutYourDead()); | ||
t.is(boyd1.possiblyDeadSet, 0); | ||
t.is(boyd1.possiblyRetiredSet, 0); | ||
log.length = 0; | ||
|
||
// clearing the collections should delete both the data key and the | ||
// ordinal key for each entry, but it won't delete the values, that | ||
// is deferred until BOYD | ||
await dispatch(makeMessage(rootA, 'clear', [])); | ||
|
||
// The .clear() will do an initial get(vc.5.) to start the iterator, | ||
// then for each collection entry it will getNextKey()/get(), then a | ||
// delete() of the data row, a get()+delete() of the refcount, and a | ||
// delete() of the ordinal row (vc.5|o+v10/1). Then we'll see an | ||
// extra getNextKey() (which stops the iteration). At the end of the | ||
// loop, it will write out the new "|entryCount" value with one last | ||
// set(). Total: 1*COUNT+1 getNextKey, 2*COUNT get, 3*COUNT delete, | ||
// plus 1 set. | ||
t.is(log.filter(l => l.type === 'vatstoreGetNextKey').length, 1 * COUNT + 1); | ||
t.is(log.filter(l => l.type === 'vatstoreGet').length, 2 * COUNT + 1); | ||
t.is(log.filter(l => l.type === 'vatstoreDelete').length, 3 * COUNT); | ||
t.is(log.filter(l => l.type === 'vatstoreSet').length, 1); | ||
t.is(log.length, 6 * COUNT + 3); | ||
log.length = 0; | ||
|
||
// this ought to delete the VOs. bug #8756 | ||
await dispatch(makeBringOutYourDead()); | ||
|
||
// We expect to see the objects get deleted. The scanForDeadObjects | ||
// does a get(rc)/get(es) on each to decide the object is really | ||
// dead. Then deleting the objects does a get(vom) to see what | ||
// references need to be decremented, then a redundant | ||
// get(rc)/get(es), and a delete(rc)/delete(es) (redundant, in this | ||
// case, since the RAM pillar was the only one left, and rc/es keys | ||
// didn't exist). A delete(vom) is emitted for each, but held in the | ||
// data cache until end of crank. Then the retirement check does a | ||
// getNextKey(ir) scan. | ||
// | ||
// Total: 5*COUNT get, 1*COUNT getNextKey, 3*COUNT delete | ||
t.is(log.filter(l => l.type === 'vatstoreGet').length, 5 * COUNT); | ||
t.is(log.filter(l => l.type === 'vatstoreSet').length, 0 * COUNT); | ||
t.is(log.filter(l => l.type === 'vatstoreGetNextKey').length, 1 * COUNT); | ||
t.is(log.filter(l => l.type === 'vatstoreDelete').length, 3 * COUNT); | ||
t.is(log.length, 9 * COUNT); | ||
}); | ||
|
||
test('weak collection deletion will retire contents', async t => { | ||
const { syscall, log } = buildSyscall(); | ||
const gcTools = makeMockGC(); | ||
const COUNT = 5; | ||
const allVrefs = []; | ||
const allKslots = []; | ||
for (let i = 0; i < COUNT; i += 1) { | ||
const vref = `o-${i + 1}`; | ||
allVrefs.push(vref); | ||
allKslots.push(kslot(vref, 'imported')); | ||
} | ||
|
||
// Import a bunch of Presences and hold them in a weakset. Drop the | ||
// imports, but retain recognition, until we drop the weakset, which | ||
// should delete the collection and retire the keys. | ||
function build(vatPowers) { | ||
const { makeScalarBigWeakSetStore } = vatPowers.VatData; | ||
const recognizer = makeScalarBigWeakSetStore('recognizer'); | ||
const root = Far('root', { | ||
create(presences) { | ||
for (const p of presences) { | ||
recognizer.add(p); | ||
// we immediately delete the presence, but the finalizers | ||
// won't run until gcTools.flushAllFRs() | ||
gcTools.kill(p); | ||
} | ||
}, | ||
drop() { | ||
gcTools.kill(recognizer); | ||
}, | ||
}); | ||
return root; | ||
} | ||
|
||
const ls = makeLiveSlots(syscall, 'vatA', {}, {}, gcTools, undefined, () => ({ | ||
buildRootObject: build, | ||
})); | ||
const { dispatch } = ls; | ||
await dispatch(makeStartVat(kser())); | ||
log.length = 0; | ||
|
||
const rootA = 'o+0'; | ||
|
||
await dispatch(makeMessage(rootA, 'create', [allKslots])); | ||
log.length = 0; | ||
|
||
// Collect the representatives, leaving only the virtual-data | ||
// pillar. This BOYD finds non-zero virtual-data refcounts for all | ||
// five VOs, so they are not deleted. | ||
gcTools.flushAllFRs(); | ||
await dispatch(makeBringOutYourDead()); | ||
t.deepEqual(log.at(-1), { type: 'dropImports', slots: allVrefs }); | ||
log.length = 0; | ||
|
||
// dropping the collection makes it UNREACHABLE but it won't be | ||
// COLLECTED until BOYD | ||
await dispatch(makeMessage(rootA, 'drop', [])); | ||
t.is(log.length, 0); | ||
|
||
// this will delete the collection and should retire the imports | ||
gcTools.flushAllFRs(); | ||
await dispatch(makeBringOutYourDead()); | ||
|
||
// Collection deletion starts with scanForDeadObjects doing a | ||
// get(rc)+get(es) on the collection vref, to decide if it's really | ||
// dead. Then, the deletion phase does another get(rc)+get(es), then | ||
// iterates over rows. This starts with the weird get(vc.5.), then | ||
// for each entry, we see getNextKey(), get+delete of the data row, | ||
// delete(ir), and delete(reverse-mapping). One more getNextKey() | ||
// ends the iteration. Then we get(schemata) to see if the | ||
// keyshape/valueshape has references that need deletion. Then we | ||
// find and delete metadata: | ||
|
||
// * getNextKey, delete(|nextOrdinal) | ||
// * getNextKey (deletion of |schemata is deferred) | ||
// * getNextKey ends the iteration | ||
// Then delete(rc)+delete(es) for the collection vref, even though | ||
// they didn't exist. Then getNextKey to scan for ir records | ||
// (none). Then finally delete(|schemata) | ||
// | ||
// Total: 5+1*COUNT+1 get, 1*COUNT+1+3+1 getNextKey, 3*COUNT+4 delete | ||
|
||
t.is(log.filter(l => l.type === 'vatstoreGetNextKey').length, COUNT + 5); | ||
t.is(log.filter(l => l.type === 'vatstoreGet').length, COUNT + 6); | ||
t.is(log.filter(l => l.type === 'vatstoreDelete').length, 3 * COUNT + 4); | ||
t.is(log.filter(l => l.type === 'vatstoreSet').length, 0); | ||
t.is(log.length, 5 * COUNT + 15); | ||
log.length = 0; | ||
}); |