This commit is contained in:
Howard Stearns 2016-02-07 18:29:10 -08:00
parent e52b910e53
commit b08f364447
6 changed files with 265 additions and 30 deletions

View file

@ -66,11 +66,6 @@
dimensions = Vec3.multiply(scale, properties.dimensions);
baton = virtualBaton({
batonName: 'io.highfidelity.tribble',
connectionTest: function (id) {
var connected = baton.validId(id);
print(MyAvatar.sessionUUID, id, connected);
return connected;
},
debugFlow: debug.flow,
debugSend: debug.send,
debugReceive: debug.receive

View file

@ -31,7 +31,8 @@ function virtualBatonf(options) {
var allowedDeviation = number * variability;
return number - allowedDeviation + (Math.random() * 2 * allowedDeviation);
}
// Allow testing outside in a harness of High Fidelity.
// 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,
@ -43,20 +44,20 @@ function virtualBatonf(options) {
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}).
// 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 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.)
// 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); }
@ -126,7 +127,7 @@ function virtualBatonf(options) {
// 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.
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!
//
@ -136,7 +137,7 @@ function virtualBatonf(options) {
//
// 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.
// 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) {
@ -144,8 +145,8 @@ function virtualBatonf(options) {
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.
// 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;
@ -170,7 +171,7 @@ function virtualBatonf(options) {
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
// 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) {
@ -178,7 +179,7 @@ function virtualBatonf(options) {
// timers.clearTimeout(electionWatchdog) and not return.
return;
}
thisTimeout = randomize(electionTimeout, 0.5); // for accurate nSubcribers counting.
thisTimeout = randomize(electionTimeout, 0.5); // Note use in nSubcribers.
electionWatchdog = timers.setTimeout(function () {
electionWatchdog = null;
propose();
@ -208,7 +209,7 @@ function virtualBatonf(options) {
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.
// IWBNI if we started our next proposal right now/here, but we need a decent nNack count.
// Lets save that optimization for another day...
}
});
@ -232,7 +233,7 @@ function virtualBatonf(options) {
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
// 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.
@ -288,14 +289,16 @@ function virtualBatonf(options) {
}
});
// 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 you release the baton held by you,
// whether this is by you calling release(), or by loosing
// 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. Otherwise, you will not participate in further elections.
// being called.
exports.claim = function claim(onElection, onRelease) {
debugFlow('baton:', batonName, instanceId, 'claim');
if (claimCallback) {
@ -314,7 +317,7 @@ function virtualBatonf(options) {
// 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.
if (optionalReplacementOnRelease) { // If you want to change.
releaseCallback = optionalReplacementOnRelease;
}
if (acceptedId() !== instanceId) {
@ -323,7 +326,7 @@ function virtualBatonf(options) {
}
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.
propose();
}
return exports;
};
@ -348,7 +351,7 @@ function virtualBatonf(options) {
propose();
return exports;
}
if (typeof module !== 'undefined') {
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 = 1; // FIXME 200;
var LIFETIME = 30; // FIXME 60; // 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: true, send: true}
});
for (i = 0; (i < numToCreate) && (totalCreated < NUMBER_TO_CREATE); i++) {
Entities.addEntity({

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,220 @@
"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.');
});
});