Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
893b09f
test: pin the transaction invariants #1005 left broken
grypez Aug 6, 2026
0493967
fix: keep a crank's store work inside one transaction
sirtimid Aug 6, 2026
5e6cf5d
test(kernel-test): reap until the vat's GC is visible, not three times
sirtimid Aug 6, 2026
272f920
test: fix rollback crank test
sirtimid Aug 6, 2026
eeb5652
fix(ocap-kernel): forget every savepoint when a crank rollback fails
sirtimid Aug 6, 2026
d9971b0
fix(kernel-store): log an abort that fails while discarding a transac…
sirtimid Aug 6, 2026
48d46c6
test(ocap-kernel): pin the flush's ordering against a failing enqueue
sirtimid Aug 6, 2026
dc8d1de
docs: correct the transaction claims review found wrong
sirtimid Aug 6, 2026
dc6eef4
docs: cut the padding from this branch's comments and changelogs
sirtimid Aug 6, 2026
323dd18
fix(ocap-kernel): revert cached values and GC candidates on crank rol…
sirtimid Aug 10, 2026
6c28b96
test(kernel-store): pin the failed COMMIT that wedges `_inTx`
grypez Aug 10, 2026
3d3fe3e
test(ocap-kernel): pin the endCrank failure that buries the real error
grypez Aug 10, 2026
f1e0ed0
test(kernel-node-runtime): pin the kernel store's missing logger
grypez Aug 10, 2026
c10b5b9
test(ocap-kernel): pin the release failure lost at the remote savepoint
grypez Aug 10, 2026
7ed40cd
test: tighten the four repros after review
grypez Aug 10, 2026
afc664f
fix(kernel-store): clear `_inTx` before the COMMIT, not after
sirtimid Aug 13, 2026
10b042a
fix(ocap-kernel): stop `endCrank` burying the error that killed the r…
sirtimid Aug 13, 2026
5470554
fix(ocap-kernel): report the remote release failure, not a missing sa…
sirtimid Aug 13, 2026
726d063
test(ocap-kernel): pin the in-memory revert against a failed crank ro…
sirtimid Aug 13, 2026
daae217
fix(runtimes): give the kernel store a logger
sirtimid Aug 13, 2026
8e0676b
chore: cite this PR in the changelogs
sirtimid Aug 13, 2026
2724dff
test(kernel-store): pin the transaction a failed COMMIT abandons
sirtimid Aug 20, 2026
eab4681
fix(kernel-store): discard the transaction a failed COMMIT leaves open
sirtimid Aug 20, 2026
cf07c28
docs: trim changelog entries and comments
sirtimid Aug 27, 2026
d8318d5
docs: cut the leftover narration from this branch's tests
sirtimid Aug 27, 2026
29744e4
test(kernel-store): pin the transaction a failed COMMIT leaves open
grypez Sep 7, 2026
198380a
fix(kernel-store): refuse to commit a transaction an abort could not end
sirtimid Sep 7, 2026
64ffca1
test(ocap-kernel): pin the audit that runs after the crank answers it…
grypez Sep 7, 2026
be4f406
fix(ocap-kernel): audit reference counts after the crank commits, not…
sirtimid Sep 7, 2026
775636b
test(kernel-store): pin the crank commit point against a foreign save…
grypez Sep 7, 2026
5e7a314
fix(ocap-kernel): keep a crank and an out-of-crank savepoint from ove…
sirtimid Sep 7, 2026
19689c6
test(kernel-test): reap until the shared import is released, not thre…
sirtimid Sep 7, 2026
9b4717f
docs: changelog entries for the driver, audit, and savepoint fixes
sirtimid Sep 7, 2026
7579669
fix(kernel-store): refuse every write while a transaction is abandone…
sirtimid Sep 8, 2026
7694758
fix(ocap-kernel): let `stop` release the workers when the store refus…
sirtimid Sep 8, 2026
99eeb9c
chore: point this PR's changelog links at the repo's new org
sirtimid Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kernel-browser-runtime/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- The kernel worker gives the kernel store a logger, so the wasm SQLite driver's diagnostics reach the log rather than nowhere ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- Process platform-services RPC request handlers in the background so a request handler that fires a reentrant outbound RPC (e.g. transport handshake calling back into the kernel) cannot deadlock waiting for its response ([#948](https://github.com/MetaMask/ocap-kernel/pull/948))

## [0.6.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ async function main(): Promise<void> {
isJsonRpcMessage,
),
PlatformServicesClient.make(globalThis as PostMessageTarget),
makeSQLKernelDatabase({ dbFilename: DB_FILENAME }),
makeSQLKernelDatabase({
dbFilename: DB_FILENAME,
logger: logger.subLogger({ tags: ['kernel-store'] }),
}),
]);

