Merge pull request #7042 from howard-stearns/virtualBaton

Virtual baton
This commit is contained in:
Brad Hefta-Gaub 2016-02-10 13:48:34 -08:00
commit d7db08d150
7 changed files with 694 additions and 13 deletions

View file

@ -1,5 +1,6 @@
(function () {
// See tests/performance/tribbles.js
Script.include("../libraries/virtualBaton.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) {
@ -27,13 +29,37 @@
accumulated = 0;
}
}
function randomCentered() { return Math.random() - 0.5; }
function randomVector() { return {x: randomCentered() * dimensions.x, y: randomCentered() * dimensions.y, z: randomCentered() * dimensions.z}; }
function randomCentered() {
return Math.random() - 0.5;
}
function randomVector() {
return {x: randomCentered() * dimensions.x, y: randomCentered() * dimensions.y, z: randomCentered() * dimensions.z};
}
function move() {
var newData = {velocity: Vec3.sum({x: 0, y: 1, z: 0}, randomVector()), angularVelocity: Vec3.multiply(Math.PI, randomVector())};
var nextChange = Math.ceil(Math.random() * 2000 / moveRate);
Entities.editEntity(entityID, newData);
if (!shutdown) { Script.setTimeout(move, nextChange); }
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;
@ -41,26 +67,35 @@
var userData = properties.userData && JSON.parse(properties.userData);
var moveTimeout = userData ? userData.moveTimeout : 0;
var editTimeout = userData ? userData.editTimeout : 0;
var debug = (userData && userData.debug) || {};
editRate = (userData && userData.editRate) || editRate;
moveRate = (moveRate && userData.moveRate) || moveRate;
oldColor = properties.color;
dimensions = Vec3.multiply(scale, properties.dimensions);
baton = virtualBaton({
batonName: 'io.highfidelity.tribble:' + entityID, // One winner for each entity
debugFlow: debug.flow,
debugSend: debug.send,
debugReceive: debug.receive
});
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) {
Script.setTimeout(move, 1000);
if (moveTimeout > 0) {
Script.setTimeout(function () { shutdown = true; }, moveTimeout * 1000);
Script.setTimeout(function () {
shutdown = true;
}, moveTimeout * 1000);
}
}
};
this.unload = function () {
baton.unload();
shutdown = true;
if (hasUpdate) { Script.update.disconnect(update); }
stopUpdate();
};
})

View file

@ -0,0 +1,381 @@
"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; // one side of the deviation range
var allowedDeviationRange = allowedDeviation * 2; // total range for +/- deviation
var randomDeviation = Math.random() * allowedDeviationRange;
var result = number - allowedDeviation + randomDeviation;
return result;
}
// Allow testing outside in a harness outside of High Fidelity.
// See sourceCodeSandbox/tests/mocha/test/testVirtualBaton.js
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', instanceId: 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 might also supply a connectionTest 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.
// 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' to 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 close, so that we'll
// lock things up for the full electionTimeout to gather 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 network 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); // Note use in nSubcribers.
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 right now/here, 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 && connectionTest(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 describes 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.
}
});
// Public Interface
//
// 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 the baton held by you is released,
// whether this is by you calling release(), or by losing
// an election when you become disconnected.
// You may claim again at any time after the start of onRelease
// being called.
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) { // If you want to change.
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();
}
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)) {
bestPromise.winner = null; // used if the quorum agrees that old winner is not there
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') { // Allow testing in nodejs.
module.exports = virtualBatonf;
} else {
virtualBaton = virtualBatonf;
}

View file

