mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 07:24:52 +09:00
Invalidate instance copies when player data is imported or deleted
Generations alone could not order a repair against copies held by instances. Deleting dropped the watermark, so a stale instance could skip downloading the new baseline. An import could land on the same generation as an upload that never reached the controller, and the instance would then keep its own copy. Imports and deletes now bump a per-player epoch, which the controller returns on acquire and checks on upload. Instances throw away a copy from an earlier epoch, and deleted players keep their generation and epoch in player_sync_deleted.json. A restarted download also takes the new generation instead of the outdated one. Generations in player data must now be non-negative integers, so an invalid import is refused before it reaches the datastore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d14148606
commit
f12b016cec
9 files changed
+159
-39
No files matched your search
@@ -104,9 +104,15 @@ describe("player_sync controller", function() {
|
||||
const response = await handler(msg.AcquireRequest)(new msg.AcquireRequest(instanceId, playerName));
|
||||
return response.status === "acquired";
|
||||
}
|
||||
async function saved() {
|
||||
async function saved(file = "player_sync.json") {
|
||||
await controller.hooks.save.invoke();
|
||||
return new Map(JSON.parse(await fs.readFile(path.join(databaseDir, "player_sync.json"), "utf8")));
|
||||
return new Map(JSON.parse(await fs.readFile(path.join(databaseDir, file), "utf8")));
|
||||
}
|
||||
async function upload(instanceId, data) {
|
||||
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));
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
@@ -170,10 +176,25 @@ describe("player_sync controller", function() {
|
||||
async function release(name) {
|
||||
await handler(msg.ForceReleaseRequest)(new msg.ForceReleaseRequest(name));
|
||||
}
|
||||
it("should store data with a bumped generation", async function() {
|
||||
it("should store data with a bumped generation and epoch", async function() {
|
||||
await release("stored");
|
||||
assert.deepEqual(await set("stored", playerData("other", 1)), new msg.SetPlayerDataResponse(4));
|
||||
assert.deepEqual((await saved()).get("stored"), playerData("stored", 4));
|
||||
assert.deepEqual((await saved()).get("stored"), playerData("stored", 4, { epoch: 1 }));
|
||||
});
|
||||
it("should invalidate unconfirmed uploads from instances", async function() {
|
||||
// Instance 1 left with generation 4 but the upload never reached the controller
|
||||
await release("stored");
|
||||
await set("stored", playerData("stored", 0));
|
||||
await upload(1, playerData("stored", 4));
|
||||
assert.deepEqual((await saved()).get("stored"), playerData("stored", 4, { epoch: 1 }));
|
||||
assert.deepEqual(
|
||||
await acquireResponse(1, "stored"), new msg.AcquireResponse("acquired", 4, true, undefined, 1),
|
||||
);
|
||||
});
|
||||
it("should ignore the epoch in imported data", async function() {
|
||||
await release("stored");
|
||||
await set("stored", playerData("stored", 0, { epoch: 7 }));
|
||||
assert.equal((await saved()).get("stored").epoch, 1);
|
||||
});
|
||||
it("should keep a higher provided generation", async function() {
|
||||
await release("stored");
|
||||
@@ -198,6 +219,32 @@ describe("player_sync controller", function() {
|
||||
await del("stored");
|
||||
assert(!(await saved()).has("stored"));
|
||||
});
|
||||
it("should keep the generation and bump the epoch", async function() {
|
||||
await handler(msg.ForceReleaseRequest)(new msg.ForceReleaseRequest("stored"));
|
||||
await del("stored");
|
||||
assert.deepEqual(
|
||||
await acquireResponse(2, "stored"), new msg.AcquireResponse("acquired", 3, false, undefined, 1),
|
||||
);
|
||||
assert.deepEqual((await saved("player_sync_deleted.json")).get("stored"), { generation: 3, epoch: 1 });
|
||||
|
||||
controller = await load();
|
||||
assert.deepEqual(
|
||||
await acquireResponse(2, "stored"), new msg.AcquireResponse("acquired", 3, false, undefined, 1),
|
||||
);
|
||||
});
|
||||
it("should accept the next upload from the new epoch only", async function() {
|
||||
await handler(msg.ForceReleaseRequest)(new msg.ForceReleaseRequest("stored"));
|
||||
await del("stored");
|
||||
await upload(1, playerData("stored", 11));
|
||||
assert(!(await saved()).has("stored"));
|
||||
await upload(2, playerData("stored", 4, { epoch: 1 }));
|
||||
assert.deepEqual((await saved()).get("stored"), playerData("stored", 4, { epoch: 1 }));
|
||||
assert(!(await saved("player_sync_deleted.json")).has("stored"));
|
||||
|
||||
// The other instance downloads the baseline instead of uploading its stale copy
|
||||
await upload(1, playerData("stored", 11));
|
||||
assert.equal((await saved()).get("stored").generation, 4);
|
||||
});
|
||||
it("should reject unknown players", async function() {
|
||||
await assert.rejects(del("unknown"), new lib.RequestError("No player data stored for unknown"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user