package freenet.node.probe;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import freenet.config.InvalidConfigValueException;
import freenet.config.NodeNeedRestartException;
import freenet.config.SubConfig;
import freenet.io.comm.AsyncMessageFilterCallback;
import freenet.io.comm.ByteCounter;
import freenet.io.comm.DMT;
import freenet.io.comm.DisconnectedException;
import freenet.io.comm.Message;
import freenet.io.comm.MessageFilter;
import freenet.io.comm.NotConnectedException;
import freenet.io.comm.PeerContext;
import freenet.node.Location;
import freenet.node.Node;
import freenet.node.OpennetManager;
import freenet.node.PeerNode;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.api.BooleanCallback;
import freenet.support.api.LongCallback;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
/**
* Handles starting, routing, and responding to Metropolis-Hastings corrected probes.
*
* Possible future additions to these probes' results include:
* <ul>
* <li>Starting a regular request for a key.</li>
* <li>Success rates for remote requests by HTL; perhaps over some larger amount of time than the past hour.</li>
* </ul>
*
* @see freenet.node.probe Explanation of Metropolis-Hastings correction
*/
public class Probe implements ByteCounter {
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
private static volatile boolean logWARNING;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logWARNING = Logger.shouldLog(Logger.LogLevel.WARNING, this);
logMINOR = Logger.shouldLog(Logger.LogLevel.MINOR, this);
logDEBUG = Logger.shouldLog(Logger.LogLevel.DEBUG, this);
}
});
}
private final static String SOURCE_DISCONNECT = "Previous step in probe chain no longer connected.";
/**
* Maximum hopsToLive value to clamp requests to.
*/
public static final byte MAX_HTL = 70;
/**
* Maximum number of forwarding attempts to make before failing with DISCONNECTED.
*/
public static final int MAX_SEND_ATTEMPTS = 50;
/**
* Probability of HTL decrement at HTL = 1.
*/
public static final float DECREMENT_PROBABILITY = 0.2f;
/**
* In ms, per HTL above HTL = 1.
*/
public static final long TIMEOUT_PER_HTL = SECONDS.toMillis(3);
/**
* In ms, to account for probabilistic decrement at HTL = 1.
*/
public static final long TIMEOUT_HTL1 = (long) (TIMEOUT_PER_HTL / DECREMENT_PROBABILITY);
/**
* To make the timing less obvious when a node responds with a local result instead of forwarding at
* HTL = 1, delay for a number of milliseconds, specifically an exponential distribution with this constant as
* its mean.
*/
public static final long WAIT_BASE = SECONDS.toMillis(1);
/**
* Maximum number of milliseconds to wait before sending a response.
*/
public static final long WAIT_MAX = SECONDS.toMillis(2);
/**
* Maximum number of probes accepted from a single peer in the past minute.
*/
public final int COUNTER_MAX_PEER = 10;
/**
* Maximum number of probes started locally in the past minute. This is the maximum conceivable value; the
* probes should be used with a number of requests per minute closer to the per-peer limit times the minimum
* expected number of peers. Around this value, and certainly above it, remote OVERLOADs may start coming
* in, which are not useful. The Metropolis-Hastings correction makes behavior potentially inconsistent, so
* keeping an eye on remote OVERLOADs is wise.
*/
public final int COUNTER_MAX_LOCAL = COUNTER_MAX_PEER * OpennetManager.MAX_PEERS_FOR_SCALING;
/**
* Number of accepted probes in the last minute, keyed by peer.
*/
private final Map<PeerNode, Counter> accepted;
private final Node node;
private final Timer timer;
//Whether to respond to different types of probe requests.
private volatile boolean respondBandwidth;
private volatile boolean respondBuild;
private volatile boolean respondIdentifier;
private volatile boolean respondLinkLengths;
private volatile boolean respondLocation;
private volatile boolean respondStoreSize;
private volatile boolean respondUptime;
private volatile boolean respondRejectStats;
private volatile boolean respondOverallBulkOutputCapacityUsage;
private volatile long probeIdentifier;
/**
* Applies multiplicative Gaussian noise of mean 1.0 and the specified sigma to the input value.
* @param input Value to apply noise to.
* @param sigma Proportion change at one standard deviation.
* @return Value +/- Gaussian percentage.
*/
private final double randomNoise(final double input, final double sigma) {
return node.nodeStats.randomNoise(input, sigma);
}
/**
* Counts as probe request transfer.
* @param bytes Bytes received.
*/
@Override
public void sentBytes(int bytes) {
node.nodeStats.probeRequestCtr.sentBytes(bytes);
}
/**
* Counts as probe request transfer.
* @param bytes Bytes received.
*/
@Override
public void receivedBytes(int bytes) {
node.nodeStats.probeRequestCtr.receivedBytes(bytes);
}
/**
* No payload in probes.
* @param bytes Ignored.
*/
@Override
public void sentPayload(int bytes) {}
public Probe(final Node node) {
this.node = node;
this.accepted = Collections.synchronizedMap(new HashMap<PeerNode, Counter>());
this.timer = new Timer(true);
int sortOrder = 0;
final SubConfig nodeConfig = node.config.get("node");
nodeConfig.register("probeBandwidth", true, sortOrder++, true, true, "Node.probeBandwidthShort",
"Node.probeBandwidthLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondBandwidth;
}
@Override
public void set(Boolean val) {
respondBandwidth = val;
}
});
respondBandwidth = nodeConfig.getBoolean("probeBandwidth");
nodeConfig.register("probeBuild", true, sortOrder++, true, true, "Node.probeBuildShort",
"Node.probeBuildLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondBuild;
}
@Override
public void set(Boolean val) {
respondBuild = val;
}
});
respondBuild = nodeConfig.getBoolean("probeBuild");
nodeConfig.register("probeIdentifier", true, sortOrder++, true, true,
"Node.probeRespondIdentifierShort", "Node.probeRespondIdentifierLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondIdentifier;
}
@Override
public void set(Boolean val) {
respondIdentifier = val;
}
});
respondIdentifier = nodeConfig.getBoolean("probeIdentifier");
nodeConfig.register("probeLinkLengths", true, sortOrder++, true, true, "Node.probeLinkLengthsShort",
"Node.probeLinkLengthsLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondLinkLengths;
}
@Override
public void set(Boolean val) {
respondLinkLengths = val;
}
});
respondLinkLengths = nodeConfig.getBoolean("probeLinkLengths");
nodeConfig.register("probeLocation", true, sortOrder++, true, true, "Node.probeLocationShort",
"Node.probeLocationLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondLocation;
}
@Override
public void set(Boolean val) {
respondLocation = val;
}
});
respondLocation = nodeConfig.getBoolean("probeLocation");
nodeConfig.register("probeStoreSize", true, sortOrder++, true, true, "Node.probeStoreSizeShort",
"Node.probeStoreSizeLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondStoreSize;
}
@Override
public void set(Boolean val) {
respondStoreSize = val;
}
});
respondStoreSize = nodeConfig.getBoolean("probeStoreSize");
nodeConfig.register("probeUptime", true, sortOrder++, true, true, "Node.probeUptimeShort",
"Node.probeUptimeLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondUptime;
}
@Override
public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
respondUptime = val;
}
});
respondUptime = nodeConfig.getBoolean("probeUptime");
nodeConfig.register("probeRejectStats", true, sortOrder++, true, true, "Node.probeRejectStatsShort",
"Node.probeRejectStatsLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondRejectStats;
}
@Override
public void set(Boolean val) throws InvalidConfigValueException, NodeNeedRestartException {
respondRejectStats = val;
}
});
respondRejectStats = nodeConfig.getBoolean("probeRejectStats");
nodeConfig.register("probeOverallBulkOutputCapacityUsage", true, sortOrder++, true, true, "Node.respondOverallBulkOutputCapacityUsage",
"Node.respondOverallBulkOutputCapacityUsageLong", new BooleanCallback() {
@Override
public Boolean get() {
return respondOverallBulkOutputCapacityUsage;
}
@Override
public void set(Boolean val)
throws InvalidConfigValueException,
NodeNeedRestartException {
respondOverallBulkOutputCapacityUsage = val;
}
});
respondOverallBulkOutputCapacityUsage = nodeConfig.getBoolean("probeOverallBulkOutputCapacityUsage");
nodeConfig.register("identifier", -1, sortOrder++, true, true, "Node.probeIdentifierShort",
"Node.probeIdentifierLong", new LongCallback() {
@Override
public Long get() {
return probeIdentifier;
}
@Override
public void set(Long val) {
probeIdentifier = val;
//-1 is reserved for picking a random value; don't pick it randomly.
while(probeIdentifier == -1) probeIdentifier = node.random.nextLong();
}
}, false);
probeIdentifier = nodeConfig.getLong("identifier");
/*
* set() is not used when setting up an option with its default value, so do so manually to avoid using
* an identifier of -1.
*/
try {
if(probeIdentifier == -1) {
nodeConfig.getOption("identifier").setValue("-1");
//TODO: Store config here as it has changed?
node.config.store();
}
} catch (InvalidConfigValueException e) {
Logger.error(Probe.class, "node.identifier set() unexpectedly threw.", e);
} catch (NodeNeedRestartException e) {
Logger.error(Probe.class, "node.identifier set() unexpectedly threw.", e);
}
}
/**
* Sends an outgoing probe request.
* @param htl htl for this outgoing probe: should be [1, MAX_HTL]
* @param listener will be called with results.
* @see Listener
*/
public void start(final byte htl, final long uid, final Type type, final Listener listener) {
request(DMT.createProbeRequest(htl, uid, type), null, listener);
}
/**
* Processes an incoming probe request; relays results back to source.
* If the probe has a positive HTL, routes with MH correction and probabilistically decrements HTL.
* If the probe comes to have an HTL of zero: (an incoming HTL of less than one is discarded.)
* Returns (as node settings allow) exactly one of:
* <ul>
* <li>unique identifier and integer 7-day uptime percentage</li>
* <li>uptime: 48-hour percentage or 7-day percentage</li>
* <li>output bandwidth</li>
* <li>store size</li>
* <li>link lengths</li>
* <li>location</li>
* <li>build number</li>
* </ul>
*
* @param message probe request, containing HTL
*/
public void request(Message message, PeerNode source) {
request(message, source, new ResultRelay(source, message.getLong(DMT.UID)));
}
/**
* Processes a probe request, calling the listener with any results.
* @param source node from which the probe request was received. If null, it is considered to have been sent
* by the local node.
* @param listener listener for probe response.
*/
private void request(final Message message, final PeerNode source, final Listener listener) {
final Long uid = message.getLong(DMT.UID);
final byte typeCode = message.getByte(DMT.TYPE);
final Type type;
if (Type.isValid(typeCode)) {
type = Type.valueOf(typeCode);
if (logDEBUG) Logger.debug(Probe.class, "Probe type is " + type.name() + ".");
} else {
if (logMINOR) Logger.minor(Probe.class, "Invalid probe type " + typeCode + ".");
listener.onError(Error.UNRECOGNIZED_TYPE, typeCode, true);
return;
}
byte htl = message.getByte(DMT.HTL);
if (htl < 1) {
if (logWARNING) {
Logger.warning(Probe.class, "Received out-of-bounds HTL of " + htl + " from " +
source.getIdentityString() + " (" + source.userToString() + "); discarding.");
}
return;
} else if (htl > MAX_HTL) {
if (logMINOR) {
Logger.minor(Probe.class, "Received out-of-bounds HTL of " + htl + " from " +
source.getIdentityString() + " (" + source.userToString() + "); interpreting as " +
MAX_HTL + ".");
}
htl = MAX_HTL;
}
boolean availableSlot = true;
TimerTask task = null;
//Allocate one of this peer's probe request slots for 60 seconds; send an overload if none are available.
synchronized (accepted) {
//If no counter exists for the current source, add one.
if (!accepted.containsKey(source)) {
// Null source is started locally.
accepted.put(source, new Counter(source == null ? COUNTER_MAX_LOCAL : COUNTER_MAX_PEER));
}
final Counter counter = accepted.get(source);
if (counter.value() == counter.maxAccepted) {
//Set a flag instead of sending inside the lock.
availableSlot = false;
} else {
//There's a free slot; increment the counter.
counter.increment();
task = new TimerTask() {
@Override
public void run() {
synchronized (accepted) {
counter.decrement();
/* Once the counter hits zero, there's no reason to keep it around as it
* can just be recreated when this peer sends another probe request
* without changing behavior. To do otherwise would accumulate counters
* at zero over time.
*/
if (counter.value() == 0) {
accepted.remove(source);
}
}
}
};
}
}
if (!availableSlot) {
//Send an overload error back to the source.
if (logDEBUG) Logger.debug(Probe.class, "Already accepted maximum number of probes; rejecting incoming.");
listener.onError(Error.OVERLOAD, null, true);
return;
}
//One-minute window on acceptance; free up this probe slot in 60 seconds.
timer.schedule(task, MINUTES.toMillis(1));
/*
* Route to a peer, using Metropolis-Hastings correction and ignoring backoff to get a more uniform
* endpoint distribution. HTL is decremented before routing so that it's possible to respond locally without
* attempting to route first. Send a local response if HTL is zero now or becomes zero while trying to route.
* During routing HTL decrements if a candidate is rejected by the Metropolis-Hastings correction.
*/
htl = probabilisticDecrement(htl);
if (htl == 0 || !route(type, uid, htl, listener)) {
long wait = WAIT_MAX;
while (wait >= WAIT_MAX) wait = (long)(-Math.log(node.random.nextDouble()) * WAIT_BASE / Math.E);
timer.schedule(new TimerTask() {
@Override
public void run() {
respond(type, listener);
}
}, wait);
}
}
/**
* Attempts to route the message to a peer. If the maximum number of send attempts is exceeded, fails with the
* error CANNOT_FORWARD.
* @return True if no further action needed; false if HTL decremented to zero and a local response is needed.
*/
private boolean route(final Type type, final long uid, byte htl, final Listener listener) {
//Recreate the request so that any sub-messages or unintended fields are not forwarded.
final Message message = DMT.createProbeRequest(htl, uid, type);
PeerNode[] peers;
//Degree of the local node.
int degree;
PeerNode candidate;
/*
* Attempt to forward until success or until reaching the send attempt limit.
*/
for (int sendAttempts = 0; sendAttempts < MAX_SEND_ATTEMPTS; sendAttempts++) {
peers = node.getConnectedPeers();
degree = peers.length;
//Can't handle a probe request if not connected to peers.
if (degree == 0 ) {
if (logMINOR) {
Logger.minor(Probe.class, "Aborting probe request: no connections.");
}
/*
* If this is a locally-started request, not a relayed one, give an error.
* Otherwise, in this case there's nowhere to send the error.
*/
listener.onError(Error.DISCONNECTED, null, true);
return true;
}
candidate = peers[node.random.nextInt(degree)];
if (candidate.isConnected()) {
//acceptProbability is the MH correction.
float acceptProbability;
int candidateDegree = candidate.getDegree();
/* Candidate's degree is unknown; fall back to random walk by accepting this candidate
* regardless of its degree.
*/
if (candidateDegree == 0) acceptProbability = 1.0f;
else acceptProbability = (float)degree / candidateDegree;
if (logDEBUG) Logger.debug(Probe.class, "acceptProbability is " + acceptProbability);
if (node.random.nextFloat() < acceptProbability) {
if (logDEBUG) Logger.debug(Probe.class, "Accepted candidate.");
//Filter for response to this probe with requested result type.
final MessageFilter filter = createResponseFilter(type, candidate, uid, htl);
message.set(DMT.HTL, htl);
try {
node.getUSM().addAsyncFilter(filter, new ResultListener(listener), this);
if (logDEBUG) Logger.debug(Probe.class, "Sending.");
candidate.sendAsync(message, null, this);
return true;
} catch (NotConnectedException e) {
if (logMINOR) Logger.minor(Probe.class, "Peer became disconnected between check and send attempt.", e);
// Peer no longer connected - sending was not successful. Try again.
} catch (DisconnectedException e) {
if (logMINOR) Logger.minor(Probe.class, "Peer became disconnected while attempting to add filter.", e);
// Peer no longer connected - cannot send. Try again.
}
} else {
/*
* Metropolis-Hastings correction rejected - decrement HTL so that it can run out depending on
* relative degrees.
*/
htl = probabilisticDecrement(htl);
if (htl == 0) return false;
}
} else {
if (logMINOR) Logger.minor(Probe.class, "Peer in connectedPeers was not connected.", new Exception());
}
}
// Send attempt limit reached.
if (logWARNING) {
Logger.warning(Probe.class, "Aborting probe request: send attempt limit reached.");
}
listener.onError(Error.CANNOT_FORWARD, null, true);
return true;
}
/**
* @param type probe result type requested.
* @param candidate node to filter for response from.
* @param uid probe request uid, also to be used in any result.
* @param htl current probe HTL; used to calculate timeout.
* @return filter for the requested result type, probe error, and probe refusal.
*/
private static MessageFilter createResponseFilter(final Type type, final PeerNode candidate, final long uid, final byte htl) {
final long timeout = (htl - 1) * TIMEOUT_PER_HTL + TIMEOUT_HTL1;
final MessageFilter filter = createFilter(candidate, uid, timeout);
switch (type) {
case BANDWIDTH: filter.setType(DMT.ProbeBandwidth); break;
case BUILD: filter.setType(DMT.ProbeBuild); break;
case IDENTIFIER: filter.setType(DMT.ProbeIdentifier); break;
case LINK_LENGTHS: filter.setType(DMT.ProbeLinkLengths); break;
case LOCATION: filter.setType(DMT.ProbeLocation); break;
case STORE_SIZE: filter.setType(DMT.ProbeStoreSize); break;
case UPTIME_48H:
case UPTIME_7D: filter.setType(DMT.ProbeUptime); break;
case REJECT_STATS: filter.setType(DMT.ProbeRejectStats); break;
case OVERALL_BULK_OUTPUT_CAPACITY_USAGE: filter.setType(DMT.ProbeOverallBulkOutputCapacityUsage); break;
default: throw new UnsupportedOperationException("Missing filter for " + type.name());
}
//Refusal or an error should also be listened for so it can be relayed.
filter.or(createFilter(candidate, uid, timeout).setType(DMT.ProbeRefused)
.or(createFilter(candidate, uid, timeout).setType(DMT.ProbeError)));
return filter;
}
private static MessageFilter createFilter(final PeerNode source, final long uid, final long timeout) {
return MessageFilter.create().setSource(source).setField(DMT.UID, uid).setTimeout(timeout);
}
/**
* Depending on node settings, sends a message to source containing either a refusal or the requested result.
*/
private void respond(final Type type, final Listener listener) {
if (!respondTo(type)) {
listener.onRefused();
return;
}
/*
* This adds noise to the results to make information less identifiable. The goal is making it difficult
* to determine which value a node actually has; that any given value could mean a small range of common
* values. Different result types have different sigma values such that one sigma contains multiple
* reasonable values.
*/
switch (type) {
case BANDWIDTH:
/*
* 5% noise:
* Reasonable output bandwidth limit is 20 KiB and people are likely to set limits in increments
* of 1 KiB. 1 KiB / 20 KiB = 0.05 sigma.
* 1,024 (2^10) bytes per KiB.
*/
listener.onOutputBandwidth((float)randomNoise((double)node.getOutputBandwidthLimit()/(1 << 10), 0.05));
break;
case BUILD:
listener.onBuild(node.nodeUpdater.getMainVersion());
break;
case IDENTIFIER:
/*
* 5% noise:
* Reasonable uptime percentage is at least ~40 hours a week, or ~20%. This uptime is
* quantized so only something above a full percentage point (0.01 * 168 hours = 1.68 hours) of
* change will be guaranteed (from a percentage with a decimal component close to zero) to be
* reflected. 1% / 20% = 0.05 sigma.
*
* 7-day uptime with random noise, then quantized. Quantization is to make it very, very
* difficult to get useful information out of any given result because it is included with an
* identifier,
*/
long percent = Math.round(randomNoise(100*node.uptime.getUptimeWeek(), 0.05));
//Clamp to byte.
if (percent > Byte.MAX_VALUE) percent = Byte.MAX_VALUE;
else if (percent < Byte.MIN_VALUE) percent = Byte.MIN_VALUE;
listener.onIdentifier(probeIdentifier, (byte)percent);
break;
case LINK_LENGTHS:
PeerNode[] peers = node.getConnectedPeers();
float[] linkLengths = new float[peers.length];
int i = 0;
/*
* 1% noise:
* Link lengths are in the range [0.0, 0.5], and any change is enough to make the
* match not exact between locations. Taking as an example a link length of 0.2. and with the
* assumption that a change of 0.002 is enough to make it still useful for statistics but not
* useful for identification, 0.002 change / 0.2 link length = 0.01 sigma.
*/
double myLoc = node.getLocation();
for (PeerNode peer : peers) {
double peerLoc = peer.getLocation();
if (Location.isValid(peerLoc)) {
linkLengths[i++] = (float)randomNoise(Location.distance(myLoc, peerLoc), 0.01);
}
}
linkLengths = java.util.Arrays.copyOf(linkLengths, i);
java.util.Arrays.sort(linkLengths);
listener.onLinkLengths(linkLengths);
break;
case LOCATION:
listener.onLocation((float)node.getLocation());
break;
case STORE_SIZE:
/*
* 5% noise:
* Reasonable datastore size is 20 GiB, and size is likely set in, at most, increments of 1 GiB.
* 1 GiB / 20 GiB = 0.05 sigma.
* 1,073,741,824 bytes (2^30) per GiB.
*/
listener.onStoreSize((float)randomNoise((double)node.getStoreSize()/(1 << 30), 0.05));
break;
case UPTIME_48H:
/*
* 8% noise:
* Continuing with the assumption that reasonable weekly uptime is around 40 hours, this allows
* for 6 hours per day, 12 hours per 48 hours, or 25%. A half-hour seems a sufficient amount of
* ambiguity, so 0.5 hours / 48 hours ~= 1%, and 1% / 25% = 0.04 sigma.
*/
listener.onUptime((float)randomNoise(100*node.uptime.getUptime(), 0.04));
break;
case UPTIME_7D:
/*
* 2.4% noise:
* As a 168-hour uptime covers a longer period 1 hour of ambiguity seems sufficient.
* 1 hour / 168 hours ~= 0.6%, and 0.6% / 20% = 0.03 sigma.
*/
listener.onUptime((float)randomNoise(100*node.uptime.getUptimeWeek(), 0.03));
break;
case REJECT_STATS:
byte[] stats = node.nodeStats.getNoisyRejectStats();
listener.onRejectStats(stats);
break;
case OVERALL_BULK_OUTPUT_CAPACITY_USAGE:
byte bandwidthClass =
DMT.bandwidthClassForCapacityUsage(node.getOutputBandwidthLimit());
listener.onOverallBulkOutputCapacity(bandwidthClass,
(float)randomNoise(node.nodeStats.getBandwidthLiabilityUsage(), 0.1));
break;
default:
throw new UnsupportedOperationException("Missing response for " + type.name());
}
}
private boolean respondTo(Type type) {
switch (type){
case BANDWIDTH: return respondBandwidth;
case BUILD: return respondBuild;
case IDENTIFIER: return respondIdentifier;
case LINK_LENGTHS: return respondLinkLengths;
case LOCATION: return respondLocation;
case STORE_SIZE: return respondStoreSize;
case UPTIME_48H:
case UPTIME_7D: return respondUptime;
case REJECT_STATS: return respondRejectStats;
case OVERALL_BULK_OUTPUT_CAPACITY_USAGE: return respondOverallBulkOutputCapacityUsage;
default: throw new UnsupportedOperationException("Missing permissions check for " + type.name());
}
}
/**
* Decrements 20% of the time at HTL 1; otherwise always. This is to protect the responding node, whereas the
* anonymity of the node which initiated the request is not a concern.
* @param htl current HTL
* @return new HTL
*/
private byte probabilisticDecrement(byte htl) {
assert htl > 0;
if (htl == 1) {
if (node.random.nextFloat() < DECREMENT_PROBABILITY) return 0;
return 1;
}
return (byte)(htl - 1);
}
/**
* Filter listener which determines the type of result and calls the appropriate probe listener method.
*/
private class ResultListener implements AsyncMessageFilterCallback {
private final Listener listener;
/**
* @param listener to call appropriate methods for events such as matched messages or timeout.
*/
public ResultListener(Listener listener) {
this.listener = listener;
}
@Override
public void onDisconnect(PeerContext context) {
if (logDEBUG) Logger.debug(Probe.class, "Next node in chain disconnected.");
listener.onError(Error.DISCONNECTED, null, true);
}
/**
* Parses provided message and calls appropriate Probe.Listener method for the type of result.
* @param message Probe result.
*/
@Override
public void onMatched(Message message) {
if(logDEBUG) Logger.debug(Probe.class, "Matched " + message.getSpec().getName());
if (message.getSpec().equals(DMT.ProbeBandwidth)) {
listener.onOutputBandwidth(message.getFloat(DMT.OUTPUT_BANDWIDTH_UPPER_LIMIT));
} else if (message.getSpec().equals(DMT.ProbeBuild)) {
listener.onBuild(message.getInt(DMT.BUILD));
} else if (message.getSpec().equals(DMT.ProbeIdentifier)) {
listener.onIdentifier(message.getLong(DMT.PROBE_IDENTIFIER), message.getByte(DMT.UPTIME_PERCENT));
} else if (message.getSpec().equals(DMT.ProbeLinkLengths)) {
listener.onLinkLengths(message.getFloatArray(DMT.LINK_LENGTHS));
} else if (message.getSpec().equals(DMT.ProbeLocation)) {
listener.onLocation(message.getFloat(DMT.LOCATION));
} else if (message.getSpec().equals(DMT.ProbeStoreSize)) {
listener.onStoreSize(message.getFloat(DMT.STORE_SIZE));
} else if (message.getSpec().equals(DMT.ProbeUptime)) {
listener.onUptime(message.getFloat(DMT.UPTIME_PERCENT));
} else if (message.getSpec().equals(DMT.ProbeRejectStats)) {
listener.onRejectStats(message.getShortBufferBytes(DMT.REJECT_STATS));
} else if (message.getSpec().equals(DMT.ProbeOverallBulkOutputCapacityUsage)) {
listener.onOverallBulkOutputCapacity(message.getByte(DMT.OUTPUT_BANDWIDTH_CLASS), message.getFloat(DMT.CAPACITY_USAGE));
} else if (message.getSpec().equals(DMT.ProbeError)) {
final byte rawError = message.getByte(DMT.TYPE);
if (Error.isValid(rawError)) {
listener.onError(Error.valueOf(rawError), null, false);
} else {
//Not recognized locally.
listener.onError(Error.UNKNOWN, rawError, false);
}
} else if (message.getSpec().equals(DMT.ProbeRefused)) {
listener.onRefused();
} else {
throw new UnsupportedOperationException("Missing handling for " + message.getSpec().getName());
}
}
@Override
public void onRestarted(PeerContext context) {}
@Override
public void onTimeout() {
if (logDEBUG) Logger.debug(Probe.class, "Timed out.");
listener.onError(Error.TIMEOUT, null, true);
}
@Override
public boolean shouldTimeout() {
return false;
}
}
/**
* Listener which relays responses to the node specified during construction. Used for received probe requests.
* This leads to reconstructing the messages, but removes potentially harmful sub-messages and also removes the
* need for duplicate message sending code elsewhere, If the result includes a trace this would be the place
* to add local results to it.
*/
private class ResultRelay implements Listener {
private final PeerNode source;
private final Long uid;
/**
* @param source peer from which the request was received and to which send the response.
* @throws IllegalArgumentException if source is null.
*/
public ResultRelay(PeerNode source, Long uid) {
this.source = source;
this.uid = uid;
}
private void send(Message message) {
if (!source.isConnected()) {
if (logDEBUG) Logger.debug(Probe.class, SOURCE_DISCONNECT);
return;
}
if (logDEBUG) Logger.debug(Probe.class, "Relaying " + message.getSpec().getName() + " back" +
" to " + source.userToString());
try {
source.sendAsync(message, null, Probe.this);
} catch (NotConnectedException e) {
if (logDEBUG) Logger.debug(Probe.class, SOURCE_DISCONNECT, e);
}
}
@Override
public void onError(Error error, Byte code, boolean local) {
send(DMT.createProbeError(uid, error));
}
@Override
public void onRefused() {
send(DMT.createProbeRefused(uid));
}
@Override
public void onOutputBandwidth(float outputBandwidth) {
send(DMT.createProbeBandwidth(uid, outputBandwidth));
}
@Override
public void onBuild(int build) {
send(DMT.createProbeBuild(uid, build));
}
@Override
public void onIdentifier(long identifier, byte uptimePercentage) {
send(DMT.createProbeIdentifier(uid, identifier, uptimePercentage));
}
@Override
public void onLinkLengths(float[] linkLengths) {
send(DMT.createProbeLinkLengths(uid, linkLengths));
}
@Override
public void onLocation(float location) {
send(DMT.createProbeLocation(uid, location));
}
@Override
public void onStoreSize(float storeSize) {
send(DMT.createProbeStoreSize(uid, storeSize));
}
@Override
public void onUptime(float uptimePercentage) {
send(DMT.createProbeUptime(uid, uptimePercentage));
}
@Override
public void onRejectStats(byte[] stats) {
if(stats.length < 4) {
Logger.warning(this, "Unknown length for stats: "+stats.length);
onError(Error.UNKNOWN, Error.UNKNOWN.code, true);
} else {
if(stats.length > 4)
stats = Arrays.copyOf(stats, 4);
send(DMT.createProbeRejectStats(uid, stats));
}
}
@Override
public void onOverallBulkOutputCapacity(
byte bandwidthClassForCapacityUsage, float capacityUsage) {
send(DMT.createProbeOverallBulkOutputCapacityUsage(uid, bandwidthClassForCapacityUsage, capacityUsage));
// TODO Auto-generated method stub
}
}
}