setupConsoleForwarding({
Expand Down
1 change: 1 addition & 0 deletions packages/kernel-node-runtime/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `makeKernel` gives the kernel store a logger, so the SQLite driver's diagnostics reach the log rather than nowhere ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- The RPC socket server refuses to bind a Unix socket that has a live listener, rather than unlinking it and orphaning the previous owner; stale socket files with no listener are still cleaned up automatically ([#952](https://github.com/MetaMask/ocap-kernel/pull/952))

## [0.1.0]
Expand Down
12 changes: 11 additions & 1 deletion packages/kernel-node-runtime/src/kernel/make-kernel.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { Logger } from '@metamask/logger';
import { Kernel } from '@metamask/ocap-kernel';
import { describe, expect, it, vi } from 'vitest';

Expand All @@ -8,7 +10,7 @@ vi.mock('@metamask/kernel-store/sqlite/nodejs', async () => {
'../../../ocap-kernel/test/storage.ts'
);
return {
makeSQLKernelDatabase: makeMapKernelDatabase,
makeSQLKernelDatabase: vi.fn(makeMapKernelDatabase),
};
});

Expand All @@ -18,4 +20,12 @@ describe('makeKernel', () => {

expect(kernel).toBeInstanceOf(Kernel);
});

it('gives the kernel store a logger', async () => {
await makeKernel({});

expect(vi.mocked(makeSQLKernelDatabase)).toHaveBeenCalledWith(
expect.objectContaining({ logger: expect.any(Logger) }),
);
});
});
5 changes: 4 additions & 1 deletion packages/kernel-node-runtime/src/kernel/make-kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ export async function makeKernel({
});

// Initialize kernel store.
const kernelDatabase = await makeSQLKernelDatabase({ dbFilename });
const kernelDatabase = await makeSQLKernelDatabase({
dbFilename,
logger: rootLogger.subLogger({ tags: ['kernel-store'] }),
});

