From dc50efe7ec9895ce0e446eab06907f59352fbb65 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sun, 7 Feb 2016 10:42:43 -0800 Subject: [PATCH] final? --- .../{tribble.js => tribble.x.34.js} | 37 +- examples/libraries/virtualBaton.31.js | 252 ------------- examples/libraries/virtualBaton.39.js | 354 ++++++++++++++++++ 3 files changed, 386 insertions(+), 257 deletions(-) rename examples/entityScripts/{tribble.js => tribble.x.34.js} (68%) delete mode 100644 examples/libraries/virtualBaton.31.js create mode 100644 examples/libraries/virtualBaton.39.js diff --git a/examples/entityScripts/tribble.js b/examples/entityScripts/tribble.x.34.js similarity index 68% rename from examples/entityScripts/tribble.js rename to examples/entityScripts/tribble.x.34.js index 3f84901344..145adfc325 100644 --- a/examples/entityScripts/tribble.js +++ b/examples/entityScripts/tribble.x.34.js @@ -1,5 +1,6 @@ (function () { // See tests/performance/tribbles.js + Script.include("../libraries/virtualBaton.39.js"); var dimensions, oldColor, entityID, editRate = 60, moveRate = 1, @@ -7,7 +8,8 @@ accumulated = 0, increment = {red: 1, green: 1, blue: 1}, hasUpdate = false, - shutdown = false; + shutdown = false, + baton; function nextWavelength(color) { var old = oldColor[color]; if (old === 255) { @@ -35,6 +37,22 @@ Entities.editEntity(entityID, newData); if (!shutdown) { Script.setTimeout(move, nextChange); } } + function startUpdate() { + print('startUpdate', entityID); + hasUpdate = true; + Script.update.connect(update); + } + function stopUpdate() { + print('stopUpdate', entityID, hasUpdate); + if (!hasUpdate) { return; } + hasUpdate = false; + Script.update.disconnect(update); + } + function stopUpdateAndReclaim() { + print('stopUpdateAndReclaim', entityID); + stopUpdate(); + baton.claim(startUpdate, stopUpdateAndReclaim); + } this.preload = function (givenEntityID) { entityID = givenEntityID; var properties = Entities.getEntityProperties(entityID); @@ -45,11 +63,19 @@ moveRate = (moveRate && userData.moveRate) || moveRate; oldColor = properties.color; dimensions = Vec3.multiply(scale, properties.dimensions); + baton = virtualBaton({ + // FIXME: batonName: 'io.highfidelity.tribble:' + entityID, + // If we wanted to have only one tribble change colors, we could do: + batonName: 'io.highfidelity.tribble', + instanceId: entityID + MyAvatar.sessionUUID, + debugFlow: true, + debugSend: true, + debugReceive: true + }); if (editTimeout) { - hasUpdate = true; - Script.update.connect(update); + baton.claim(startUpdate, stopUpdateAndReclaim); if (editTimeout > 0) { - Script.setTimeout(function () { Script.update.disconnect(update); hasUpdate = false; }, editTimeout * 1000); + Script.setTimeout(stopUpdate, editTimeout * 1000); } } if (moveTimeout) { @@ -60,7 +86,8 @@ } }; this.unload = function () { + baton.unload(); shutdown = true; - if (hasUpdate) { Script.update.disconnect(update); } + stopUpdate(); }; }) diff --git a/examples/libraries/virtualBaton.31.js b/examples/libraries/virtualBaton.31.js deleted file mode 100644 index 5df4710d3e..0000000000 --- a/examples/libraries/virtualBaton.31.js +++ /dev/null @@ -1,252 +0,0 @@ -"use strict"; -/*jslint nomen: true, plusplus: true, vars: true */ -/*global Entities, Script, MyAvatar, Messages, AvatarList, print */ -// -// Created by Howard Stearns -// Copyright 2016 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 -// -// Allows cooperating scripts to pass a "virtual baton" between them, -// which is useful when part of a script should only be executed by -// the one participant that is holding this particular baton. -// -// A virtual baton is simply any string agreed upon by the scripts -// that use it. Only one script at a time can hold the baton, and it -// holds it until that script releases it, or the other scripts -// determine that the holding script is not responding. The script -// automatically determines who among claimants has the baton, if anyone, -// and holds an "election" if necessary. -// -// See entityScript/tribble.js as an example, and the functions -// virtualBaton(), claim(), release(). -// - -// Answers a new virtualBaton for the given parameters, of which 'key' -// is required. -virtualBaton = function virtualBaton(options) { - // Answer averages (number +/- variability). Avoids having everyone act in lockstep. - function randomize(number, variability) { - var randomPart = number * variability; - return number - (randomPart / 2) + (Math.random() * randomPart); - } - var key = options.key, - useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, - exports = options.exports || {}, - electionTimeout = options.electionTimeout || randomize(1000, 0.2), // ms. If no winner in this time, hold a new election. - claimCallback, - releaseCallback, - ourId = MyAvatar.sessionUUID; // better be stable! - if (!key) { - throw new Error("A VirtualBaton must specify a key."); - } - function debug() { - print.apply(null, [].map.call(arguments, JSON.stringify)); - } - function debugFlow() { - if (options.debugFlow) { debug.apply(null, arguments); } - } - - // Messages: Just synactic sugar for hooking things up to Messages system. - // We create separate subchannel strings for each operation within our general channelKey, instead of using - // a switch in the receiver. - var channelKey = "io.highfidelity.virtualBaton:" + key, - subchannelHandlers = {}, // Message channel string => {function, op} - subchannelKeys = {}; // operation => Message channel string - function subchannelKey(operation) { return channelKey + ':' + operation; } - function receive(operation, handler) { // Record a handler for an operation on our channelKey - var subKey = subchannelKey(operation); - subchannelHandlers[subKey] = {receiver: handler, op: operation}; - subchannelKeys[operation] = subKey; - Messages.subscribe(subKey); - } - function sendHelper(subchannel, data) { - var message = JSON.stringify(data); - Messages.sendMessage(subchannel, message); - } - function send1(operation, destination, data) { // Send data for an operation to just one destination on our channelKey. - if (options.debugSend) { debug('baton:', ourId, '=>', destination, operation, data); } - sendHelper(subchannelKey(operation) + destination, data); - } - function send(operation, data) { // Send data for an operation on our channelKey. - if (options.debugSend) { debug('baton:', ourId, '=>', '-', operation, data); } - sendHelper(subchannelKeys[operation], data); - } - Messages.messageReceived.connect(function (channel, messageString, senderID) { - var handler = subchannelHandlers[channel]; - if (!handler) { return; } - var data = JSON.parse(messageString); - if (options.debugReceive) { debug('baton:', senderID, '=>', ourId, handler.op, data); } - handler.receiver(data); - }); - - // Internally, this uses the Paxos algorith to hold elections. - // Alternatively, we could have the message server pick and maintain a winner, but it would - // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. - // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer - // (such as the server). - // - // Paxos makes several tests of one "proposal number" versus another, assuming - // that better proposals from the same proposer have a higher number, - // and different proposers use a different set of numbers. We achieve that - // by dividing the "number" into two parts, and integer and a proposerId, - // which keeps the combined number unique and yet still strictly ordered. - function betterNumber(number, best) { - // FIXME restore debug('baton: betterNumber', number, best); - //FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId)); - return (number.number || 0) > best.number; - } - // Paxos Proposer behavior - var nPromises = 0, - proposalNumber = 0, - nQuorum, - mostRecentInterested, - bestPromise = {number: 0}, - electionWatchdog, - recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize - recheckWatchdog; - function propose() { - debugFlow('baton:', ourId, 'propose', !!claimCallback); - if (electionWatchdog) { Script.clearTimeout(electionWatchdog); } - if (!claimCallback) { return; } // We're not participating. - electionWatchdog = Script.setTimeout(propose, electionTimeout); - nPromises = 0; - proposalNumber = Math.max(proposalNumber, bestPromise.number); - nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT! - send('prepare!', {number: ++proposalNumber, proposerId: ourId}); - } - // We create a distinguished promise subchannel for our id, because promises need only be sent to the proposer. - receive('promise' + ourId, function (data) { - if (data.proposerId !== ourId) { return; } // Only the proposer needs to do anything. - mostRecentInterested = mostRecentInterested || data.interested; - if (betterNumber(data, bestPromise)) { - bestPromise = data; - } - if (++nPromises >= nQuorum) { - var answer = {number: data.proposalNumber, proposerId: data.proposerId, winner: data.winner}; - if (!answer.winner) { // we get to pick - answer.winner = claimCallback ? ourId : mostRecentInterested; - } - send('accept!', answer); - } - }); - // Paxos Acceptor behavior - var bestProposal = {number: 0}, accepted = {}; - function acceptedId() { return accepted && accepted.winner; } - receive('prepare!', function (data) { - if (useOptimizations) { // Don't waste time with low future proposals. - proposalNumber = Math.max(proposalNumber, data.number); - } - if (betterNumber(data, bestProposal)) { - bestProposal = data; - if (claimCallback && useOptimizations) { - // Let the proposer know we're interested in the job if the proposer doesn't - // know who else to pick. Avoids needing to start multiple simultaneous proposals. - accepted.interested = ourId; - } - send1('promise', data.proposerId, - accepted.winner ? // data must include proposerId and number so that proposer catalogs results. - {number: accepted.number, winner: accepted.winner, proposerId: data.proposerId, proposalNumber: data.number} : - {proposerId: data.proposerId, proposalNumber: data.number}); - } // FIXME nack? - }); - receive('accept!', function (data) { - if (!betterNumber(bestProposal, data)) { - bestProposal = data; - if (useOptimizations) { - // The Paxos literature describe every acceptor sending 'accepted' to - // every proposer and learner. In our case, these are the same nodes that received - // the 'accept!' message, so we can send to just the originating proposer and invoke - // our own accepted handler directly. - // Note that this optimization cannot be used with Byzantine Paxos (which needs another - // multi-broadcast to detect lying and collusion). - subchannelHandlers[subchannelKey('accepted') + ourId].receiver(data); - send1('accepted', data.proposerId, data); - } else { - send('accepted', data); - } - } - }); - // Paxos Learner behavior. - receive('accepted' + (useOptimizations ? ourId : ''), function (data) { // See note in 'accept!' regarding use of ourId here. - accepted = data; - if (acceptedId() === ourId) { // Note that we might not have been the proposer. - if (electionWatchdog) { - Script.clearTimeout(electionWatchdog); - electionWatchdog = null; - } - if (claimCallback) { - var callback = claimCallback; - claimCallback = undefined; - callback(key); - } else if (!releaseCallback) { // We won, but have been released and are no longer interested. - propose(); // Propose that someone else take the job. - } - } - }); - - receive('release', function (data) { - if (!betterNumber(accepted, data)) { // Unless our data is fresher... - debugFlow('baton:', ourId, 'handle release', data); - accepted.winner = null; // ... allow next proposer to have his way by making this explicitly null (not undefined). - if (recheckWatchdog) { - Script.clearInterval(recheckWatchdog); - recheckWatchdog = null; - } - } - }); - function localRelease() { - var callback = releaseCallback, oldAccepted = accepted; - releaseCallback = undefined; - accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number. - debugFlow('baton: localRelease', key, !!callback); - if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok. - callback(key); // Pass key so that clients may use the same handler for different batons. - return oldAccepted; - } - - // Registers an intent to hold the baton: - // Calls onElection(key) once, if you are elected by the scripts - // to be the unique holder of the baton, which may be never. - // Calls onRelease(key) once, if you release the baton held by you, - // whether this is by you calling release(), or by loosing - // an election when you become disconnected. - // You may claim again at any time after the start of onRelease - // being called. Otherwise, you will not participate in further elections. - exports.claim = function claim(onElection, onRelease) { - debugFlow('baton:', ourId, 'claim'); - if (claimCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim."); - return; - } - if (releaseCallback) { - print("Ignoring attempt to claim virtualBaton " + key + ", which is somehow incorrect released, and that should not happen."); - return; - } - claimCallback = onElection; - releaseCallback = onRelease; - propose(); - }; - - // Release the baton you hold, or just log that you are not holding it. - exports.release = function release(optionalReplacementOnRelease) { - debugFlow('baton:', ourId, 'release'); - if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. - releaseCallback = optionalReplacementOnRelease; - } - if (acceptedId() !== ourId) { - print("Ignoring attempt to release virtualBaton " + key + ", which is not being held."); - return; - } - var released = localRelease(); - if (released) { - send('release', released); // Let everyone know right away, including old number in case we overlap with reclaim. - } - if (!claimCallback) { // No claim set in release callback. - propose(); // We are the distinguished proposer, but we'll pick anyone else interested. - } - }; - - return exports; -}; diff --git a/examples/libraries/virtualBaton.39.js b/examples/libraries/virtualBaton.39.js new file mode 100644 index 0000000000..726a5b2c49 --- /dev/null +++ b/examples/libraries/virtualBaton.39.js @@ -0,0 +1,354 @@ +"use strict"; +/*jslint nomen: true, plusplus: true, vars: true */ +/*global Messages, Script, MyAvatar, AvatarList, Entities, print */ + +// Created by Howard Stearns +// Copyright 2016 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 +// +// Allows cooperating scripts to pass a "virtual baton" between them, +// which is useful when part of a script should only be executed by +// the one participant that is holding this particular baton. +// +// A virtual baton is simply any string agreed upon by the scripts +// that use it. Only one script at a time can hold the baton, and it +// holds it until that script releases it, or the other scripts +// determine that the holding script is not responding. The script +// automatically determines who among claimants has the baton, if anyone, +// and holds an "election" if necessary. +// +// See entityScript/tribble.js as an example, and the functions +// virtualBaton(), claim(), release(). +// + +// Answers a new virtualBaton for the given parameters, of which 'key' +// is required. +function virtualBatonf(options) { + // Answer averages (number +/- variability). Avoids having everyone act in lockstep. + function randomize(number, variability) { + var allowedDeviation = number * variability; + return number - allowedDeviation + (Math.random() * 2 * allowedDeviation); + } + // Allow testing outside in a harness of High Fidelity. + var globals = options.globals || {}, + messages = globals.Messages || Messages, + myAvatar = globals.MyAvatar || MyAvatar, + avatarList = globals.AvatarList || AvatarList, + entities = globals.Entities || Entities, + timers = globals.Script || Script, + log = globals.print || print; + + var batonName = options.batonName, // The identify of the baton. + // instanceId is the identify of this particular copy of the script among all copies using the same batonName + // in the domain. For example, if you wanted only one entity among multiple entity scripts to hold the baton, + // you could specify virtualBaton({batonName: 'someBatonName', id: MyAvatar.sessionUUID + entityID}). + instanceId = options.instanceId || myAvatar.sessionUUID, + // virtualBaton() returns the exports object with properties. You can pass in an object to be side-effected. + exports = options.exports || {}, + // Handy to set false if we believe the optimizations are wrong, or to use both values in a test harness. + useOptimizations = (options.useOptimizations === undefined) ? true : options.useOptimizations, + // + electionTimeout = options.electionTimeout || randomize(500, 0.2), // ms. If no winner in this time, hold a new election. + recheckInterval = options.recheckInterval || randomize(500, 0.2), // ms. Check that winners remain connected. + // If you supply your own instanceId, you can also supply a connectionTest to check that answers + // truthy iff the given id is still valid and connected, and is run at recheckInterval. You can use + // exports.validId (see below), and the default answers truthy if id is valid or a concatenation of two valid + // ids. (This handles the most common cases of instanceId being either (the default) MyAvatar.sessionUUID, an + // entityID, or the concatenation (in either order) of both.) + connectionTest = options.connectionTest || function connectionTest(id) { + var idLength = 38; + if (id.length === idLength) { return exports.validId(id); } + return (id.length === 2 * idLength) && exports.validId(id.slice(0, idLength)) && exports.validId(id.slice(idLength)); + }; + + if (!batonName) { + throw new Error("A virtualBaton must specify a batonName."); + } + // Truthy if id exists as either a connected avatar or valid entity. + exports.validId = function validId(id) { + var avatar = avatarList.getAvatar(id); + if (avatar && (avatar.sessionUUID === id)) { return true; } + var properties = entities.getEntityProperties(id, ['type']); + return properties && properties.type; + }; + + // Various logging, controllable through options. + function debug() { // Display the arguments not just [Object object]. + log.apply(null, [].map.call(arguments, JSON.stringify)); + } + function debugFlow() { + if (options.debugFlow) { debug.apply(null, arguments); } + } + function debugSend(destination, operation, data) { + if (options.debugSend) { debug('baton:', batonName, instanceId, 's=>', destination, operation, data); } + } + function debugReceive(senderID, operation, data) { // senderID is client sessionUUID -- not necessarily instanceID! + if (options.debugReceive) { debug('baton:', batonName, senderID, '=>r', instanceId, operation, data); } + } + + // Messages: Just synactic sugar for hooking things up to Messages system. + // We create separate subchannel strings for each operation within our general channelKey, instead of using + // a switch in the receiver. + var channelKey = "io.highfidelity.virtualBaton:" + batonName, + subchannelHandlers = {}, // Message channel string => {receiver, op} + subchannelKeys = {}; // operation => Message channel string + function subchannelKey(operation) { return channelKey + ':' + operation; } + function receive(operation, handler) { // Record a handler for an operation on our channelKey + var subKey = subchannelKey(operation); + subchannelHandlers[subKey] = {receiver: handler, op: operation}; + subchannelKeys[operation] = subKey; + messages.subscribe(subKey); + } + function sendHelper(subchannel, data) { + var message = JSON.stringify(data); + messages.sendMessage(subchannel, message); + } + function send1(operation, destination, data) { // Send data for an operation to just one destination on our channelKey. + debugSend(destination, operation, data); + sendHelper(subchannelKey(operation) + destination, data); + } + function send(operation, data) { // Send data for an operation on our channelKey. + debugSend('-', operation, data); + sendHelper(subchannelKeys[operation], data); + } + function messageHandler(channel, messageString, senderID) { + var handler = subchannelHandlers[channel]; + if (!handler) { return; } + var data = JSON.parse(messageString); + debugReceive(senderID, handler.op, data); + handler.receiver(data); + } + messages.messageReceived.connect(messageHandler); + + var nPromises = 0, nAccepted = 0, electionWatchdog; + + // It would be great if we had a way to know how many subscribers our channel has. Failing that... + var nNack = 0, previousNSubscribers = 0, lastGathering = 0, thisTimeout = electionTimeout; + function nSubscribers() { // Answer the number of subscribers - or zero to indicate that we have to wait. + // To find nQuorum, we need to know how many scripts are being run using this batonName, which isn't + // the same as the number of clients! + // + // If we overestimate by too much, we may fail to reach consensus, which triggers a new + // election proposal, so we take the number of acceptors to be the max(nPromises, nAccepted) + // + nNack reported in the previous round. + // + // If we understimate by too much, there can be different pockets on the Internet that each + // believe they have agreement on different holders of the baton, which is precisely what + // the virtualBaton is supposed to avoid. Therefore we need to allow 'nack' gather stragglers. + + var now = Date.now(), elapsed = now - lastGathering; + if (elapsed >= thisTimeout) { + previousNSubscribers = Math.max(nPromises, nAccepted) + nNack; + lastGathering = now; + } // ...otherwise we use the previous value unchanged. + + // On startup, we do one proposal that we cannot possibly win, so that we'll + // lock things up for timeout to gather the number of responses. + if (!previousNSubscribers) { + var LARGE_INTEGER = Number.MAX_SAFE_INTEGER || (-1 >>> 1); // QT doesn't define the ECMA constant. Max int will do for our purposes. + previousNSubscribers = LARGE_INTEGER; + } + return previousNSubscribers; + } + + // MAIN ALGORITHM + // + // Internally, this uses the Paxos algorith to hold elections. + // Alternatively, we could have the message server pick and maintain a winner, but it would + // still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages. + // Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer + // (such as the server). + function betterNumber(number, best) { + return (number.number || 0) > best.number; + } + // Paxos Proposer behavior + var proposalNumber = 0, + nQuorum = 0, + bestPromise = {number: 0}, + claimCallback, + releaseCallback; + function propose() { // Make a new proposal, so that we learn/update the proposalNumber and winner. + // Even though we send back a 'nack' if the proposal is obsolete, with net work errors + // there's no way to know for certain that we've failed. The electionWatchdog will try a new + // proposal if we have not been accepted by a quorum after election Timeout. + if (electionWatchdog) { + // If we had a means of determining nSubscribers other than by counting, we could just + // timers.clearTimeout(electionWatchdog) and not return. + return; + } + thisTimeout = randomize(electionTimeout, 0.5); // for accurate nSubcribers counting. + electionWatchdog = timers.setTimeout(function () { + electionWatchdog = null; + propose(); + }, thisTimeout); + var nAcceptors = nSubscribers(); + nQuorum = Math.floor(nAcceptors / 2) + 1; + + proposalNumber = Math.max(proposalNumber, bestPromise.number) + 1; + debugFlow('baton:', batonName, instanceId, 'propose', proposalNumber, + 'claim:', !!claimCallback, 'nAcceptors:', nAcceptors, nPromises, nAccepted, nNack); + nPromises = nAccepted = nNack = 0; + send('prepare!', {number: proposalNumber, proposerId: instanceId}); + } + // We create a distinguished promise subchannel for our id, because promises need only be sent to the proposer. + receive('promise' + instanceId, function (data) { + if (betterNumber(data, bestPromise)) { + bestPromise = data; + } + if ((data.proposalNumber === proposalNumber) && (++nPromises >= nQuorum)) { // Note check for not being a previous round + var answer = {number: data.proposalNumber, proposerId: data.proposerId, winner: bestPromise.winner}; // Not data.number. + if (!answer.winner || (answer.winner === instanceId)) { // We get to pick. + answer.winner = claimCallback ? instanceId : null; + } + send('accept!', answer); + } + }); + receive('nack' + instanceId, function (data) { // An acceptor reports more recent data... + if (data.proposalNumber === proposalNumber) { + nNack++; // For updating nQuorum. + // IWBNI if we started our next proposal now (here, in a setTimeout, but we need a decent nNack count. + // Lets save that optimization for another day... + } + }); + // Paxos Acceptor behavior + var bestProposal = {number: 0}, accepted = {}; + function acceptedId() { return accepted && accepted.winner; } + receive('prepare!', function (data) { + var response = {proposalNumber: data.number, proposerId: data.proposerId}; + if (betterNumber(data, bestProposal)) { + bestProposal = data; + if (accepted.winner) { + response.number = accepted.number; + response.winner = accepted.winner; + } + send1('promise', data.proposerId, response); + } else { + send1('nack', data.proposerId, response); + } + }); + receive('accept!', function (data) { + if (!betterNumber(bestProposal, data)) { + bestProposal = accepted = data; // Update both with current data. Might have missed the proposal earlier. + if (useOptimizations) { + // The Paxos literature describe every acceptor sending 'accepted' to + // every proposer and learner. In our case, these are the same nodes that received + // the 'accept!' message, so we can send to just the originating proposer and invoke + // our own accepted handler directly. + // Note that this optimization cannot be used with Byzantine Paxos (which needs another + // multi-broadcast to detect lying and collusion). + debugSend('/', 'accepted', data); + debugReceive(instanceId, 'accepted', data); // direct on next line, which doesn't get logging. + subchannelHandlers[subchannelKey('accepted') + instanceId].receiver(data); + if (data.proposerId !== instanceId) { // i.e., we didn't already do it directly on the line above. + send1('accepted', data.proposerId, data); + } + } else { + send('accepted', data); + } + } else { + send1('nack', data.proposerId, {proposalNumber: data.number}); + } + }); + // Paxos Learner behavior. + function localRelease() { + var callback = releaseCallback; + debugFlow('baton:', batonName, 'localRelease', 'callback:', !!releaseCallback); + if (!releaseCallback) { return; } // Already released, but we might still receive a stale message. That's ok. + releaseCallback = undefined; + callback(batonName); // Pass batonName so that clients may use the same handler for different batons. + } + receive('accepted' + (useOptimizations ? instanceId : ''), function (data) { // See note in 'accept!' regarding use of instanceId here. + if (betterNumber(accepted, data)) { // Especially when !useOptimizations, we can receive other acceptances late. + return; + } + var oldAccepted = accepted; + debugFlow('baton:', batonName, instanceId, 'accepted', data.number, data.winner); + accepted = data; + // If we are proposer, make sure we get a quorum of acceptances. + if ((data.proposerId === instanceId) && (data.number === proposalNumber) && (++nAccepted >= nQuorum)) { + if (electionWatchdog) { + timers.clearTimeout(electionWatchdog); + electionWatchdog = null; + } + } + // If we are the winner -- regardless of whether we were the proposer. + if (acceptedId() === instanceId) { + if (claimCallback) { + var callback = claimCallback; + claimCallback = undefined; + callback(batonName); + } else if (!releaseCallback) { // We won, but have been released and are no longer interested. + // Propose that someone else take the job. + timers.setTimeout(propose, 0); // Asynchronous to queue message handling if some are synchronous and others not. + } + } else if (releaseCallback && (oldAccepted.winner === instanceId)) { // We've been released by someone else! + localRelease(); // This can happen if enough people thought we'd disconnected. + } + }); + + // Registers an intent to hold the baton: + // Calls onElection(batonName) once, if you are elected by the scripts + // to be the unique holder of the baton, which may be never. + // Calls onRelease(batonName) once, if you release the baton held by you, + // whether this is by you calling release(), or by loosing + // an election when you become disconnected. + // You may claim again at any time after the start of onRelease + // being called. Otherwise, you will not participate in further elections. + exports.claim = function claim(onElection, onRelease) { + debugFlow('baton:', batonName, instanceId, 'claim'); + if (claimCallback) { + log("Ignoring attempt to claim virtualBaton " + batonName + ", which is already waiting for claim."); + return; + } + if (releaseCallback) { + log("Ignoring attempt to claim virtualBaton " + batonName + ", which is somehow incorrect released, and that should not happen."); + return; + } + claimCallback = onElection; + releaseCallback = onRelease; + propose(); + return exports; // Allows chaining. e.g., var baton = virtualBaton({batonName: 'foo'}.claim(onClaim, onRelease); + }; + // Release the baton you hold, or just log that you are not holding it. + exports.release = function release(optionalReplacementOnRelease) { + debugFlow('baton:', batonName, instanceId, 'release'); + if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't. + releaseCallback = optionalReplacementOnRelease; + } + if (acceptedId() !== instanceId) { + log("Ignoring attempt to release virtualBaton " + batonName + ", which is not being held."); + return; + } + localRelease(); + if (!claimCallback) { // No claim set in release callback. + propose(); // We are the distinguished proposer, but we'll pick anyone else interested, else set it to null. + } + return exports; + }; + exports.recheckWatchdog = timers.setInterval(function recheck() { + var holder = acceptedId(); // If we're waiting and we notice the holder is gone, ... + if (holder && claimCallback && !electionWatchdog && !connectionTest(holder)) { + propose(); // ... propose an election. + } + }, recheckInterval); + exports.unload = function unload() { // Disconnect from everything. + messages.messageReceived.disconnect(messageHandler); + timers.clearInterval(exports.recheckWatchdog); + if (electionWatchdog) { timers.clearTimeout(electionWatchdog); } + electionWatchdog = claimCallback = releaseCallback = null; + Object.keys(subchannelHandlers).forEach(messages.unsubscribe); + debugFlow('baton:', batonName, instanceId, 'unload'); + return exports; + }; + + // Gather nAcceptors by making two proposals with some gathering time, even without a claim. + propose(); + return exports; +} +if (typeof module !== 'undefined') { + module.exports = virtualBatonf; +} else { + virtualBaton = virtualBatonf; +}