package freenet.node;
import static java.util.concurrent.TimeUnit.HOURS;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.HashSet;
import freenet.keys.Key;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
/** Tracks recent requests for a specific key. If we have recently routed to a specific
* node, and failed, we should not route to it again, unless it is at a higher HTL.
* Different failures cause different timeouts. Similarly we track the nodes that have
* requested the key, because for both sets of nodes, when we find the data we offer them
* it; this greatly improves latency and efficiency for polling-based tools. For nodes
* we have routed to, we keep up to HTL separate entries; for nodes we have received
* requests from, we keep only one entry.
*
* SECURITY: All this could be a security risk if not regularly cleared - which it is,
* of course: We forget about either kind of node after a fixed period, in
* cleanupRequested(), which the FailureTable calls regularly. Against a near-omnipotent
* attacker able to compromise nodes at will of course it is still a security risk to
* track anything but we have bigger problems at that level.
* @author toad
*/
class FailureTableEntry implements TimedOutNodesList {
/** The key */
final Key key; // FIXME should this be stored compressed somehow e.g. just the routing key?
/** Time of creation of this entry */
long creationTime;
/** Time we last received a request for the key */
long receivedTime;
/** Time we last received a DNF after sending a request for a key */
long sentTime;
/** WeakReference's to PeerNodeUnlocked's who have requested the key */
WeakReference<? extends PeerNodeUnlocked>[] requestorNodes;
/** Times at which they requested it */
long[] requestorTimes;
/** Boot ID when they requested it. We don't send it to restarted nodes, as a
* (weak, but useful if combined with other measures) protection against seizure. */
long[] requestorBootIDs;
short[] requestorHTLs;
// FIXME Note that just because a node is in this list doesn't mean it DNFed or RFed.
// We include *ALL* nodes we routed to here!
/** WeakReference's to PeerNodeUnlocked's we have requested it from */
WeakReference<? extends PeerNodeUnlocked>[] requestedNodes;
/** Their locations when we requested it. This may be needed in the future to
* determine whether to let a request through that we would otherwise have
* failed with RecentlyFailed, because the node we would route it to is closer
* to the target than any we've routed to in the past. */
double[] requestedLocs;
long[] requestedBootIDs;
long[] requestedTimes;
/** Timeouts for each node for purposes of RecentlyFailed. We accept what
* they say, subject to an upper limit, because we MUST NOT suppress too
* many requests, as that could lead to a self-sustaining key blocking. */
long[] requestedTimeoutsRF;
/** Timeouts for each node for purposes of per-node failure tables. We use
* our own estimates, based on time elapsed, for most failure modes; a fixed
* period for DNF and RecentlyFailed. */
long[] requestedTimeoutsFT;
short[] requestedTimeoutHTLs;
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback() {
@Override
public void shouldUpdate() {
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
/** We remember that a node has asked us for a key for up to an hour; after that, we won't offer the key, and
* if we receive an offer from that node, we will reject it */
static final long MAX_TIME_BETWEEN_REQUEST_AND_OFFER = HOURS.toMillis(1);
public static final long[] EMPTY_LONG_ARRAY = new long[0];
public static final short[] EMPTY_SHORT_ARRAY = new short[0];
public static final double[] EMPTY_DOUBLE_ARRAY = new double[0];
@SuppressWarnings("unchecked")
public static final WeakReference<? extends PeerNodeUnlocked>[] EMPTY_WEAK_REFERENCE = new WeakReference[0];
FailureTableEntry(Key key) {
this.key = key.archivalCopy();
long now = System.currentTimeMillis();
creationTime = now;
receivedTime = -1;
sentTime = -1;
requestorNodes = EMPTY_WEAK_REFERENCE;
requestorTimes = EMPTY_LONG_ARRAY;
requestorBootIDs = EMPTY_LONG_ARRAY;
requestorHTLs = EMPTY_SHORT_ARRAY;
requestedNodes = EMPTY_WEAK_REFERENCE;
requestedLocs = EMPTY_DOUBLE_ARRAY;
requestedBootIDs = EMPTY_LONG_ARRAY;
requestedTimes = EMPTY_LONG_ARRAY;
requestedTimeoutsRF = EMPTY_LONG_ARRAY;
requestedTimeoutsFT = EMPTY_LONG_ARRAY;
requestedTimeoutHTLs = EMPTY_SHORT_ARRAY;
}
/** A request failed to a specific peer.
* @param routedTo The peer we routed to.
* @param rfTimeout The time until we can route to the node again, for purposes of RecentlyFailed.
* @param ftTimeout The time until we can route to the node again, for purposes of per-node failure tables.
* @param now The current time.
* @param htl The HTL of the request. Note that timeouts only apply to the same HTL.
*/
public synchronized void failedTo(PeerNodeUnlocked routedTo, long rfTimeout, long ftTimeout, long now, short htl) {
if(logMINOR) {
Logger.minor(this, "Failed sending request to "+routedTo.shortToString()+" : timeout "+rfTimeout+" / "+ftTimeout);
}
int idx = addRequestedFrom(routedTo, htl, now);
if(rfTimeout > 0) {
long curTimeoutTime = requestedTimeoutsRF[idx];
long newTimeoutTime = now + rfTimeout;
if(newTimeoutTime > curTimeoutTime) {
requestedTimeoutsRF[idx] = newTimeoutTime;
requestedTimeoutHTLs[idx] = htl;
}
}
if(ftTimeout > 0) {
long curTimeoutTime = requestedTimeoutsFT[idx];
long newTimeoutTime = now + ftTimeout;
if(newTimeoutTime > curTimeoutTime) {
requestedTimeoutsFT[idx] = newTimeoutTime;
requestedTimeoutHTLs[idx] = htl;
}
}
}
// These are rather low level, in an attempt to absolutely minimize memory usage...
// The two methods have almost identical code/logic.
// Dunno if there's a more elegant way of dealing with this which doesn't significantly increase
// per entry byte cost.
// Note also this will generate some churn...
synchronized int addRequestor(PeerNodeUnlocked requestor, long now, short origHTL) {
if(logMINOR) Logger.minor(this, "Adding requestors: "+requestor+" at "+now);
receivedTime = now;
boolean includedAlready = false;
int nulls = 0;
int ret = -1;
for(int i=0;i<requestorNodes.length;i++) {
PeerNodeUnlocked got = requestorNodes[i] == null ? null : requestorNodes[i].get();
// No longer subscribed if they have rebooted, or expired
if(got == requestor) {
// Update existing entry
includedAlready = true;
requestorTimes[i] = now;
requestorBootIDs[i] = requestor.getBootID();
requestorHTLs[i] = origHTL;
ret = i;
break;
} else if(got != null &&
(got.getBootID() != requestorBootIDs[i] || now - requestorTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER)) {
requestorNodes[i] = null;
got = null;
}
if(got == null)
nulls++;
}
if(nulls == 0 && includedAlready) return ret;
int notIncluded = includedAlready ? 0 : 1;
// Because weak, these can become null; doesn't matter, but we want to minimise memory usage
if(nulls == 1 && !includedAlready) {
// Nice special case
for(int i=0;i<requestorNodes.length;i++) {
if(requestorNodes[i] == null || requestorNodes[i].get() == null) {
requestorNodes[i] = requestor.getWeakRef();
requestorTimes[i] = now;
requestorBootIDs[i] = requestor.getBootID();
requestorHTLs[i] = origHTL;
return i;
}
}
}
@SuppressWarnings("unchecked")
WeakReference<? extends PeerNodeUnlocked>[] newRequestorNodes = new WeakReference[requestorNodes.length+notIncluded-nulls];
long[] newRequestorTimes = new long[requestorNodes.length+notIncluded-nulls];
long[] newRequestorBootIDs = new long[requestorNodes.length+notIncluded-nulls];
short[] newRequestorHTLs = new short[requestorNodes.length+notIncluded-nulls];
int toIndex = 0;
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
PeerNodeUnlocked pn = ref == null ? null : ref.get();
if(pn == null) continue;
if(pn == requestor) ret = toIndex;
newRequestorNodes[toIndex] = requestorNodes[i];
newRequestorTimes[toIndex] = requestorTimes[i];
newRequestorBootIDs[toIndex] = requestorBootIDs[i];
newRequestorHTLs[toIndex] = requestorHTLs[i];
toIndex++;
}
if(!includedAlready) {
newRequestorNodes[toIndex] = requestor.getWeakRef();
newRequestorTimes[toIndex] = now;
newRequestorBootIDs[toIndex] = requestor.getBootID();
newRequestorHTLs[toIndex] = origHTL;
ret = toIndex;
toIndex++;
}
for(int i=toIndex;i<newRequestorNodes.length;i++) newRequestorNodes[i] = null;
if(toIndex > newRequestorNodes.length + 2) {
newRequestorNodes = Arrays.copyOf(newRequestorNodes, toIndex);
newRequestorTimes = Arrays.copyOf(newRequestorTimes, toIndex);
newRequestorBootIDs = Arrays.copyOf(newRequestorBootIDs, toIndex);
newRequestorHTLs = Arrays.copyOf(newRequestorHTLs, toIndex);
}
requestorNodes = newRequestorNodes;
requestorTimes = newRequestorTimes;
requestorBootIDs = newRequestorBootIDs;
requestorHTLs = newRequestorHTLs;
return ret;
}
/** Add a requested from entry to the node. If there already is one reuse it but only
* if the HTL matches. Return the index so we can update timeouts etc.
* @param requestedFrom The node we have routed the request to.
* @param htl The HTL at which the request was sent.
* @param now The current time.
* @return The index of the new or old entry.
*/
private synchronized int addRequestedFrom(PeerNodeUnlocked requestedFrom, short htl, long now) {
if(logMINOR) Logger.minor(this, "Adding requested from: "+requestedFrom+" at "+now);
sentTime = now;
boolean includedAlready = false;
int nulls = 0;
int ret = -1;
for(int i=0;i<requestedNodes.length;i++) {
PeerNodeUnlocked got = requestedNodes[i] == null ? null : requestedNodes[i].get();
if(got == requestedFrom && (requestedTimeoutsRF[i] == -1 || requestedTimeoutsFT[i] == -1 || requestedTimeoutHTLs[i] == htl)) {
includedAlready = true;
requestedLocs[i] = requestedFrom.getLocation();
requestedBootIDs[i] = requestedFrom.getBootID();
requestedTimes[i] = now;
ret = i;
} else if(got != null &&
(got.getBootID() != requestedBootIDs[i] || now - requestedTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER)) {
requestedNodes[i] = null;
got = null;
}
if(got == null)
nulls++;
}
if(includedAlready && nulls == 0) return ret;
int notIncluded = includedAlready ? 0 : 1;
// Because weak, these can become null; doesn't matter, but we want to minimise memory usage
if(nulls == 1 && !includedAlready) {
// Nice special case
for(int i=0;i<requestedNodes.length;i++) {
if(requestedNodes[i] == null || requestedNodes[i].get() == null) {
requestedNodes[i] = requestedFrom.getWeakRef();
requestedLocs[i] = requestedFrom.getLocation();
requestedBootIDs[i] = requestedFrom.getBootID();
requestedTimes[i] = now;
requestedTimeoutsRF[i] = -1;
requestedTimeoutsFT[i] = -1;
requestedTimeoutHTLs[i] = (short) -1;
return i;
}
}
}
@SuppressWarnings("unchecked")
WeakReference<? extends PeerNodeUnlocked>[] newRequestedNodes = new WeakReference[requestedNodes.length+notIncluded-nulls];
double[] newRequestedLocs = new double[requestedNodes.length+notIncluded-nulls];
long[] newRequestedBootIDs = new long[requestedNodes.length+notIncluded-nulls];
long[] newRequestedTimes = new long[requestedNodes.length+notIncluded-nulls];
long[] newRequestedTimeoutsFT = new long[requestedNodes.length+notIncluded-nulls];
long[] newRequestedTimeoutsRF = new long[requestedNodes.length+notIncluded-nulls];
short[] newRequestedTimeoutHTLs = new short[requestedNodes.length+notIncluded-nulls];
int toIndex = 0;
for(int i=0;i<requestedNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestedNodes[i];
PeerNodeUnlocked pn = ref == null ? null : ref.get();
if(pn == null) continue;
if(pn == requestedFrom) ret = toIndex;
newRequestedNodes[toIndex] = requestedNodes[i];
newRequestedTimes[toIndex] = requestedTimes[i];
newRequestedBootIDs[toIndex] = requestedBootIDs[i];
newRequestedLocs[toIndex] = requestedLocs[i];
newRequestedTimeoutsFT[toIndex] = requestedTimeoutsFT[i];
newRequestedTimeoutsRF[toIndex] = requestedTimeoutsRF[i];
newRequestedTimeoutHTLs[toIndex] = requestedTimeoutHTLs[i];
toIndex++;
}
if(!includedAlready) {
ret = toIndex;
newRequestedNodes[toIndex] = requestedFrom.getWeakRef();
newRequestedTimes[toIndex] = now;
newRequestedBootIDs[toIndex] = requestedFrom.getBootID();
newRequestedLocs[toIndex] = requestedFrom.getLocation();
newRequestedTimeoutsFT[toIndex] = -1;
newRequestedTimeoutsRF[toIndex] = -1;
newRequestedTimeoutHTLs[toIndex] = (short) -1;
ret = toIndex;
toIndex++;
}
for(int i=toIndex;i<newRequestedNodes.length;i++) newRequestedNodes[i] = null;
if(toIndex > newRequestedNodes.length + 2) {
newRequestedNodes = Arrays.copyOf(newRequestedNodes, toIndex);
newRequestedLocs = Arrays.copyOf(newRequestedLocs, toIndex);
newRequestedBootIDs = Arrays.copyOf(newRequestedBootIDs, toIndex);
newRequestedTimes = Arrays.copyOf(newRequestedTimes, toIndex);
newRequestedTimeoutsRF = Arrays.copyOf(newRequestedTimeoutsRF, toIndex);
newRequestedTimeoutsFT = Arrays.copyOf(newRequestedTimeoutsFT, toIndex);
newRequestedTimeoutHTLs = Arrays.copyOf(newRequestedTimeoutHTLs, toIndex);
}
requestedNodes = newRequestedNodes;
requestedLocs = newRequestedLocs;
requestedBootIDs = newRequestedBootIDs;
requestedTimes = newRequestedTimes;
requestedTimeoutsRF = newRequestedTimeoutsRF;
requestedTimeoutsFT = newRequestedTimeoutsFT;
requestedTimeoutHTLs = newRequestedTimeoutHTLs;
return ret;
}
/** Offer this key to all the nodes that have requested it, and all the nodes it has been requested from.
* Called after a) the data has been stored, and b) this entry has been removed from the FT */
public void offer() {
HashSet<PeerNodeUnlocked> set = new HashSet<PeerNodeUnlocked>();
final boolean logMINOR = FailureTableEntry.logMINOR;
if(logMINOR) Logger.minor(this, "Sending offers to nodes which requested the key from us: ("+requestorNodes.length+") for "+key);
synchronized(this) {
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) continue;
if(pn.getBootID() != requestorBootIDs[i]) continue;
if(!set.add(pn)) {
Logger.error(this, "Node is in requestorNodes twice: "+pn);
}
}
if(logMINOR) Logger.minor(this, "Sending offers to nodes which we sent the key to: ("+requestedNodes.length+") for "+key);
for(int i=0;i<requestedNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestedNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) continue;
if(pn.getBootID() != requestedBootIDs[i]) continue;
if(!set.add(pn)) continue;
}
}
// Do the offers outside the lock.
// We do not need to hold it, offer() doesn't do anything that affects us.
for(PeerNodeUnlocked pn : set) {
if(logMINOR) Logger.minor(this, "Offering to "+pn);
pn.offer(key);
}
}
/**
* Has any node asked for this key?
*/
public synchronized boolean othersWant(PeerNodeUnlocked peer) {
boolean anyValid = false;
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) {
requestorNodes[i] = null;
continue;
}
long bootID = pn.getBootID();
if(bootID != requestorBootIDs[i]) {
requestorNodes[i] = null;
continue;
}
anyValid = true;
}
if(!anyValid) {
requestorNodes = EMPTY_WEAK_REFERENCE;
requestorTimes = requestorBootIDs = EMPTY_LONG_ARRAY;
requestorHTLs = EMPTY_SHORT_ARRAY;
}
return anyValid;
}
/**
* Has this peer asked us for the key?
*/
public synchronized boolean askedByPeer(PeerNodeUnlocked peer, long now) {
boolean anyValid = false;
boolean ret = false;
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) {
requestorNodes[i] = null;
continue;
}
long bootID = pn.getBootID();
if(bootID != requestorBootIDs[i]) {
requestorNodes[i] = null;
continue;
}
if(now - requestorTimes[i] < MAX_TIME_BETWEEN_REQUEST_AND_OFFER) {
if(pn == peer) ret = true;
anyValid = true;
}
}
if(!anyValid) {
requestorNodes = EMPTY_WEAK_REFERENCE;
requestorTimes = requestorBootIDs = EMPTY_LONG_ARRAY;
requestorHTLs = EMPTY_SHORT_ARRAY;
}
return ret;
}
/**
* Have we asked this peer for the key?
*/
public synchronized boolean askedFromPeer(PeerNodeUnlocked peer, long now) {
boolean anyValid = false;
boolean ret = false;
for(int i=0;i<requestedNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestedNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) {
requestedNodes[i] = null;
continue;
}
long bootID = pn.getBootID();
if(bootID != requestedBootIDs[i]) {
requestedNodes[i] = null;
continue;
}
anyValid = true;
if(now - requestedTimes[i] < MAX_TIME_BETWEEN_REQUEST_AND_OFFER) {
if(pn == peer) ret = true;
anyValid = true;
}
}
if(!anyValid) {
requestedNodes = EMPTY_WEAK_REFERENCE;
requestedTimes = requestedBootIDs = requestedTimeoutsRF = requestedTimeoutsFT = EMPTY_LONG_ARRAY;
requestedTimeoutHTLs =EMPTY_SHORT_ARRAY;
}
return ret;
}
public synchronized boolean isEmpty(long now) {
if(requestedNodes.length > 0) return false;
if(requestorNodes.length > 0) return false;
return true;
}
/** Get the timeout time for the given peer, taking HTL into account.
* If there was a timeout at HTL 1, and we are now sending a request at
* HTL 2, we ignore the timeout. */
@Override
public synchronized long getTimeoutTime(PeerNode peer, short htl, long now, boolean forPerNodeFailureTables) {
long timeout = -1;
for(int i=0;i<requestedNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestedNodes[i];
if(ref != null && ref.get() == peer) {
if(requestedTimeoutHTLs[i] >= htl) {
long thisTimeout = forPerNodeFailureTables ? requestedTimeoutsFT[i] : requestedTimeoutsRF[i];
if(thisTimeout > timeout && thisTimeout > now)
timeout = thisTimeout;
}
}
}
return timeout;
}
public synchronized boolean cleanup() {
long now = System.currentTimeMillis(); // don't pass in as a pass over the whole FT may take a while. get it in the method.
boolean empty = cleanupRequestor(now);
empty &= cleanupRequested(now);
return empty;
}
private boolean cleanupRequestor(long now) {
boolean empty = true;
int x = 0;
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) continue;
long bootID = pn.getBootID();
if(bootID != requestorBootIDs[i]) continue;
if(!pn.isConnected()) continue;
if(now - requestorTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER) continue;
empty = false;
requestorNodes[x] = requestorNodes[i];
requestorTimes[x] = requestorTimes[i];
requestorBootIDs[x] = requestorBootIDs[i];
requestorHTLs[x] = requestorHTLs[i];
x++;
}
if(x < requestorNodes.length) {
requestorNodes = Arrays.copyOf(requestorNodes, x);
requestorTimes = Arrays.copyOf(requestorTimes, x);
requestorBootIDs = Arrays.copyOf(requestorBootIDs, x);
requestorHTLs = Arrays.copyOf(requestorHTLs, x);
}
return empty;
}
private boolean cleanupRequested(long now) {
boolean empty = true;
int x = 0;
for(int i=0;i<requestedNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestedNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) continue;
long bootID = pn.getBootID();
if(bootID != requestedBootIDs[i]) continue;
if(!pn.isConnected()) continue;
if(now - requestedTimes[i] > MAX_TIME_BETWEEN_REQUEST_AND_OFFER) continue;
empty = false;
requestedNodes[x] = requestedNodes[i];
requestedTimes[x] = requestedTimes[i];
requestedBootIDs[x] = requestedBootIDs[i];
requestedLocs[x] = requestedLocs[i];
if(now < requestedTimeoutsRF[x] || now < requestedTimeoutsFT[x]) {
requestedTimeoutsRF[x] = requestedTimeoutsRF[i];
requestedTimeoutsFT[x] = requestedTimeoutsFT[i];
requestedTimeoutHTLs[x] = requestedTimeoutHTLs[i];
} else {
requestedTimeoutsRF[x] = -1;
requestedTimeoutsFT[x] = -1;
requestedTimeoutHTLs[x] = (short)-1;
}
x++;
}
if(x < requestedNodes.length) {
requestedNodes = Arrays.copyOf(requestedNodes, x);
requestedTimes = Arrays.copyOf(requestedTimes, x);
requestedBootIDs = Arrays.copyOf(requestedBootIDs, x);
requestedLocs = Arrays.copyOf(requestedLocs, x);
requestedTimeoutsRF = Arrays.copyOf(requestedTimeoutsRF, x);
requestedTimeoutsFT = Arrays.copyOf(requestedTimeoutsFT, x);
requestedTimeoutHTLs = Arrays.copyOf(requestedTimeoutHTLs, x);
}
return empty;
}
public boolean isEmpty() {
return isEmpty(System.currentTimeMillis());
}
public synchronized short minRequestorHTL(short htl) {
long now = System.currentTimeMillis();
boolean anyValid = false;
for(int i=0;i<requestorNodes.length;i++) {
WeakReference<? extends PeerNodeUnlocked> ref = requestorNodes[i];
if(ref == null) continue;
PeerNodeUnlocked pn = ref.get();
if(pn == null) {
requestorNodes[i] = null;
continue;
}
long bootID = pn.getBootID();
if(bootID != requestorBootIDs[i]) {
requestorNodes[i] = null;
continue;
}
if(now - requestorTimes[i] < MAX_TIME_BETWEEN_REQUEST_AND_OFFER) {
if(requestorHTLs[i] < htl) htl = requestorHTLs[i];
}
anyValid = true;
}
if(!anyValid) {
requestorNodes = EMPTY_WEAK_REFERENCE;
requestorTimes = requestorBootIDs = EMPTY_LONG_ARRAY;
requestorHTLs = EMPTY_SHORT_ARRAY;
}
return htl;
}
}