Keep a newer queue change over a queue reply it overtook

A QueueEvent arriving in the same socket read as a QueueSyncRequest reply is
applied before the reply, which then wrote the older queue over it. Skip the
reply when a QueueEvent was applied while it was in flight.

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:20:36 +11:00
1 parent fd39e2a171
commit b10807b6d6
2 files changed
+19

No files matched your search

+8
View File
@@ -26,6 +26,7 @@ type IpcQueue = { queue: unknown, player?: string };
export default async function(context: InstancePluginContext) {
const { instance, logger, plugin } = context;
let syncStarted = false;
let queueEventsApplied = 0;
// Commands are sent one at a time so they execute in order
let rconQueue: Promise<unknown> = Promise.resolve();
@@ -50,7 +51,13 @@ export default async function(context: InstancePluginContext) {
// queue null asks for the cluster queue without seeding it
async function followQueue(queue: string[] | null, player: string | null) {
let applied = queueEventsApplied;
let result = await instance.sendTo("controller", new QueueSyncRequest(queue, player));
// A QueueEvent handled meanwhile is at least as new as this reply, since the controller broadcasts every
// queue change in order with its replies. Arriving in the same read, it is applied before the reply.
if (queueEventsApplied !== applied) {
return;
}
await applyQueue({ queue: result });
}
@@ -116,6 +123,7 @@ export default async function(context: InstancePluginContext) {
if (event.player !== null) {
data.player = event.player;
}
queueEventsApplied += 1;
await applyQueue(data);
});
+11
View File
@@ -117,6 +117,17 @@ describe("research_sync instance", function() {
instance.sendTo = async () => { throw new Error("gone"); };
await instance.hooks.controllerConnectionEvent.invoke("connect");
});
it("should not let the reply overwrite a newer QueueEvent delivered with it", async function() {
// As if the reply and the broadcast came in one socket read: the event is handled before the
// reply resolves the request's await
instance.sendTo = async (dst, message) => {
sent.push(message);
mock.getHandler(instance, QueueEvent)(new QueueEvent(["new"], null, "B"));
return ["old"];
};
await instance.hooks.controllerConnectionEvent.invoke("connect");
assert.equal(lastCommand(), applyCommand({ queue: ["new"], instance: "B" }));
});
});
it("should forward queue changes from the game", async function() {