Skip to content

Commit 8383fc3

Browse files
authored
feat(kernel): Add removable bootstrap keep‑alive pin (#502)
Closes #494 This PR implements a `pin` and an `unpin` method for vat root objects, that external code (tests, control panel) can call to keep a vat or arbitrary kernel object alive to ensure that references are counted. A small reorganize of the kernel store methods was required and I added some tests that were missing.
1 parent 16276d7 commit 8383fc3

21 files changed

+1116
-356
lines changed

packages/kernel-test/src/garbage-collection.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ describe('Garbage Collection', () => {
6363
importerVatId = vats.find(
6464
(rows) => rows.config.parameters?.name === 'Importer',
6565
)?.id as VatId;
66-
exporterKRef = kernelStore.erefToKref(exporterVatId, 'o+0') as KRef;
67-
importerKRef = kernelStore.erefToKref(importerVatId, 'o+0') as KRef;
66+
exporterKRef = kernelStore.getRootObject(exporterVatId) as KRef;
67+
importerKRef = kernelStore.getRootObject(importerVatId) as KRef;
6868
});
6969

7070
it('objects are tracked with reference counts', async () => {

packages/kernel/src/Kernel.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,34 @@ export class Kernel {
420420
}
421421
}
422422

423+
/**
424+
* Pin a vat root.
425+
*
426+
* @param vatId - The ID of the vat.
427+
* @returns The KRef of the vat root.
428+
*/
429+
pinVatRoot(vatId: VatId): KRef {
430+
const kref = this.#kernelStore.getRootObject(vatId);
431+
if (!kref) {
432+
throw new VatNotFoundError(vatId);
433+
}
434+
this.#kernelStore.pinObject(kref);
435+
return kref;
436+
}
437+
438+
/**
439+
* Unpin a vat root.
440+
*
441+
* @param vatId - The ID of the vat.
442+
*/
443+
unpinVatRoot(vatId: VatId): void {
444+
const kref = this.#kernelStore.getRootObject(vatId);
445+
if (!kref) {
446+
throw new VatNotFoundError(vatId);
447+
}
448+
this.#kernelStore.unpinObject(kref);
449+
}
450+
423451
/**
424452
* Reset the kernel state.
425453
* This is for debugging purposes only.

packages/kernel/src/store/index.test.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
1-
import type { Message } from '@agoric/swingset-liveslots';
21
import type { KernelDatabase } from '@ocap/store';
32
import { describe, it, expect, beforeEach } from 'vitest';
43

54
import { makeKernelStore } from './index.ts';
65
import { makeMapKernelDatabase } from '../../test/storage.ts';
76
import type { RunQueueItem } from '../types.ts';
87

9-
/**
10-
* Mock Message: A stupid TS hack to allow trivial use of plain strings as if they
11-
* were Messages, since, for testing purposes here, all that's necessary to be a
12-
* "message" is to be stringifiable.
13-
*
14-
* @param str - A string.
15-
* @returns The same string coerced to type Message.
16-
*/
17-
function mm(str: string): Message {
18-
return str as unknown as Message;
19-
}
20-
218
/**
229
* Mock RunQueueItem: A stupid TS hack to allow trivial use of plain strings
2310
* as if they were RunQueueItems, since, for testing purposes here, all
@@ -89,11 +76,13 @@ describe('kernel store', () => {
8976
'getNextVatId',
9077
'getObjectRefCount',
9178
'getOwner',
79+
'getPinnedObjects',
9280
'getPromisesByDecider',
9381
'getQueueLength',
9482
'getReachableAndVatSlot',
9583
'getReachableFlag',
9684
'getRefCount',
85+
'getRootObject',
9786
'getTerminatedVats',
9887
'getVatConfig',
9988
'getVatIDs',
@@ -104,6 +93,7 @@ describe('kernel store', () => {
10493
'initEndpoint',
10594
'initKernelObject',
10695
'initKernelPromise',
96+
'isObjectPinned',
10797
'isRootObject',
10898
'isVatTerminated',
10999
'kernelRefExists',
@@ -114,7 +104,7 @@ describe('kernel store', () => {
114104
'markVatAsTerminated',
115105
'nextReapAction',
116106
'nextTerminatedVatCleanup',
117-
'refCountKey',
107+
'pinObject',
118108
'reset',
119109
'resolveKernelPromise',
120110
'retireKernelObjects',
@@ -128,6 +118,7 @@ describe('kernel store', () => {
128118
'translateMessageKtoV',
129119
'translateRefKtoV',
130120
'translateSyscallVtoK',
121+
'unpinObject',
131122
]);
132123
});
133124
});
@@ -208,14 +199,34 @@ describe('kernel store', () => {
208199
expect(ks.initKernelPromise()).toStrictEqual(['kp2', kp2]);
209200
expect(ks.getKernelPromise('kp1')).toStrictEqual(kp1);
210201
expect(ks.getKernelPromise('kp2')).toStrictEqual(kp2);
211-
ks.enqueuePromiseMessage('kp1', mm('first message to kp1'));
212-
ks.enqueuePromiseMessage('kp1', mm('second message to kp1'));
202+
const msg1 = {
203+
methargs: {
204+
body: 'first message to kp1',
205+
slots: [],
206+
},
207+
result: null,
208+
};
209+
ks.enqueuePromiseMessage('kp1', msg1);
210+
const msg2 = {
211+
methargs: {
212+
body: 'second message to kp1',
213+
slots: [],
214+
},
215+
result: null,
216+
};
217+
ks.enqueuePromiseMessage('kp1', msg2);
213218
expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([
214-
'first message to kp1',
215-
'second message to kp1',
219+
msg1,
220+
msg2,
216221
]);
217222
expect(ks.getKernelPromiseMessageQueue('kp1')).toStrictEqual([]);
218-
ks.enqueuePromiseMessage('kp1', mm('sacrificial message'));
223+
ks.enqueuePromiseMessage('kp1', {
224+
methargs: {
225+
body: 'sacrificial message',
226+
slots: [],
227+
},
228+
result: null,
229+
});
219230
ks.deleteKernelPromise('kp1');
220231
expect(() => ks.getKernelPromise('kp1')).toThrow(
221232
'unknown kernel promise kp1',

packages/kernel/src/store/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { getCListMethods } from './methods/clist.ts';
6363
import { getGCMethods } from './methods/gc.ts';
6464
import { getIdMethods } from './methods/id.ts';
6565
import { getObjectMethods } from './methods/object.ts';
66+
import { getPinMethods } from './methods/pinned.ts';
6667
import { getPromiseMethods } from './methods/promise.ts';
6768
import { getQueueMethods } from './methods/queue.ts';
6869
import { getReachableMethods } from './methods/reachable.ts';
@@ -135,6 +136,8 @@ export function makeKernelStore(kdb: KernelDatabase) {
135136
const vat = getVatMethods(context);
136137
const reachable = getReachableMethods(context);
137138
const translators = getTranslators(context);
139+
const pinned = getPinMethods(context);
140+
138141
/**
139142
* Create a new VatStore for a vat.
140143
*
@@ -190,6 +193,7 @@ export function makeKernelStore(kdb: KernelDatabase) {
190193
...cList,
191194
...vat,
192195
...translators,
196+
...pinned,
193197
makeVatStore,
194198
deleteVat,
195199
clear,

packages/kernel/src/store/methods/base.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@ describe('base-methods', () => {
2020
});
2121
});
2222

23+
describe('refCountKey', () => {
24+
it('generates correct reference count keys', () => {
25+
expect(baseStore.refCountKey('ko1')).toBe('ko1.refCount');
26+
expect(baseStore.refCountKey('kp42')).toBe('kp42.refCount');
27+
expect(baseStore.refCountKey('v7')).toBe('v7.refCount');
28+
});
29+
});
30+
31+
describe('getOwnerKey', () => {
32+
it('generates correct owner keys', () => {
33+
expect(baseStore.getOwnerKey('ko1')).toBe('ko1.owner');
34+
expect(baseStore.getOwnerKey('kp42')).toBe('kp42.owner');
35+
expect(baseStore.getOwnerKey('v7')).toBe('v7.owner');
36+
});
37+
});
38+
2339
describe('incCounter', () => {
2440
it('increments a stored counter value', () => {
2541
// Create a stored value to increment
@@ -159,6 +175,43 @@ describe('base-methods', () => {
159175
});
160176

161177
describe('provideStoredQueue', () => {
178+
it('creates a queue with cache when cached=true', () => {
179+
const cachedQueue = baseStore.provideStoredQueue('cached-queue', true);
180+
cachedQueue.enqueue({ id: 1 });
181+
expect(kv.get('queue.cached-queue.head')).toBe('2');
182+
cachedQueue.delete();
183+
expect(kv.get('queue.cached-queue.head')).toBeUndefined();
184+
});
185+
186+
it('creates a queue without cache when cached=false', () => {
187+
const rawQueue = baseStore.provideStoredQueue('raw-queue', false);
188+
rawQueue.enqueue({ id: 1 });
189+
expect(kv.get('queue.raw-queue.head')).toBe('2');
190+
rawQueue.delete();
191+
expect(kv.get('queue.raw-queue.head')).toBeUndefined();
192+
});
193+
194+
it('throws if queue is not initialized properly', () => {
195+
kv.set('queue.broken-queue.head', '1');
196+
const originalSet = kv.set;
197+
vi.spyOn(kv, 'set').mockImplementation((key, value) => {
198+
if (key !== 'queue.broken-queue.tail') {
199+
return originalSet.call(kv, key, value);
200+
}
201+
return undefined;
202+
});
203+
expect(() => baseStore.provideStoredQueue('broken-queue')).toThrow(
204+
'queue broken-queue not initialized',
205+
);
206+
});
207+
208+
it('returns undefined when dequeueing from a deleted queue', () => {
209+
const queue = baseStore.provideStoredQueue('test-dequeue-deleted');
210+
queue.enqueue({ id: 'test' });
211+
kv.delete('queue.test-dequeue-deleted.head');
212+
expect(queue.dequeue()).toBeUndefined();
213+
});
214+
162215
it('throws when enqueueing into a deleted queue', () => {
163216
// Create a queue properly
164217
const queue = baseStore.provideStoredQueue('test', false);

packages/kernel/src/store/methods/base.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ export function getBaseMethods(kv: KVStore) {
2222
return `${endpointId}.c.${kref}`;
2323
}
2424

25+
/**
26+
* Generate the storage key for a kernel entity's reference count.
27+
*
28+
* @param kref - The KRef of interest.
29+
* @returns the key to store the indicated reference count at.
30+
*/
31+
function refCountKey(kref: KRef): string {
32+
return `${kref}.refCount`;
33+
}
34+
35+
/**
36+
* Generate the storage key for a kernel entity's owner.
37+
*
38+
* @param kref - The KRef of interest.
39+
* @returns the key to store the indicated owner at.
40+
*/
41+
function getOwnerKey(kref: KRef): string {
42+
return `${kref}.owner`;
43+
}
44+
2545
/**
2646
* Increment the value of a persistently stored counter.
2747
*
@@ -176,6 +196,8 @@ export function getBaseMethods(kv: KVStore) {
176196

177197
return {
178198
getSlotKey,
199+
refCountKey,
200+
getOwnerKey,
179201
incCounter,
180202
provideCachedStoredValue,
181203
provideRawStoredValue,

0 commit comments

Comments
 (0)