mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 15:24:52 +09:00
Keep auto paused instances paused
Every RCON command makes an auto paused Factorio server run one tick, so subspace_storage's 1 s inventory update and 5 s ping, plus a statistics_exporter gather on every Prometheus scrape, kept a server with nobody online running at about 1.3 ticks per second. The instance now records the auto_pause server setting it was started with and exposes autoPaused, true while that setting is on and no players are online, along with an autoPauseChanged hook. subspace_storage skips its timers while paused and sends a ping and inventory as soon as a player joins. statistics_exporter gathers once after the game pauses and keeps serving those values until it resumes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ebdbb1fe56
commit
11e067a933
11 files changed
+227
-19
No files matched your search
@@ -175,18 +175,25 @@ export async function gatherMetrics(instance: Instance, pluginName: string) {
|
||||
|
||||
export default async function(context: InstancePluginContext) {
|
||||
const { instance, plugin } = context;
|
||||
// Stats of an auto paused game do not change, and gathering them runs a tick.
|
||||
let gatheredWhilePaused = false;
|
||||
|
||||
instance.hooks.metrics.attach(plugin.name, async () => {
|
||||
if (instance.status !== "running") {
|
||||
return;
|
||||
}
|
||||
if (instance.autoPaused && gatheredWhilePaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait configured timeout for the metrics to be collected. It may
|
||||
// take a long time for the command to go through if the command
|
||||
// stream is overloaded. Should the timeout be exceeded the
|
||||
// previous values for the metrics will end up being sent to controller.
|
||||
let timeoutMs = instance.config.get("statistics_exporter.command_timeout") * 1000;
|
||||
const paused = instance.autoPaused;
|
||||
await lib.timeout(gatherMetrics(instance, plugin.name), timeoutMs, undefined);
|
||||
gatheredWhilePaused = paused;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,19 @@ describe("statistics_exporter plugin", function() {
|
||||
"7357", "player", "nauvis").get(),
|
||||
2);
|
||||
});
|
||||
it("should gather once while auto paused", async function() {
|
||||
mockInstance.server.rconCommands.length = 0;
|
||||
mockInstance.autoPaused = true;
|
||||
try {
|
||||
await mockInstance.hooks.metrics.invoke();
|
||||
await mockInstance.hooks.metrics.invoke();
|
||||
assert.equal(mockInstance.server.rconCommands.length, 1);
|
||||
} finally {
|
||||
mockInstance.autoPaused = false;
|
||||
}
|
||||
await mockInstance.hooks.metrics.invoke();
|
||||
assert.equal(mockInstance.server.rconCommands.length, 2);
|
||||
});
|
||||
it("should pass on JSON parse errors", async function() {
|
||||
let string = "An error occured\n";
|
||||
mockInstance.server.rconCommandResults.set(
|
||||
|
||||
@@ -87,14 +87,38 @@ export default async function(context: InstancePluginContext) {
|
||||
cachedInventoryItems = event.items;
|
||||
});
|
||||
|
||||
function sendPing() {
|
||||
if (!host.connected) {
|
||||
return; // Only ping if we are actually connected to the controller.
|
||||
}
|
||||
instance.sendRcon(
|
||||
"/sc __subspace_storage__ global.ticksSinceMasterPinged = 0", true, plugin.name
|
||||
).catch(err => unexpectedError(err));
|
||||
}
|
||||
|
||||
function sendInventory() {
|
||||
if (instance.status !== "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadItems = [
|
||||
...cachedInventoryItems,
|
||||
new Item("signal-unixtime", Math.floor(Date.now() / 1000), "normal"),
|
||||
];
|
||||
const payloadJson = lib.escapeString(JSON.stringify(payloadItems));
|
||||
const task = instance.sendRcon(
|
||||
`/sc __subspace_storage__ UpdateInvData("${payloadJson}")`, true, plugin.name
|
||||
).catch(err => unexpectedError(err));
|
||||
pendingTasks.add(task);
|
||||
task.finally(() => { pendingTasks.delete(task); });
|
||||
}
|
||||
|
||||
instance.hooks.start.attach(plugin.name, async () => {
|
||||
// Each command runs a tick on an auto paused game.
|
||||
pingId = setInterval(() => {
|
||||
if (!host.connected) {
|
||||
return; // Only ping if we are actually connected to the controller.
|
||||
if (!instance.autoPaused) {
|
||||
sendPing();
|
||||
}
|
||||
instance.sendRcon(
|
||||
"/sc __subspace_storage__ global.ticksSinceMasterPinged = 0", true, plugin.name
|
||||
).catch(err => unexpectedError(err));
|
||||
}, 5000);
|
||||
|
||||
let items = await instance.sendTo("controller", new GetStorageRequest());
|
||||
@@ -105,23 +129,19 @@ export default async function(context: InstancePluginContext) {
|
||||
|
||||
// Ensure a payload with updated time is sent every second
|
||||
timeUpdateId = setInterval(() => {
|
||||
if (instance.status !== "running") {
|
||||
return;
|
||||
if (!instance.autoPaused) {
|
||||
sendInventory();
|
||||
}
|
||||
|
||||
const payloadItems = [
|
||||
...cachedInventoryItems,
|
||||
new Item("signal-unixtime", Math.floor(Date.now() / 1000), "normal"),
|
||||
];
|
||||
const payloadJson = lib.escapeString(JSON.stringify(payloadItems));
|
||||
const task = instance.sendRcon(
|
||||
`/sc __subspace_storage__ UpdateInvData("${payloadJson}")`, true, plugin.name
|
||||
).catch(err => unexpectedError(err));
|
||||
pendingTasks.add(task);
|
||||
task.finally(() => { pendingTasks.delete(task); });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
instance.hooks.autoPauseChanged.attach(plugin.name, async (paused: boolean) => {
|
||||
if (!paused && instance.status === "running") {
|
||||
sendPing();
|
||||
sendInventory();
|
||||
}
|
||||
});
|
||||
|
||||
instance.hooks.stop.attach(plugin.name, async () => {
|
||||
clearInterval(pingId);
|
||||
clearInterval(timeUpdateId);
|
||||
|
||||
@@ -129,5 +129,30 @@ describe("subspace_storage plugin", function() {
|
||||
nodeMock.timers.reset();
|
||||
}
|
||||
});
|
||||
it("should not send commands while auto paused and catch up on unpause", async function() {
|
||||
nodeMock.timers.enable({ apis: ["setInterval", "Date"], now: 1000000 });
|
||||
const { instance: startedInstance, host } = await mock.loadInstancePlugin(instanceEntrypoint, info);
|
||||
host.connected = true;
|
||||
startedInstance.sendTo = async () => [];
|
||||
try {
|
||||
await startedInstance.hooks.start.invoke();
|
||||
startedInstance.server.rconCommands.length = 0;
|
||||
startedInstance.autoPaused = true;
|
||||
nodeMock.timers.tick(10000);
|
||||
assert.deepEqual(startedInstance.server.rconCommands, []);
|
||||
|
||||
startedInstance.autoPaused = false;
|
||||
await startedInstance.hooks.autoPauseChanged.invoke(false);
|
||||
assert.deepEqual(startedInstance.server.rconCommands, [
|
||||
"/sc __subspace_storage__ global.ticksSinceMasterPinged = 0",
|
||||
`/sc __subspace_storage__ UpdateInvData("${lib.escapeString(JSON.stringify(
|
||||
[new Item("signal-unixtime", 1010)]
|
||||
))}")`,
|
||||
]);
|
||||
} finally {
|
||||
await startedInstance.hooks.stop.invoke();
|
||||
nodeMock.timers.reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user