// // AgentPoolController.js // acScripts // // Created by Sam Gateau on 11/23/15. // Copyright 2015 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // function printDebug(message) { print(message); } (function() { var SERVICE_CHANNEL = "com.highfidelity.playback.service"; var COMMAND_CHANNEL = "com.highfidelity.playback.command"; // The time between alive messages on the command channel var ALIVE_PERIOD = 3; var NUM_CYCLES_BEFORE_RESET = 8; // Service Actions var MASTER_ID = -1; var INVALID_AGENT = -2; var BROADCAST_AGENTS = -3; var MASTER_ALIVE = "MASTER_ALIVE"; var AGENT_ALIVE = "AGENT_ALIVE"; var AGENT_READY = "READY"; var MASTER_HIRE_AGENT = "HIRE" var MASTER_FIRE_AGENT = "FIRE"; var makeUniqueUUID = function(SEUUID) { //return SEUUID + Math.random(); // forget complexity, just give me a four digit pin return (Math.random() * 10000).toFixed(0); } var packServiceMessage = function(dest, command, src) { var message = { dest: dest, command: command, src: src }; return JSON.stringify(message); }; var unpackServiceMessage = function(message) { return JSON.parse(message); }; var packCommandMessage = function(dest, action, argument) { var message = { dest_key: dest, action_key: action, argument_key: argument }; return JSON.stringify(message); }; var unpackCommandMessage = function(message) { return JSON.parse(message); }; // Actor //--------------------------------- var Actor = function() { this.agentID = INVALID_AGENT; this.lastAliveCycle = 0; this.onHired = function(actor) {}; this.onFired = function(actor) {}; }; Actor.prototype.isConnected = function () { return (this.agentID != INVALID_AGENT); } Actor.prototype.alive = function () { printDebug("Agent UUID =" + this.agentID + " Alive was " + this.lastAliveCycle); this.lastAliveCycle = 0; } Actor.prototype.incrementAliveCycle = function () { printDebug("Actor.prototype.incrementAliveCycle UUID =" + this.agentID + " Alive pre increment " + this.lastAliveCycle); if (this.isConnected()) { this.lastAliveCycle++; printDebug("Agent UUID =" + this.agentID + " Alive incremented " + this.lastAliveCycle); } return this.lastAliveCycle; } this.Actor = Actor; // master side //--------------------------------- var MasterController = function() { this.timeSinceLastAlive = 0; this.knownAgents = new Array; this.hiredActors = new Array; this.hiringAgentsQueue = new Array; this.subscribed = false; }; MasterController.prototype.destroy = function() { if (this.subscribed) { Messages.unsubscribe(SERVICE_CHANNEL); Messages.unsubscribe(COMMAND_CHANNEL); this.subscribed = true; } }; MasterController.prototype.reset = function() { this.timeSinceLastAlive = 0; if (!this.subscribed) { Messages.subscribe(COMMAND_CHANNEL); Messages.subscribe(SERVICE_CHANNEL); var localThis = this; Messages.messageReceived.connect(function (channel, message, senderID) { if (channel == SERVICE_CHANNEL) { localThis._processServiceMessage(message, senderID); return; } }); } // ready to roll, enable this.subscribed = true; printDebug("Master Started"); }; MasterController.prototype._processServiceMessage = function(message, senderID) { var service = unpackServiceMessage(message); if (service.dest == MASTER_ID) { if (service.command == AGENT_READY) { // check to see if we know about this agent var agentIndex = this.knownAgents.indexOf(service.src); if (agentIndex < 0) { this._onAgentAvailableForHiring(service.src); } else { // Master think the agent is hired but not the other way around, forget about it printDebug("New agent still sending ready ? " + service.src + " " + agentIndex + " Forgeting about it"); // this._removeHiredAgent(agentIndex); } } else if (service.command == AGENT_ALIVE) { // check to see if we know about this agent var agentIndex = this.knownAgents.indexOf(service.src); if (agentIndex >= 0) { // yes so reset its alive beat this.hiredActors[agentIndex].alive(); return; } else { return; } } } }; MasterController.prototype._onAgentAvailableForHiring = function(agentID) { if (this.hiringAgentsQueue.length == 0) { printDebug("No Actor on the hiring queue"); return; } printDebug("MasterController.prototype._onAgentAvailableForHiring " + agentID); var newActor = this.hiringAgentsQueue.pop(); var indexOfNewAgent = this.knownAgents.push(agentID); newActor.alive(); newActor.agentID = agentID; this.hiredActors.push(newActor); printDebug("New agent available to be hired " + agentID + " " + indexOfNewAgent); var serviceMessage = packServiceMessage(agentID, MASTER_HIRE_AGENT, MASTER_ID); printDebug("serviceMessage = " + serviceMessage); Messages.sendMessage(SERVICE_CHANNEL, serviceMessage); printDebug("message sent calling the actor" + JSON.stringify(newActor) ); newActor.onHired(newActor); } MasterController.prototype.sendCommand = function(target, action, argument) { if (this.subscribed) { var command = packCommandMessage(target, action, argument); printDebug(command); Messages.sendMessage(COMMAND_CHANNEL, command); } }; MasterController.prototype.update = function(deltaTime) { this.timeSinceLastAlive += deltaTime; if (this.timeSinceLastAlive > ALIVE_PERIOD) { this.timeSinceLastAlive = 0; Messages.sendMessage(SERVICE_CHANNEL, packServiceMessage(BROADCAST_AGENTS, MASTER_ALIVE, MASTER_ID)); { // Check for alive connected agents var lostAgents = new Array(); for (var i = 0; i < this.hiredActors.length; i++) { var actor = this.hiredActors[i]; var lastAlive = actor.incrementAliveCycle() if (lastAlive > NUM_CYCLES_BEFORE_RESET) { printDebug("Agent Lost, firing Agent #" + i + " ID " + actor.agentID); lostAgents.push(i); } } // now fire gathered lost agents from end to begin while (lostAgents.length > 0) { printDebug("Firing " + lostAgents.length + " agents" + JSON.stringify(lostAgents)); this.fireAgent(this.hiredActors[lostAgents.pop()]); } } } }; MasterController.prototype.hireAgent = function(actor) { if (actor == null) { printDebug("trying to hire an agent with a null actor, abort"); return; } if (actor.isConnected()) { printDebug("trying to hire an agent already connected, abort"); return; } this.hiringAgentsQueue.unshift(actor); }; MasterController.prototype.fireAgent = function(actor) { // check to see if we know about this agent printDebug("MasterController.prototype.fireAgent" + actor.agentID); // Try the waiting list first var waitingIndex = this.hiringAgentsQueue.indexOf(actor); if (waitingIndex >= 0) { printDebug("fireAgent found actor on waiting queue #" + waitingIndex); var lostActor = this.hiringAgentsQueue.splice(waitingIndex, 1); if (lostActor.length) { lostActor[0].onFired(lostActor[0]); } return; } // then the hired agents var actorIndex = this.knownAgents.indexOf(actor.agentID); if (actorIndex >= 0) { printDebug("fired actor found #" + actorIndex); this._removeHiredAgent(actorIndex); } } MasterController.prototype._removeHiredAgent = function(actorIndex) { // check to see if we know about this agent if (actorIndex >= 0) { printDebug("MasterController.prototype._removeHiredAgent #" + this.knownAgents[actorIndex]) this.knownAgents.splice(actorIndex, 1); var lostActor = this.hiredActors[actorIndex]; this.hiredActors.splice(actorIndex, 1); var lostAgentID = lostActor.agentID; lostActor.agentID = INVALID_AGENT; if (lostAgentID != INVALID_AGENT) { printDebug("fired actor is still connected, send fire command"); Messages.sendMessage(SERVICE_CHANNEL, packServiceMessage(lostAgentID, MASTER_FIRE_AGENT, MASTER_ID)); } lostActor.onFired(lostActor); } } this.MasterController = MasterController; // agent side //--------------------------------- var AgentController = function() { this.subscribed = false; this._init(); this.onHired = function() {}; this.onCommand = function(command) {}; this.onFired = function() {}; }; AgentController.prototype._init = function() { this.isHired= false; this.timeSinceLastAlive = 0; this.numCyclesWithoutAlive = 0; this.agentUUID = makeUniqueUUID(Agent.sessionUUID); printDebug("this.agentUUID = " + this.agentUUID); } AgentController.prototype.destroy = function() { if (this.subscribed) { this.fired(); Messages.unsubscribe(SERVICE_CHANNEL); Messages.unsubscribe(COMMAND_CHANNEL); this.subscribed = true; } }; AgentController.prototype.reset = function() { // If already hired, fire this.fired(); if (!this.subscribed) { Messages.subscribe(COMMAND_CHANNEL); Messages.subscribe(SERVICE_CHANNEL); var localThis = this; Messages.messageReceived.connect(function (channel, message, senderID) { if (channel == SERVICE_CHANNEL) { localThis._processServiceMessage(message, senderID); return; } if (channel == COMMAND_CHANNEL) { localThis._processCommandMessage(message, senderID); return; } }); } this.subscribed = true; printDebug("Client Started"); }; AgentController.prototype._processServiceMessage = function(message, senderID) { var service = unpackServiceMessage(message); printDebug("Client " + this.agentUUID + " Received message = " + message); if (service.dest == this.agentUUID) { if (service.command != AGENT_READY) { // this is potentially a message to hire me if i m not already if (!this.isHired && (service.command == MASTER_HIRE_AGENT)) { printDebug(service.command); this.isHired = true; printDebug("Client Hired by master UUID" + service.src); this.onHired(); this.alive(); return; } // Or maybe a message to fire me if i m not hired if (this.isHired && (service.command == MASTER_FIRE_AGENT)) { printDebug("Client Fired by master UUID" + senderID); this.fired(); return; } } } else if ((service.src == MASTER_ID) && (service.command == MASTER_ALIVE)) { this.numCyclesWithoutAlive = 0; return; } } AgentController.prototype._processCommandMessage = function(message, senderID) { // ONly work if hired if (this.isHired) { var command = unpackCommandMessage(message); if ((command.dest_key == this.agentUUID) || (command.dest_key == BROADCAST_AGENTS)) { printDebug("Command received = " + JSON.stringify(command) + senderID); this.onCommand(command); } } }; AgentController.prototype.update = function(deltaTime) { this.timeSinceLastAlive += deltaTime; if (this.timeSinceLastAlive > ALIVE_PERIOD) { if (this.subscribed) { if (!this.isHired) { Messages.sendMessage(SERVICE_CHANNEL, packServiceMessage(MASTER_ID, AGENT_READY, this.agentUUID)); //printDebug("Client Ready" + SERVICE_CHANNEL + AGENT_READY); } else { // Send alive beat printDebug("beat !"); Messages.sendMessage(SERVICE_CHANNEL, packServiceMessage(MASTER_ID, AGENT_ALIVE, this.agentUUID)); // Listen for master beat this.numCyclesWithoutAlive++; if (this.numCyclesWithoutAlive > NUM_CYCLES_BEFORE_RESET) { printDebug("Master Lost, self firing Agent"); this.fired(); } } } this.timeSinceLastAlive = 0; } }; AgentController.prototype.fired = function() { // clear the state first var wasHired = this.isHired; // reset this._init(); // then custom fire if was hired if (wasHired) { this.onFired(); } } this.AgentController = AgentController; this.BROADCAST_AGENTS = BROADCAST_AGENTS; })();