@ -14,8 +14,8 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print;
// The _TIMEOUT parameters can be 0 for no activity, and -1 to be active indefinitely.
//
var NUMBER_TO_CREATE = 200;
var LIFETIME = 60; // seconds
var NUMBER_TO_CREATE = 100;
var LIFETIME = 120; // seconds
var EDIT_RATE = 60; // hz
var EDIT_TIMEOUT = -1;
var MOVE_RATE = 1; // hz
@ -68,7 +68,8 @@ Script.setInterval(function () {
moveTimeout: MOVE_TIMEOUT,
moveRate: MOVE_RATE,
editTimeout: EDIT_TIMEOUT,
editRate: EDIT_RATE
editRate: EDIT_RATE,
debug: {flow: false, send: false, receive: false}
});
for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) {
Entities.addEntity({

View file

@ -4,7 +4,7 @@ enable_testing()
# add the test directories
file(GLOB TEST_SUBDIRS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/*")
list(REMOVE_ITEM TEST_SUBDIRS "CMakeFiles")
list(REMOVE_ITEM TEST_SUBDIRS "CMakeFiles" "mocha")
foreach(DIR ${TEST_SUBDIRS})
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${DIR}")
set(TEST_PROJ_NAME ${DIR})

5
tests/mocha/README.md Normal file
View file

@ -0,0 +1,5 @@
mocha tests of javascript code (e.g., from ../../examples/libraries/).
```
npm install
npm test
```

11
tests/mocha/package.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "HighFidelityTests",
"version": "1.0.0",
"scripts": {
"test": "mocha"
},
"license": "Apache 2.0",
"devDependencies": {
"mocha": "^2.2.1"
}
}

View file

@ -0,0 +1,248 @@
"use strict";
/*jslint nomen: true, plusplus: true, vars: true */
var assert = require('assert');
var mocha = require('mocha'), describe = mocha.describe, it = mocha.it, after = mocha.after;
var virtualBaton = require('../../../examples/libraries/virtualBaton.js');
describe('temp', function () {
var messageCount = 0, testStart = Date.now();
function makeMessager(nodes, me, mode) { // shim for High Fidelity Message system
function noopSend(channel, string, source) {
}
function hasChannel(node, channel) {
return -1 !== node.subscribed.indexOf(channel);
}
function sendSync(channel, message, nodes, skip) {
nodes.forEach(function (node) {
if (!hasChannel(node, channel) || (node === skip)) {
return;
}
node.sender(channel, message, me.name);
});
}
nodes.forEach(function (node) {
node.sender = node.sender || noopSend;
node.subscribed = node.subscribed || [];
});
return {
subscriberCount: function () {
var c = 0;
nodes.forEach(function (n) {
if (n.subscribed.length) {
c++;
}
});
return c;
},
subscribe: function (channel) {
me.subscribed.push(channel);
},
unsubscribe: function (channel) {
me.subscribed.splice(me.subscribed.indexOf(channel), 1);
},
sendMessage: function (channel, message) {
if ((mode === 'immediate2Me') && hasChannel(me, channel)) {
me.sender(channel, message, me.name);
}
if (mode === 'immediate') {
sendSync(channel, message, nodes, null);
} else {
process.nextTick(function () {
sendSync(channel, message, nodes, (mode === 'immediate2Me') ? me : null);
});
}
},
messageReceived: {
connect: function (f) {
me.sender = function (c, m, i) {
messageCount++; f(c, m, i);
};
},
disconnect: function () {
me.sender = noopSend;
}
}
};
}
var debug = {}; //{flow: true, send: false, receive: false};
function makeBaton(testKey, nodes, node, debug, mode, optimize) {
debug = debug || {};
var baton = virtualBaton({
batonName: testKey,
debugSend: debug.send,
debugReceive: debug.receive,
debugFlow: debug.flow,
useOptimizations: optimize,
connectionTest: function (id) {
return baton.validId(id);
},
globals: {
Messages: makeMessager(nodes, node, mode),
MyAvatar: {sessionUUID: node.name},
Script: {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval
},
AvatarList: {
getAvatar: function (id) {
return {sessionUUID: id};
}
},
Entities: {getEntityProperties: function () {
}},
print: console.log
}
});
return baton;
}
function noRelease(batonName) {
assert.ok(!batonName, "should not release");
}
function defineABunch(mode, optimize) {
function makeKey(prefix) {
return prefix + mode + (optimize ? '-opt' : '');
}
var testKeys = makeKey('single-');
it(testKeys, function (done) {
var nodes = [{name: 'a'}];
var a = makeBaton(testKeys, nodes, nodes[0], debug, mode).claim(function (key) {
console.log('claimed a');
assert.equal(testKeys, key);
a.unload();
done();
}, noRelease);
});
var testKeydp = makeKey('dual-parallel-');
it(testKeydp, function (done) {
this.timeout(10000);
var nodes = [{name: 'ap'}, {name: 'bp'}];
var a = makeBaton(testKeydp, nodes, nodes[0], debug, mode, optimize),
b = makeBaton(testKeydp, nodes, nodes[1], debug, mode, optimize);
function accepted(key) { // Under some circumstances of network timing, either a or b can win.
console.log('claimed ap');
assert.equal(testKeydp, key);
done();
}
a.claim(accepted, noRelease);
b.claim(accepted, noRelease);
});
var testKeyds = makeKey('dual-serial-');
it(testKeyds, function (done) {
var nodes = [{name: 'as'}, {name: 'bs'}],
gotA = false,
gotB = false;
makeBaton(testKeyds, nodes, nodes[0], debug, mode, optimize).claim(function (key) {
console.log('claimed as', key);
assert.ok(!gotA, "should not get A after B");
gotA = true;
done();
}, noRelease);
setTimeout(function () {
makeBaton(testKeyds, nodes, nodes[1], debug, mode, optimize).claim(function (key) {
console.log('claimed bs', key);
assert.ok(!gotB, "should not get B after A");
gotB = true;
done();
}, noRelease);
}, 500);
});
var testKeydsl = makeKey('dual-serial-long-');
it(testKeydsl, function (done) {
this.timeout(5000);
var nodes = [{name: 'al'}, {name: 'bl'}],
gotA = false,
gotB = false,
releaseA = false;
makeBaton(testKeydsl, nodes, nodes[0], debug, mode, optimize).claim(function (key) {
console.log('claimed al', key);
assert.ok(!gotB, "should not get A after B");
gotA = true;
if (!gotB) {
done();
}
}, function () {
assert.ok(gotA, "Should claim it first");
releaseA = true;
if (gotB) {
done();
}
});
setTimeout(function () {
makeBaton(testKeydsl, nodes, nodes[1], debug, mode, optimize).claim(function (key) {
console.log('claimed bl', key);
gotB = true;
if (releaseA) {
done();
}
}, noRelease);
}, 3000);
});
var testKeydsr = makeKey('dual-serial-with-release-');
it(testKeydsr, function (done) {
this.timeout(5000);
var nodes = [{name: 'asr'}, {name: 'bsr'}],
gotClaimA = false,
gotReleaseA = false,
a = makeBaton(testKeydsr, nodes, nodes[0], debug, mode, optimize),
b = makeBaton(testKeydsr, nodes, nodes[1], debug, mode, optimize);
a.claim(function (key) {
console.log('claimed asr');
assert.equal(testKeydsr, key);
gotClaimA = true;
b.claim(function (key) {
console.log('claimed bsr');
assert.equal(testKeydsr, key);
assert.ok(gotReleaseA);
done();
}, noRelease);
a.release();
}, function (key) {
console.log('released asr');
assert.equal(testKeydsr, key);
assert.ok(gotClaimA);
gotReleaseA = true;
});
});
var testKeydpr = makeKey('dual-parallel-with-release-');
it(testKeydpr, function (done) {
this.timeout(5000);
var nodes = [{name: 'ar'}, {name: 'br'}];
var a = makeBaton(testKeydpr, nodes, nodes[0], debug, mode, optimize),
b = makeBaton(testKeydpr, nodes, nodes[1], debug, mode, optimize),
gotClaimA = false,
gotReleaseA = false,
gotClaimB = false;
a.claim(function (key) {
console.log('claimed ar');
assert.equal(testKeydpr, key);
gotClaimA = true;
assert.ok(!gotClaimB, "if b claimed, should not get a");
a.release();
}, function (key) {
console.log('released ar');
assert.equal(testKeydpr, key);
assert.ok(gotClaimA);
gotReleaseA = true;
});
b.claim(function (key) {
console.log('claimed br', gotClaimA ? 'with' : 'without', 'ar first');
assert.equal(testKeydpr, key);
gotClaimB = true;
assert.ok(!gotClaimA || gotReleaseA);
done();
}, noRelease);
});
}
function defineAllModeTests(optimize) {
defineABunch('delayed', optimize);
defineABunch('immediate2Me', optimize);
defineABunch('immediate', optimize);
}
defineAllModeTests(true);
defineAllModeTests(false);
after(function () {
console.log(messageCount, 'messages sent over', (Date.now() - testStart), 'ms.');
});
});