// Create and start kernel.
const kernel = await Kernel.make(platformServicesClient, kernelDatabase, {
Expand Down
11 changes: 11 additions & 0 deletions packages/kernel-store/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `rollbackSavepoint` discards the enclosing transaction when `ROLLBACK TO` itself fails, instead of leaving the savepoint on its stack and the transaction open ([#1005](https://github.com/MetaMask/ocap-kernel/pull/1005))
- Nothing would ever commit or abort that transaction, so every later write on the connection silently joined it, reported success, and vanished on close. Discarding it is no wider than the caller asked for: the transaction begins with the outermost savepoint, so it holds only the work the rollback was abandoning
- The rollback failure is still what gets thrown, even if aborting the transaction fails too
- `releaseSavepoint` discards the enclosing transaction when `RELEASE` fails, as `rollbackSavepoint` already did for `ROLLBACK TO`; the release failure is still what gets thrown ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- The wasm driver no longer considers itself in a transaction after a commit or abort throws, and discards the transaction a failed `COMMIT` leaves open ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- Otherwise later writes autocommitted — including the next savepoint, created bare, where `RELEASE` commits and no rollback can undo the work. The nodejs driver reads `db.inTransaction` and was never affected by the wedged flag
- The nodejs driver discards the transaction a failed `COMMIT` leaves open, as the wasm driver already did ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- `releaseSavepoint` reaches `commitIfNeeded` outside any try of its own, so a throwing `COMMIT` left the transaction open with nothing to end it. Reading `db.inTransaction` prevents a wedged flag but does not end an unowned transaction
- Neither driver commits a transaction whose discarding abort failed ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- The savepoint list was emptied while SQLite still held the transaction, so the next `createSavepoint` skipped `BEGIN`, nested inside it, and committed the abandoned crank on release. Both drivers now retry the abort before beginning, and abort rather than commit while one is outstanding
- Every write refuses while a transaction the drivers could not abort is outstanding, not just the ones that pass through `beginIfNeeded` or `commitIfNeeded` ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))
- `kernelKVStore.set`, `kernelKVStore.delete`, `clear`, `deleteVatStore`, the savepoint operations, and the nodejs driver's vatstore update joined the abandoned transaction and reported success. Teardown such as `reset` looked like it had landed, and the writes went with the transaction on close
- `executeQuery` stays exempt: its callers are debug surfaces, which would not expect a query to roll anything back
- Both drivers log an abort that fails while recovering from a failed savepoint operation ([#1021](https://github.com/Consensys-Incorporated/ocap-kernel/pull/1021))

## [0.6.0]

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';

import { makeSQLKernelDatabase } from './nodejs.ts';

/**
* Why a crank and a savepoint of someone else's must never overlap.
*
* `KernelQueue.#runLoop` calls releasing its `crank` savepoint "this crank's one
* commit point". `releaseAllSavepoints` releases `t0`, which is the outermost
* savepoint only if the crank opened the first one, and `KernelStore`'s own
* `createSavepoint` bypasses `ctx.savepoints` and so is invisible to the ordinal
* numbering. Two production paths use it: `RemoteHandle.handleRemoteMessage` and
* `RemoteManager.handleIncarnationChange`.
*
* Real SQLite through the real driver, one test per interleaving. Each records
* what SQLite actually does, which is the reason the kernel now serializes the
* two: `createSavepoint` refuses inside a crank, `startCrank` refuses while a
* caller holds the store outside one, and callers take their turn through
* `beginOutOfCrank`. See `crank.out-of-crank.test.ts` for the enforcement.
*/
describe('a savepoint the crank does not know about', () => {
it('outside the crank, leaves the crank release with nothing to commit', async () => {
const kdb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
const kv = kdb.kernelKVStore;

// RemoteHandle.handleRemoteMessage, parked on its await.
kdb.createSavepoint('receive_r1_7');

// The run loop wakes: startCrank, then the two crank savepoints.
kdb.createSavepoint('t0');
kdb.createSavepoint('t1');
kv.set('crankWrite', 'durable');

// endCrank -> releaseAllSavepoints -> releaseSavepoint('t0'). `t0` is not the
// outermost savepoint, so this releases into the remote's, not to a commit.
kdb.releaseSavepoint('t0');

// The remote message then fails, so RemoteHandle rolls its savepoint back,
// and takes the whole committed-looking crank with it.
kdb.rollbackSavepoint('receive_r1_7');

expect(kv.get('crankWrite')).toBeUndefined();
kdb.close();
});

it('inside the crank, is destroyed by the delivery rollback behind its owner', async () => {
const kdb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
const kv = kdb.kernelKVStore;

// startCrank
kdb.createSavepoint('t0');
kdb.createSavepoint('t1');

// A remote message arrives during `await deliver(queueItem)`.
kdb.createSavepoint('receive_r1_7');
kv.set('remoteSeq.r1.highestReceivedSeq', '7');

// The delivery aborts: rollbackCrank('delivery') issues ROLLBACK TO t1, and
// SQLite cancels every savepoint started after t1 -- including the remote's.
kdb.rollbackSavepoint('t1');

// The remote handler, still inside its own try, reaches its release and
// finds the savepoint gone along with everything it wrote.
expect(() => kdb.releaseSavepoint('receive_r1_7')).toThrow(
'No such savepoint: receive_r1_7',
);
expect(kv.get('remoteSeq.r1.highestReceivedSeq')).toBeUndefined();
kdb.close();
});

it('inside a crank that succeeds, is committed under its owner', async () => {
const kdb = await makeSQLKernelDatabase({ dbFilename: ':memory:' });
const kv = kdb.kernelKVStore;

// A remote message arrives and gets as far as its await, so the seq row it
// writes "at the end, within the transaction" is not written yet.
kdb.createSavepoint('t0');
kdb.createSavepoint('t1');
kdb.createSavepoint('receive_r1_7');
kv.set('remoteHalfDone', 'yes');

// endCrank releases t0, and with it everything stacked above.
kdb.releaseSavepoint('t0');

// The remote handler resumes to find its savepoint gone -- and its
// half-finished work durable regardless, so it reports a failure for an
// effect that has landed and the peer retries it.
expect(() => kdb.releaseSavepoint('receive_r1_7')).toThrow(
'No such savepoint: receive_r1_7',
);
expect(kv.get('remoteHalfDone')).toBe('yes');
kdb.close();
});
});
39 changes: 38 additions & 1 deletion packages/kernel-store/src/sqlite/nodejs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,6 @@ describe('makeSQLKernelDatabase', () => {
);

expect(mockDb._spStack).toStrictEqual([]);
// The abort is the only prepared statement this path runs.
expect(mockStatement.run).toHaveBeenCalledOnce();
mockDb.inTransaction = false;
});
Expand Down Expand Up @@ -360,6 +359,44 @@ describe('makeSQLKernelDatabase', () => {
expect(mockDb._spStack).toStrictEqual([]);
});

it('releaseSavepoint discards the transaction when the release fails', async () => {
const db = await makeSQLKernelDatabase({});
mockDb.inTransaction = true;
mockDb._spStack = ['point1'];
mockStatement.run.mockClear();
mockDb.exec.mockImplementationOnce(() => {
throw new Error('disk I/O error');
});

expect(() => db.releaseSavepoint('point1')).toThrowError(
'disk I/O error',
);

expect(mockDb._spStack).toStrictEqual([]);
// The abort is the only prepared statement this path runs.
expect(mockStatement.run).toHaveBeenCalledOnce();
mockDb.inTransaction = false;
});

it('releaseSavepoint reports the release failure even if the abort fails too', async () => {
const db = await makeSQLKernelDatabase({});
mockDb.inTransaction = true;
mockDb._spStack = ['point1'];
mockDb.exec.mockImplementationOnce(() => {
throw new Error('disk I/O error');
});
mockStatement.run.mockImplementationOnce(() => {
throw new Error('cannot rollback');
});

expect(() => db.releaseSavepoint('point1')).toThrowError(
'disk I/O error',
);

expect(mockDb._spStack).toStrictEqual([]);
mockDb.inTransaction = false;
});

it('supports nested savepoints', async () => {
const db = await makeSQLKernelDatabase({});
db.createSavepoint('outer');
Expand Down
Loading
Loading