Tag prefetched copies with the controller's run token

If the controller crashes after sending generation N but before saving it, the destination keeps that prefetch while the controller restarts at N-1, and another instance can then upload different data as generation N. The controller now picks a random token per run, sends it with every prefetch and acquire reply, and the scenario applies a held copy only when its generation and token both match, and lets a new run's copy replace one with another token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ILLISISandClaude Opus 5.5 committed 2026-10-20 12:24:24 +11:00
1 parent ca7caedfbe
commit 5e4dcada50
6 files changed
+84 -18

No files matched your search

+24 -2
View File
@@ -112,7 +112,10 @@ describe("player_sync controller", function() {
await handler(msg.UploadRequest)(new msg.UploadRequest(instanceId, data.name, data));
}
async function acquireResponse(instanceId, playerName) {
return await handler(msg.AcquireRequest)(new msg.AcquireRequest(instanceId, playerName));
const response = await handler(msg.AcquireRequest)(new msg.AcquireRequest(instanceId, playerName));
// Random per controller run, the run token tests cover it
response.token = undefined;
return response;
}
async function loadWithInstances() {
@@ -404,7 +407,8 @@ describe("player_sync controller", function() {
}
const data = generation => ({ name: "test", generation });
async function stored(controller) {
return (await mock.getHandler(controller, msg.DownloadRequest)(new msg.DownloadRequest(2, "test"))).playerData;
const download = mock.getHandler(controller, msg.DownloadRequest);
return (await download(new msg.DownloadRequest(2, "test"))).playerData;
}
it("should send the stored data to a running destination with player_sync on", async function() {
@@ -415,6 +419,24 @@ describe("player_sync controller", function() {
assert.ok(sent[0].message instanceof msg.PrefetchEvent);
assert.deepEqual(sent[0].message.playerData, data(1));
});
it("should tag the prefetch and acquire reply with one token per controller run", async function() {
const { controller, sent, upload } = await setup();
await upload(new msg.UploadRequest(1, "test", data(1), 2));
const acquire = await mock.getHandler(controller, msg.AcquireRequest)(new msg.AcquireRequest(2, "test"));
assert.equal(acquire.status, "acquired");
assert.ok(sent[0].message.token);
assert.equal(acquire.token, sent[0].message.token);
const again = await setup();
await again.upload(new msg.UploadRequest(1, "test", data(1), 2));
assert.notEqual(again.sent[0].message.token, sent[0].message.token);
});
it("should not need a token on a busy reply", async function() {
const { controller } = await setup();
const acquire = mock.getHandler(controller, msg.AcquireRequest);
await acquire(new msg.AcquireRequest(3, "test"));
assert.equal((await acquire(new msg.AcquireRequest(2, "test"))).token, undefined);
});
it("should send the stored copy, with the parts the upload's components left out", async function() {
const { sent, upload } = await setup();
await upload(new msg.UploadRequest(1, "test", { ...data(1), force: "player", tag: "first" }));