mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-07-24 09:24:15 +02:00
update
This commit is contained in:
parent
59f1cdfc18
commit
055de61ec6
2 changed files with 48 additions and 32 deletions
|
@ -26,18 +26,21 @@
|
||||||
// Answers a new virtualBaton for the given parameters, of which 'key'
|
// Answers a new virtualBaton for the given parameters, of which 'key'
|
||||||
// is required.
|
// is required.
|
||||||
virtualBaton = function virtualBaton(options) {
|
virtualBaton = function virtualBaton(options) {
|
||||||
var key = options.key,
|
var key = options.key,
|
||||||
channel = "io.highfidelity.virtualBaton:" + key,
|
channel = "io.highfidelity.virtualBaton:" + key,
|
||||||
exports = options.exports || {},
|
exports = options.exports || {},
|
||||||
claimCallback,
|
claimCallback,
|
||||||
releaseCallback,
|
releaseCallback,
|
||||||
// paxos proposer state
|
// paxos proposer state
|
||||||
nPromises = 0,
|
nPromises = 0,
|
||||||
|
proposalNumber = 0,
|
||||||
nQuorum,
|
nQuorum,
|
||||||
mostRecentInterested,
|
mostRecentInterested,
|
||||||
bestPromise = {number: 0},
|
bestPromise = {number: 0},
|
||||||
electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election
|
electionTimeout = options.electionTimeout || 1000, // ms. If no winner in this time, hold a new election. FIXME randomize
|
||||||
electionWatchdog,
|
electionWatchdog,
|
||||||
|
recheckInterval = options.recheckInterval || 1000, // ms. Check that winners remain connected. FIXME rnadomize
|
||||||
|
recheckWatchdog,
|
||||||
// paxos acceptor state
|
// paxos acceptor state
|
||||||
bestProposal = {number: 0},
|
bestProposal = {number: 0},
|
||||||
accepted = {};
|
accepted = {};
|
||||||
|
@ -45,23 +48,26 @@ virtualBaton = function virtualBaton(options) {
|
||||||
throw new Error("A VirtualBaton must specify a key.");
|
throw new Error("A VirtualBaton must specify a key.");
|
||||||
}
|
}
|
||||||
function debug() {
|
function debug() {
|
||||||
print.apply(null, [].map.call(arguments, JSON.stringify));
|
print.apply(print, [].map.call(arguments, JSON.stringify)); // fixme no console
|
||||||
|
}
|
||||||
|
function debugFlow() {
|
||||||
|
if (options.debugFlow) { debug.apply(null, arguments); }
|
||||||
}
|
}
|
||||||
function send(operation, data) {
|
function send(operation, data) {
|
||||||
debug('baton: send', operation, data);
|
if (options.debugSend) { debug('baton:', MyAvatar.sessionUUID, '=>', '-', operation, data); }
|
||||||
var message = JSON.stringify({op: operation, data: data});
|
var message = JSON.stringify({op: operation, data: data});
|
||||||
Messages.sendMessage(channel, message);
|
Messages.sendMessage(channel, message);
|
||||||
}
|
}
|
||||||
function doRelease() {
|
function localRelease() {
|
||||||
var callback = releaseCallback, oldAccepted = accepted;
|
var callback = releaseCallback, oldAccepted = accepted;
|
||||||
releaseCallback = undefined;
|
releaseCallback = undefined;
|
||||||
accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId};
|
accepted = {number: oldAccepted.number, proposerId: oldAccepted.proposerId}; // A copy without winner assigned, preserving number.
|
||||||
debug('baton: doRelease', key, callback);
|
debugFlow('baton: localRelease', key, !!callback);
|
||||||
if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok.
|
if (!callback) { return; } // Already released, but we might still receive a stale message. That's ok.
|
||||||
Messages.messageReceived.disconnect(messageHandler);
|
//Messages.messageReceived.disconnect(messageHandler);
|
||||||
Messages.unsubscribe(channel); // Messages currently allow publishing without subscription.
|
//Messages.unsubscribe(channel); // Messages currently allow publishing without subscription.
|
||||||
send('release', oldAccepted); // This order is less crufty.
|
|
||||||
callback(key); // Pass key so that clients may use the same handler for different batons.
|
callback(key); // Pass key so that clients may use the same handler for different batons.
|
||||||
|
return oldAccepted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internally, this uses the Paxos algorith to hold elections.
|
// Internally, this uses the Paxos algorith to hold elections.
|
||||||
|
@ -69,38 +75,37 @@ virtualBaton = function virtualBaton(options) {
|
||||||
// still have to deal with the same issues of verification in the presence of lost/delayed/reordered messages.
|
// 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
|
// Paxos is known to be optimal under these circumstances, except that its best to have a dedicated proposer
|
||||||
// (such as the server).
|
// (such as the server).
|
||||||
function acceptedId() { return accepted && accepted.winner; } // fixme doesn't need to be so fancy any more?
|
function acceptedId() { return accepted && accepted.winner; }
|
||||||
// Paxos makes several tests of one "proposal number" versus another, assuming
|
// Paxos makes several tests of one "proposal number" versus another, assuming
|
||||||
// that better proposals from the same proposer have a higher number,
|
// that better proposals from the same proposer have a higher number,
|
||||||
// and different proposers use a different set of numbers. We achieve that
|
// and different proposers use a different set of numbers. We achieve that
|
||||||
// by dividing the "number" into two parts, and integer and a proposerId,
|
// by dividing the "number" into two parts, and integer and a proposerId,
|
||||||
// which keeps the combined number unique and yet still strictly ordered.
|
// which keeps the combined number unique and yet still strictly ordered.
|
||||||
function betterNumber(number, best) {
|
function betterNumber(number, best) {
|
||||||
debug('baton: betterNumber', number, best);
|
// FIXME restore debug('baton: betterNumber', number, best);
|
||||||
//FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId));
|
//FIXME return ((number.number || 0) > best.number) && (!best.proposerId || (number.proposerId >= best.proposerId));
|
||||||
return (number.number || 0) > best.number;
|
return (number.number || 0) > best.number;
|
||||||
}
|
}
|
||||||
function propose(claim) {
|
function propose() {
|
||||||
debug('baton: propose', claim);
|
debugFlow('baton:', MyAvatar.sessionUUID, 'propose', !!claimCallback);
|
||||||
if (electionWatchdog) { Script.clearTimeout(electionWatchdog); }
|
if (electionWatchdog) { Script.clearTimeout(electionWatchdog); }
|
||||||
if (!claimCallback) { return; } // We're not participating.
|
if (!claimCallback) { return; } // We're not participating.
|
||||||
nPromises = 0;
|
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!
|
nQuorum = Math.floor(AvatarList.getAvatarIdentifiers().length / 2) + 1; // N.B.: ASSUMES EVERY USER IS RUNNING THE SCRIPT!
|
||||||
bestPromise = {number: ++bestPromise.number, proposerId: MyAvatar.sessionUUID, winner: claim};
|
send('prepare!', {number: ++proposalNumber, proposerId: MyAvatar.sessionUUID});
|
||||||
send('prepare!', bestPromise);
|
electionWatchdog = Script.setTimeout(propose, electionTimeout);
|
||||||
function reclaim() { propose(claim); }
|
|
||||||
electionWatchdog = Script.setTimeout(reclaim, electionTimeout);
|
|
||||||
}
|
}
|
||||||
function messageHandler(messageChannel, messageString, senderID) {
|
function messageHandler(messageChannel, messageString, senderID) {
|
||||||
if (messageChannel !== channel) { return; }
|
if (messageChannel !== channel) { return; }
|
||||||
var message = JSON.parse(messageString), data = message.data;
|
var message = JSON.parse(messageString), data = message.data;
|
||||||
debug('baton: received from', senderID, message.op, data);
|
if (options.debugReceive) { debug('baton:', senderID, '=>', MyAvatar.sessionUUID, message.op, data); }
|
||||||
switch (message.op) {
|
switch (message.op) {
|
||||||
case 'prepare!':
|
case 'prepare!':
|
||||||
// Optimization: Don't waste time with low future proposals.
|
// Optimization: Don't waste time with low future proposals.
|
||||||
// Does not remove the need for betterNumber() to consider proposerId, because
|
// Does not remove the need for betterNumber() to consider proposerId, because
|
||||||
// participants might not receive this prepare! message before their next proposal.
|
// participants might not receive this prepare! message before their next proposal.
|
||||||
//FIXME bestPromise.number = Math.max(bestPromise.number, data.number);
|
proposalNumber = Math.max(proposalNumber, data.number);
|
||||||
|
|
||||||
if (betterNumber(data, bestProposal)) {
|
if (betterNumber(data, bestProposal)) {
|
||||||
bestProposal = data;
|
bestProposal = data;
|
||||||
|
@ -111,7 +116,7 @@ virtualBaton = function virtualBaton(options) {
|
||||||
}
|
}
|
||||||
send('promise', accepted.winner ? // data must include proposerId so that proposer catalogs results.
|
send('promise', accepted.winner ? // data must include proposerId so that proposer catalogs results.
|
||||||
{number: accepted.number, proposerId: data.proposerId, winner: accepted.winner} :
|
{number: accepted.number, proposerId: data.proposerId, winner: accepted.winner} :
|
||||||
{proposerId: data.proposerId});
|
{number: data.number, proposerId: data.proposerId});
|
||||||
} // FIXME nack?
|
} // FIXME nack?
|
||||||
break;
|
break;
|
||||||
case 'promise':
|
case 'promise':
|
||||||
|
@ -130,11 +135,12 @@ virtualBaton = function virtualBaton(options) {
|
||||||
case 'accept!':
|
case 'accept!':
|
||||||
if (!betterNumber(bestProposal, data)) {
|
if (!betterNumber(bestProposal, data)) {
|
||||||
accepted = data;
|
accepted = data;
|
||||||
send('accepted', accepted);
|
//send('accepted', accepted); // With the collapsed roles here, do we need this message? Maybe just go to 'accepted' case here?
|
||||||
|
messageHandler(messageChannel, JSON.stringify({op: 'accepted', data: accepted}), senderID);
|
||||||
}
|
}
|
||||||
// FIXME: start interval (with a little random offset?) that claims if winner is ever not in AvatarList and we still claimCallback
|
|
||||||
break;
|
break;
|
||||||
case 'accepted':
|
case 'accepted':
|
||||||
|
if (betterNumber(accepted, data)) { return; }
|
||||||
accepted = data;
|
accepted = data;
|
||||||
if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not have been the proposer.
|
if (acceptedId() === MyAvatar.sessionUUID) { // Note that we might not have been the proposer.
|
||||||
if (electionWatchdog) {
|
if (electionWatchdog) {
|
||||||
|
@ -145,7 +151,7 @@ virtualBaton = function virtualBaton(options) {
|
||||||
var callback = claimCallback;
|
var callback = claimCallback;
|
||||||
claimCallback = undefined;
|
claimCallback = undefined;
|
||||||
callback(key);
|
callback(key);
|
||||||
} else { // We won, but are no longer interested.
|
} else if (!releaseCallback) { // We won, but have been released and are no longer interested.
|
||||||
propose(); // Propose that someone else take the job.
|
propose(); // Propose that someone else take the job.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,12 +159,19 @@ virtualBaton = function virtualBaton(options) {
|
||||||
case 'release':
|
case 'release':
|
||||||
if (!betterNumber(accepted, data)) { // Unless our data is fresher...
|
if (!betterNumber(accepted, data)) { // Unless our data is fresher...
|
||||||
accepted.winner = undefined; // ... allow next proposer to have his way.
|
accepted.winner = undefined; // ... allow next proposer to have his way.
|
||||||
|
if (recheckWatchdog) {
|
||||||
|
Script.clearInterval(recheckWatchdog);
|
||||||
|
recheckWatchdog = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
print("Unrecognized virtualBaton message:", message);
|
print("Unrecognized virtualBaton message:", message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Messages.messageReceived.connect(messageHandler); // FIXME MUST BE DONE. quorum will be wrong if no one claims and we only subscrbe with claims
|
||||||
|
Messages.subscribe(channel); // FIXME
|
||||||
|
|
||||||
// Registers an intent to hold the baton:
|
// Registers an intent to hold the baton:
|
||||||
// Calls onElection(key) once, if you are elected by the scripts
|
// Calls onElection(key) once, if you are elected by the scripts
|
||||||
// to be the unique holder of the baton, which may be never.
|
// to be the unique holder of the baton, which may be never.
|
||||||
|
@ -168,7 +181,7 @@ virtualBaton = function virtualBaton(options) {
|
||||||
// You may claim again at any time after the start of onRelease
|
// You may claim again at any time after the start of onRelease
|
||||||
// being called. Otherwise, you will not participate in further elections.
|
// being called. Otherwise, you will not participate in further elections.
|
||||||
exports.claim = function claim(onElection, onRelease) {
|
exports.claim = function claim(onElection, onRelease) {
|
||||||
debug('baton: claim');
|
debugFlow('baton:', MyAvatar.sessionUUID, 'claim');
|
||||||
if (claimCallback) {
|
if (claimCallback) {
|
||||||
print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim.");
|
print("Ignoring attempt to claim virtualBaton " + key + ", which is already waiting for claim.");
|
||||||
return;
|
return;
|
||||||
|
@ -179,14 +192,14 @@ virtualBaton = function virtualBaton(options) {
|
||||||
}
|
}
|
||||||
claimCallback = onElection;
|
claimCallback = onElection;
|
||||||
releaseCallback = onRelease;
|
releaseCallback = onRelease;
|
||||||
Messages.messageReceived.connect(messageHandler);
|
//Messages.messageReceived.connect(messageHandler);
|
||||||
Messages.subscribe(channel);
|
//Messages.subscribe(channel);
|
||||||
propose(MyAvatar.sessionUUID);
|
propose();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Release the baton you hold, or just log that you are not holding it.
|
// Release the baton you hold, or just log that you are not holding it.
|
||||||
exports.release = function release(optionalReplacementOnRelease) {
|
exports.release = function release(optionalReplacementOnRelease) {
|
||||||
debug('baton: release');
|
debugFlow('baton:', MyAvatar.sessionUUID, 'release');
|
||||||
if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't.
|
if (optionalReplacementOnRelease) { // E.g., maybe normal onRelease reclaims, but at shutdown you explicitly don't.
|
||||||
releaseCallback = optionalReplacementOnRelease;
|
releaseCallback = optionalReplacementOnRelease;
|
||||||
}
|
}
|
||||||
|
@ -194,7 +207,10 @@ virtualBaton = function virtualBaton(options) {
|
||||||
print("Ignoring attempt to release virtualBaton " + key + ", which is not being held.");
|
print("Ignoring attempt to release virtualBaton " + key + ", which is not being held.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
doRelease();
|
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.
|
if (!claimCallback) { // No claim set in release callback.
|
||||||
propose(); // We are the distinguished proposer, but we'll pick anyone else interested.
|
propose(); // We are the distinguished proposer, but we'll pick anyone else interested.
|
||||||
}
|
}
|
|
@ -12,9 +12,9 @@ var Vec3, Quat, MyAvatar, Entities, Camera, Script, print;
|
||||||
// All participants should run the test script.
|
// All participants should run the test script.
|
||||||
|
|
||||||
|
|
||||||
Script.include("../libraries/virtualBaton.27.js");
|
Script.include("../libraries/virtualBaton.29.js");
|
||||||
var TICKER_INTERVAL = 1000; // ms
|
var TICKER_INTERVAL = 1000; // ms
|
||||||
var baton = virtualBaton({key: 'io.highfidelity.testBaton'});
|
var baton = virtualBaton({key: 'io.highfidelity.testBaton', debugSend: true, debugFlow: true, debugReceive: true});
|
||||||
var ticker, countDown;
|
var ticker, countDown;
|
||||||
|
|
||||||
// Tick every TICKER_INTERVAL.
|
// Tick every TICKER_INTERVAL.
|
||||||
|
|
Loading…
Reference in a new issue