Refactor InstanceInfo

Refactor to store instance status, gamePort, and updatedAt to disk for
instances.  This enables InstanceDetails to properly record when it was
updated.
This commit is contained in:
Hornwitser committed 2023-12-09 17:23:24 +01:00
1 parent c248363895
commit d186f85773
7 files changed
+109 -118

No files matched your search

+2 -40
View File
@@ -228,49 +228,11 @@ export default class ControlConnection extends BaseConnection {
}
async handleInstanceDetailsGetRequest(request: lib.InstanceDetailsGetRequest) {
let instance = this._controller.getRequestInstance(request.instanceId);
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
if (assigned_host === null) {
assigned_host = undefined;
}
let game_port: number|null|undefined = instance.game_port;
if (game_port === null) {
game_port = undefined;
}
return new lib.InstanceDetails(
instance.config.get("instance.name"),
instance.id,
assigned_host,
game_port,
instance.status,
);
return this._controller.getRequestInstance(request.instanceId).toInstanceDetails();
}
async handleInstanceDetailsListRequest() {
let list = [];
for (let instance of this._controller.instances.values()) {
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
if (assigned_host === null) {
assigned_host = undefined;
}
let game_port: number|null|undefined = instance.game_port;
if (game_port === null) {
game_port = undefined;
}
list.push(new lib.InstanceDetails(
instance.config.get("instance.name"),
instance.id,
assigned_host,
game_port,
instance.status,
));
}
return list;
return [...this._controller.instances.values()].map(instance => instance.toInstanceDetails());
}
// XXX should probably add a hook for host reuqests?
+21 -39
View File
@@ -17,7 +17,7 @@ import * as lib from "@clusterio/lib";
const { logger, Summary, Gauge } = lib;
import HttpCloser from "./HttpCloser";
import InstanceInfo, { InstanceStatus } from "./InstanceInfo";
import InstanceInfo from "./InstanceInfo";
import * as metrics from "./metrics";
import * as routes from "./routes";
import ControllerUser from "./ControllerUser";
@@ -478,16 +478,22 @@ export default class Controller {
let instances = new Map();
try {
let serialized = JSON.parse(await fs.readFile(filePath, { encoding: "utf8" }));
for (let serializedConfig of serialized) {
let serialized = JSON.parse(
await fs.readFile(filePath, { encoding: "utf8" })
) as Static<typeof InstanceInfo.jsonSchema>[];
for (let json of serialized) {
if (!json.config) { // migrate: from pre Alpha 14 format.
json = { config: json, status: "running" }; // Use running to force updatedAt
}
let instanceConfig = new lib.InstanceConfig("controller");
await instanceConfig.load(serializedConfig);
let status = instanceConfig.get("instance.assigned_host") === null ? "unassigned" : "unknown";
let instance = new InstanceInfo({
config: instanceConfig,
status: status as InstanceStatus,
});
instances.set(instanceConfig.get("instance.id"), instance);
await instanceConfig.load(json.config as lib.SerializedConfig);
let instance = InstanceInfo.fromJSON(json, instanceConfig);
const status = instance.config.get("instance.assigned_host") === null ? "unassigned" : "unknown";
if (instance.status !== status) {
instance.status = status;
instance.updatedAt = Date.now();
}
instances.set(instance.id, instance);
}
} catch (err: any) {
@@ -501,11 +507,7 @@ export default class Controller {
}
static async saveInstances(filePath: string, instances: Map<number, InstanceInfo>) {
let serialized = [];
for (const instance of instances.values()) {
serialized.push(instance.config.serialize());
}
let serialized = [...instances.values()].map(instance => instance.toJSON());
await lib.safeOutputFile(filePath, JSON.stringify(serialized, null, "\t"));
}
@@ -692,7 +694,7 @@ export default class Controller {
};
instanceConfig.set("factorio.settings", settings);
let instance = new InstanceInfo({ config: instanceConfig, status: "unassigned" });
let instance = new InstanceInfo(instanceConfig, "unassigned", undefined, Date.now());
this.instances.set(instanceId, instance);
this.instancesDirty = true;
await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance);
@@ -772,6 +774,7 @@ export default class Controller {
let prev = instance.status;
instance.status = "deleted";
instance.updatedAt = Date.now();
this.instanceDetailsUpdated([instance]);
await lib.invokeHook(this.plugins, "onInstanceStatusChanged", instance, prev);
}
@@ -799,6 +802,7 @@ export default class Controller {
addInstanceHooks(instance: InstanceInfo) {
instance.config.on("fieldChanged", (group: lib.ConfigGroup, field: string, prev: any) => {
instance.updatedAt = Date.now();
if (group.name === "instance" && field === "name") {
this.instanceDetailsUpdated([instance]);
}
@@ -809,29 +813,7 @@ export default class Controller {
}
instanceDetailsUpdated(instances: InstanceInfo[]) {
const now = Date.now();
const updates: lib.InstanceDetails[] = [];
for (const instance of instances) {
let assigned_host: number|null|undefined = instance.config.get("instance.assigned_host");
if (assigned_host === null) {
assigned_host = undefined;
}
let game_port: number|null|undefined = instance.game_port;
if (game_port === null) {
game_port = undefined;
}
updates.push(new lib.InstanceDetails(
instance.config.get("instance.name"),
instance.id,
assigned_host,
game_port,
instance.status,
now,
));
}
const updates = instances.map(instance => instance.toInstanceDetails());
this.subscriptions.broadcast(new lib.InstanceDetailsUpdatesEvent(updates));
}
+11 -5
View File
@@ -5,7 +5,7 @@ import * as lib from "@clusterio/lib";
const { logger } = lib;
import BaseConnection from "./BaseConnection";
import InstanceInfo, { InstanceStatus } from "./InstanceInfo";
import InstanceInfo from "./InstanceInfo";
import HostInfo from "./HostInfo";
@@ -176,8 +176,9 @@ export default class HostConnection extends BaseConnection {
}
let prev = instance.status;
instance.status = request.status as lib.InstanceStatus;
instance.game_port = request.gamePort || null;
instance.status = request.status;
instance.gamePort = request.gamePort;
instance.updatedAt = Date.now();
logger.verbose(`Instance ${instance.config.get("instance.name")} State: ${instance.status}`);
this._controller.instanceDetailsUpdated([instance]);
await lib.invokeHook(this._controller.plugins, "onInstanceStatusChanged", instance, prev);
@@ -212,7 +213,9 @@ export default class HostConnection extends BaseConnection {
// Already have this instance, update state instead
if (controllerInstance.status !== instanceData.status) {
let prev = controllerInstance.status;
controllerInstance.status = instanceData.status as InstanceStatus;
controllerInstance.status = instanceData.status;
controllerInstance.gamePort = instanceData.gamePort;
controllerInstance.updatedAt = Date.now();
logger.verbose(`Instance ${instanceConfig.get("instance.name")} State: ${instanceData.status}`);
instanceUpdates.push(controllerInstance);
await lib.invokeHook(
@@ -224,7 +227,10 @@ export default class HostConnection extends BaseConnection {
instanceConfig.set("instance.assigned_host", this._id);
let newInstance = new InstanceInfo(
{ config: instanceConfig, status: instanceData.status as InstanceStatus }
instanceConfig,
instanceData.status,
instanceData.gamePort,
Date.now(),
);
this._controller.instances.set(instanceConfig.get("instance.id"), newInstance);
this._controller.instancesDirty = true;
+40 -26
View File
@@ -1,22 +1,5 @@
import type * as lib from "@clusterio/lib";
/**
* Current status of the instance. One of:
* - `unknown`: Instance is assigned to a host but this host is currently
* not connected to the contreller.
* - `unassigned`: Instance is not assigned to a a host and exists only on
* the controller.
* - `stopped`: Instance is stopped.
* - `starting`: Instance is in the process of starting up.
* - `running`: Instance is running normally.
* - `stopping`: Instance is in the process of stopping.
* - `creating_save`: Instance is in the process of creating a save.
* - `exporting_data`: Instance is in the process of exporting game data.
* - `deleted`: Instance has been deleted.
*/
export type InstanceStatus =
"unknown" | "unassigned" | "stopped" | "starting" | "running" |
"stopping" | "creating_save" | "exporting_data" | "deleted";
import * as lib from "@clusterio/lib";
import { Static, Type } from "@sinclair/typebox";
/**
@@ -24,25 +7,56 @@ export type InstanceStatus =
* @alias module:controller/src/InstanceInfo
*/
export default class InstanceInfo {
config: lib.InstanceConfig;
status: InstanceStatus;
game_port: number | null = null;
constructor(
{ config, status }:
{ config: lib.InstanceConfig, status: InstanceStatus }
public config: lib.InstanceConfig,
public status: lib.InstanceStatus,
public gamePort?: number,
public updatedAt = 0,
) {
this.config = config;
this.status = status;
}
toJSON() {
static jsonSchema = Type.Object({
"config": Type.Object({}),
"status": lib.InstanceStatus,
"gamePort": Type.Optional(Type.Number()),
"updatedAt": Type.Optional(Type.Number()),
});
static fromJSON(json: Static<typeof this.jsonSchema>, config: lib.InstanceConfig) {
return new this(
config,
json.status,
json.gamePort,
json.updatedAt,
);
}
toJSON(): Static<typeof InstanceInfo.jsonSchema> {
return {
config: this.config.serialize(),
status: this.status,
gamePort: this.gamePort,
updatedAt: this.updatedAt,
};
}
toInstanceDetails() {
return new lib.InstanceDetails(
this.config.get("instance.name"),
this.id,
this.config.get("instance.assigned_host") ?? undefined,
this.gamePort,
this.status,
this.updatedAt,
);
}
get isDeleted() {
return this.status === "deleted";
}
/** Shorthand for `instance.config.get("instance.id")` */
get id():number {
return this.config.get("instance.id");
+1
View File
@@ -913,6 +913,7 @@ export default class Host extends lib.Link {
list.push(new lib.RawInstanceInfo(
instanceInfo.config.serialize("controller"),
instanceConnection ? instanceConnection.instance.status : "stopped",
instanceConnection ? instanceConnection.instance.server.gamePort : undefined,
));
}
await this.send(new lib.InstancesUpdateRequest(list));
+1 -1
View File
@@ -446,7 +446,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
new lib.InstanceStatusChangedEvent(
this.id,
status,
this.server.gamePort || this.config.get("factorio.game_port") || undefined,
status !== "stopped" ? this.server.gamePort : undefined,
),
);
}
+33 -7
View File
@@ -7,10 +7,34 @@ import type { MessageRequest } from "./messages_core";
import { CollectorResultSerialized } from "../prometheus";
export type InstanceStatus =
"unknown" | "unassigned" | "stopped" | "starting" | "running"
| "stopping" | "creating_save" | "exporting_data" | "deleted"
;
export const InstanceStatus = StringEnum([
"unknown",
"unassigned",
"stopped",
"starting",
"running",
"stopping",
"creating_save",
"exporting_data",
"deleted",
]);
/**
* Current status of the instance. One of:
* - `unknown`: Instance is assigned to a host but this host is currently
* not connected to the contreller.
* - `unassigned`: Instance is not assigned to a a host and exists only on
* the controller.
* - `stopped`: Instance is stopped.
* - `starting`: Instance is in the process of starting up.
* - `running`: Instance is running normally.
* - `stopping`: Instance is in the process of stopping.
* - `creating_save`: Instance is in the process of creating a save.
* - `exporting_data`: Instance is in the process of exporting game data.
* - `deleted`: Instance has been deleted.
*/
export type InstanceStatus = Static<typeof InstanceStatus>;
export class InstanceDetails {
constructor(
public name: string,
@@ -653,7 +677,8 @@ export class RawInstanceInfo {
constructor(
public config: object,
public status: string,
public status: InstanceStatus,
public gamePort: undefined | number,
) { }
static jsonSchema = Type.Object({
@@ -661,10 +686,11 @@ export class RawInstanceInfo {
"status": StringEnum([
"stopped", "starting", "running", "stopping", "creating_save", "exporting_data",
]),
"gamePort": Type.Optional(Type.Number()),
});
static fromJSON(json: Static<typeof this.jsonSchema>) {
return new this(json.config, json.status);
return new this(json.config, json.status, json.gamePort);
}
}
@@ -757,7 +783,7 @@ export class InstanceStatusChangedEvent {
constructor(
public instanceId: number,
public status: string,
public status: InstanceStatus,
public gamePort?: number,
) { }