mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Check the run token on every prefetched chunk
Chunks of an earlier controller run's copy still in flight could land in a newer run's copy of the same generation. Every chunk now carries the token, and the scenario ignores a chunk whose token differs from its record's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5e4dcada50
commit
17f029e39e
3 files changed
+10
-9
No files matched your search
@@ -289,7 +289,8 @@ export default async function(context: InstancePluginContext) {
|
||||
logger.verbose(`Prefetching ${name}`);
|
||||
// This side doesn't know the save's cleared recipe list, so the whole stored list goes as a full delta
|
||||
await sendPlayerData(playerData, undefined, (chunk, number, total, extra) => {
|
||||
const tokenArg = number === 1 ? `,'${token}'` : "";
|
||||
// Later chunks have no components argument, so nil holds its place
|
||||
const tokenArg = number === 1 ? `,'${token}'` : `,nil,'${token}'`;
|
||||
const args = `'${name}',${generation},'${chunk}',${number},${total}${extra}${tokenArg}`;
|
||||
return `/sc player_sync.prefetch_inventory(${args})`;
|
||||
}, true);
|
||||
|
||||
@@ -701,11 +701,11 @@ end
|
||||
|
||||
-- Called by instance.ts as /sc player_sync.prefetch_inventory('name', generation, 'chunk', number, total, components,
|
||||
-- token) with a copy of a player's stored data that the controller sent ahead (PrefetchEvent) while they switch to this
|
||||
-- server. It holds
|
||||
-- the copy for a player who is offline here or doesn't exist yet. It prints only when it refuses or ignores a chunk,
|
||||
-- which stops the host sending the rest.
|
||||
-- server. It holds the copy for a player who is offline here or doesn't exist yet. Every chunk carries the token, so
|
||||
-- chunks of an earlier controller run's copy never land in a newer one. It prints only when it refuses or ignores a
|
||||
-- chunk, which stops the host sending the rest.
|
||||
--- @param components string? JSON of the components the instance stripped the copy by, given with the first chunk
|
||||
--- @param token string? The controller run that sent the copy, given with the first chunk
|
||||
--- @param token string? The controller run that sent the copy
|
||||
function player_sync.prefetch_inventory(player_name, generation, data, number, total, components, token)
|
||||
local script_data = get_script_data()
|
||||
if number == 1 then
|
||||
@@ -744,7 +744,7 @@ function player_sync.prefetch_inventory(player_name, generation, data, number, t
|
||||
end
|
||||
|
||||
local record = script_data.active_downloads[player_name]
|
||||
if not (record and record.prefetch and record.generation == generation) then
|
||||
if not (record and record.prefetch and record.generation == generation and record.token == token) then
|
||||
-- Refused at chunk 1, dropped by the acquire reply or the inactivity check, or replaced by a newer copy
|
||||
rcon.print("Ignored prefetch chunk " .. number .. " for " .. player_name)
|
||||
return
|
||||
|
||||
@@ -277,7 +277,7 @@ describe("player_sync", function() {
|
||||
const sent = lib.escapeString(JSON.stringify(Object.fromEntries(enabled.map(c => [c, true]))));
|
||||
return chunks.map((chunk, i) => "/sc player_sync.prefetch_inventory(" +
|
||||
`'test',${playerData.generation},'${lib.escapeString(chunk)}',${i + 1},${chunks.length}` +
|
||||
`${i === 0 ? `,'${sent}','tok'` : ""})`);
|
||||
`${i === 0 ? `,'${sent}','tok'` : ",nil,'tok'"})`);
|
||||
}
|
||||
|
||||
it("should send prefetch_inventory chunks with the generation and the full recipe list", async function() {
|
||||
@@ -296,11 +296,11 @@ describe("player_sync", function() {
|
||||
assert.ok(commands.length > 1, "more than one chunk");
|
||||
assert.deepEqual(instance.server.rconCommands, commands);
|
||||
});
|
||||
it("should send the token with the first chunk only", async function() {
|
||||
it("should send the token with every chunk", async function() {
|
||||
await prefetch({ generation: 3, name: "test" });
|
||||
const commands = instance.server.rconCommands;
|
||||
assert.ok(commands[0].endsWith(",'tok')"));
|
||||
assert.ok(commands.slice(1).every(command => !command.includes("tok")));
|
||||
assert.ok(commands.length > 1 && commands.slice(1).every(command => command.endsWith(",nil,'tok')")));
|
||||
});
|
||||
it("should stop when the save refuses the first chunk", async function() {
|
||||
const playerData = { generation: 3, name: "test" };
|
||||
|
||||
Reference in new issue
Block a user