Refactor instance communication

Change Instance class to be a subclass of Link and refactor the
communication to and establisment of instances to use a virtual
connection between the slave and the instance.  This is simplifies the
implementation of plugin communication for instances.

Replace InstanceRequest with a forwardTo option to Request which does
the task of forwarding in a much cleaner way.
This commit is contained in:
Hornwitser committed 2019-12-13 21:57:05 +01:00
1 parent 9f24d200b7
commit 5d8fa4e4ce
7 files changed
+381 -265

No files matched your search

+120 -113
View File
@@ -9,15 +9,47 @@ const errors = require("lib/errors");
* link.
*/
class Request {
/**
* Creates a request
*
* @param {string} type -
* message type used. Will have suffix '_request' for the request
* message and suffix '_response' for the response message.
* @param {Array<string>} links -
* Links this request is valid on. Takes an array of strings
* containing "<source>-<target>" specifications.
* @param {?string} forwardTo -
* Optional target to forward this request to. Only 'instance' is
* currently supported and adds instance_id into the
* requestProperties.
* @param {Object<string, Object>} requestProperties -
* Mapping of property values to JSON schema specifications for the
* properties that are valid in the data payload of this requst.
* @param {Object<string, Object>} responseProperties -
* Mapping of property values to JSON schema specifications for the
* properties that are valid in the data payload of the response to
* this requst.
*/
constructor({
type, links, requestProperties = {}, responseProperties = {}
type, links, forwardTo = null, requestProperties = {}, responseProperties = {}
}) {
this.type = type;
this.links = links;
this.forwardTo = forwardTo;
this.requestType = type + '_request';
this.responseType = type + '_response';
if (forwardTo === 'instance') {
requestProperties = Object.assign(
{"instance_id": { type: "integer" }}, requestProperties
);
} else if (forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
}
this._requestValidator = schema.compile({
$schema: "http://json-schema.org/draft-07/schema#",
properties: {
@@ -64,6 +96,9 @@ class Request {
* Set a the validator for the response on the given link if it's a
* source and sets the handler for the request if it is a target.
*
* If forwardTo was set to 'instance' the `forwardRequestToInstance`
* method on the link is used as the default handler.
*
* Does nothing if the link is neither a source nor a target for this
* request.
*
@@ -81,21 +116,26 @@ class Request {
// Target side. Note: Source and target is reversed here.
if (this.links.includes(`${link.target}-${link.source}`)) {
// Use forwarder if handler is not present.
if (!handler) {
if (this.forwardTo === 'instance') {
handler = link.forwardRequestToInstance;
}
}
if (!handler) {
throw new Error(`Missing handler for ${this.requestType} on ${link.source}-${link.target} link`);
}
link.setHandler(this.requestType, message => {
handler.call(link, message).then(response => {
handler.call(link, message, this).then(response => {
if (response === undefined) {
// XXX Should we allow implicit responses like this?
response = {};
}
if (Object.prototype.hasOwnProperty.call(response, 'seq')) {
throw new errors.RequestError("response contains reserved property 'seq'");
}
link.connector.send(this.responseType, { seq: message.seq, ...response });
link.connector.send(this.responseType, { ...response, seq: message.seq });
}).catch(err => {
if (!(err instanceof errors.RequestError)) {
@@ -126,95 +166,6 @@ class Request {
}
}
/**
* Represents a request sent to the master that is forwarded to an instance
*
* This is a two part request with a master part and a slave part. The
* master part is done by the forwardInstanceRequest method on the master
* side of link which finds the slave the requuested instance is on and
* forwards it there using the foward method.
*
* The slave part is done by the forwardInstanceRequest on the slave side of
* the link and finds the instance and then calls the handler with it and
* the message.
*/
class InstanceRequest {
constructor({
type, sources, requestProperties = {}, responseProperties = {}
}) {
this._masterRequest = new Request({
type: type + '_master',
links: sources.map(source => `${source}-master`),
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
this._slaveRequest = new Request({
type: type + '_slave',
links: ['master-slave'],
requestProperties: {
"instance_id": { type: "integer" },
...requestProperties,
},
responseProperties,
});
}
/**
* Attach to a link
*
* Attach master/slave part of the instance request. If this is the
* slave side the handler argument is required.
*
* Does nothing if the link is neither a source nor a target for the
* slave or master request.
*
* @param {Link} link - The link to attach to.
* @param {Function} handler -
* Async function to to handle the instance request. It is passed on
* to forwardInstanceRequest on the slave side.
* @throws {Error} if the handler is needed and not defined.
*/
attach(link, handler) {
this._masterRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(this, message);
});
if (handler) {
this._slaveRequest.attach(link, async (message) => {
return await link.forwardInstanceRequest(handler, message);
});
} else {
this._slaveRequest.attach(link);
}
}
/**
* Forward the message over the link
*
* This method is expected to be called by forwardInstanceRequest on the
* master with the slave link that the instance is on.
*/
async forward(link, message) {
let response = await this._slaveRequest.send(link, message.data);
delete response.seq;
return response;
}
/**
* Send request over the given link
*
* Sends the given data over the link to the master and waits for it to
* forward the request to the slave with the instance on and return the
* result of it.
*
* @returns {object} response data
*/
async send(link, data = {}) {
return await this._masterRequest.send(link, data);
}
}
let messages = {}
@@ -305,30 +256,34 @@ messages.createInstance = new Request({
},
});
messages.createSave = new InstanceRequest({
type: 'create_save',
sources: ['control'],
});
messages.startInstance = new InstanceRequest({
messages.startInstance = new Request({
type: 'start_instance',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.stopInstance = new InstanceRequest({
messages.createSave = new Request({
type: 'create_save',
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.stopInstance = new Request({
type: 'stop_instance',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
});
messages.deleteInstance = new InstanceRequest({
messages.deleteInstance = new Request({
type: 'delete_instance',
sources: ['control'],
links: ['control-master', 'master-slave'],
forwardTo: 'instance',
});
messages.sendRcon = new InstanceRequest({
messages.sendRcon = new Request({
type: 'send_rcon',
sources: ['control'],
links: ['control-master', 'master-slave', 'slave-instance'],
forwardTo: 'instance',
requestProperties: {
"command": { type: "string" },
},
@@ -338,10 +293,38 @@ messages.sendRcon = new InstanceRequest({
});
/**
* Represents an event sent over the link
*
* A one way message without any response or recipt confirmation that
* can be innitiated by either party of the link.
*/
class Event {
constructor({ type, links, eventProperties = {} }) {
/**
* Creates an event
*
* @param {string} type -
* message type used. Will have suffix '_event' appended to it for
* the messages sent through this event.
* @param {Array<string>} links -
* Links this event is valid on. Takes an array of strings
* containing "<source>-<target>" specifications.
* @param {?string} forwardTo -
* Optional target to forward this request to. Only 'master' is
* currently supported.
* @param {Object<string, Object>} eventProperties -
* Mapping of property values to JSON schema specifications for the
* properties that are valid in the data payload of this event.
*/
constructor({ type, links, forwardTo = null, eventProperties = {} }) {
this.type = type;
this.links = links;
this.forwardTo = forwardTo;
if (forwardTo !== 'master' && forwardTo !== null) {
throw new Error(`Invalid forwardTo value ${forwardTo}`);
}
this.eventType = type + '_event';
this._eventValidator = schema.compile({
@@ -357,16 +340,40 @@ class Event {
});
}
/**
* Attach to a link
*
* Set a the handler for the event on the given link if it's a target
* for the event.
*
* If forwardTo was set to 'master' the `forwardEventToMaster` method on
* the link is used as the default handler.
*
* Does nothing if the link is not a target for this event.
*
* @param {Link} link - The link to attach to.
* @param {Function} handler -
* Async function to invoke with link set as this to handle the
* event. Only used on the target side.
* @throws {Error} if the handler is needed and not defined.
*/
attach(link, handler) {
// Target side. Note: Source and target is reversed here.
if (this.links.includes(`${link.target}-${link.source}`)) {
// Use forwarder if handler is not present.
if (!handler) {
if (this.forwardTo === 'master') {
handler = link.forwardEventToMaster;
}
}
if (!handler) {
throw new Error(`Missing handler for ${this.eventType} on ${link.source}-${link.target} link`);
}
link.setHandler(this.eventType, message => {
// XXX Should event handlers even be allowed to be async?
handler.call(link, message).catch(err => {
handler.call(link, message, this).catch(err => {
console.error(`Unexpected error while handling ${this.eventType}`, err);
});
}, this._eventValidator);
@@ -404,7 +411,8 @@ messages.updateInstances = new Event({
messages.instanceOutput = new Event({
type: 'instance_output',
links: ['slave-master', 'master-control'],
links: ['instance-slave', 'slave-master', 'master-control'],
forwardTo: 'master',
eventProperties: {
"instance_id": { type: "integer" },
"output": {
@@ -444,7 +452,6 @@ function attachAllMessages(link) {
module.exports = {
Request,
InstanceRequest,
Event,
attachAllMessages,