Adjust query frequency based on remaining TTL

After numOfQueriesBeforeBackoff query, the mDNS discovery logic will
enter backoff mode. In backoff mode,  the query frequency will be
updated to max(20, 0.8 * shortest remaining TTL) seconds. It will help
to reduce mDNS query frequency in certain use cases.

Bug: 284480315
Test: atest CtsNetTest FrameworksNetTests
Change-Id: Iac8baaaf58cf9b3b8e67e1cd80402fdecde1d3d4
This commit is contained in:
Yuyang Huang
2023-06-14 14:27:58 +09:00
parent 2388643f92
commit f56c92f3ff
3 changed files with 356 additions and 57 deletions

View File

@@ -50,7 +50,8 @@ public class MdnsSearchOptions implements Parcelable {
source.readBoolean(), source.readBoolean(),
source.readParcelable(null), source.readParcelable(null),
source.readString(), source.readString(),
(source.dataAvail() > 0) ? source.readBoolean() : false); source.readBoolean(),
source.readInt());
} }
@Override @Override
@@ -62,9 +63,9 @@ public class MdnsSearchOptions implements Parcelable {
private final List<String> subtypes; private final List<String> subtypes;
@Nullable @Nullable
private final String resolveInstanceName; private final String resolveInstanceName;
private final boolean isPassiveMode; private final boolean isPassiveMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks; private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final int numOfQueriesBeforeBackoff;
private final boolean removeExpiredService; private final boolean removeExpiredService;
// The target network for searching. Null network means search on all possible interfaces. // The target network for searching. Null network means search on all possible interfaces.
@Nullable private final Network mNetwork; @Nullable private final Network mNetwork;
@@ -76,13 +77,15 @@ public class MdnsSearchOptions implements Parcelable {
boolean removeExpiredService, boolean removeExpiredService,
@Nullable Network network, @Nullable Network network,
@Nullable String resolveInstanceName, @Nullable String resolveInstanceName,
boolean onlyUseIpv6OnIpv6OnlyNetworks) { boolean onlyUseIpv6OnIpv6OnlyNetworks,
int numOfQueriesBeforeBackoff) {
this.subtypes = new ArrayList<>(); this.subtypes = new ArrayList<>();
if (subtypes != null) { if (subtypes != null) {
this.subtypes.addAll(subtypes); this.subtypes.addAll(subtypes);
} }
this.isPassiveMode = isPassiveMode; this.isPassiveMode = isPassiveMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks; this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.removeExpiredService = removeExpiredService; this.removeExpiredService = removeExpiredService;
mNetwork = network; mNetwork = network;
this.resolveInstanceName = resolveInstanceName; this.resolveInstanceName = resolveInstanceName;
@@ -122,6 +125,14 @@ public class MdnsSearchOptions implements Parcelable {
return onlyUseIpv6OnIpv6OnlyNetworks; return onlyUseIpv6OnIpv6OnlyNetworks;
} }
/**
* Returns number of queries should be executed before backoff mode is enabled.
* The default number is 3 if it is not set.
*/
public int numOfQueriesBeforeBackoff() {
return numOfQueriesBeforeBackoff;
}
/** Returns {@code true} if service will be removed after its TTL expires. */ /** Returns {@code true} if service will be removed after its TTL expires. */
public boolean removeExpiredService() { public boolean removeExpiredService() {
return removeExpiredService; return removeExpiredService;
@@ -159,6 +170,7 @@ public class MdnsSearchOptions implements Parcelable {
out.writeParcelable(mNetwork, 0); out.writeParcelable(mNetwork, 0);
out.writeString(resolveInstanceName); out.writeString(resolveInstanceName);
out.writeBoolean(onlyUseIpv6OnIpv6OnlyNetworks); out.writeBoolean(onlyUseIpv6OnIpv6OnlyNetworks);
out.writeInt(numOfQueriesBeforeBackoff);
} }
/** A builder to create {@link MdnsSearchOptions}. */ /** A builder to create {@link MdnsSearchOptions}. */
@@ -166,6 +178,7 @@ public class MdnsSearchOptions implements Parcelable {
private final Set<String> subtypes; private final Set<String> subtypes;
private boolean isPassiveMode = true; private boolean isPassiveMode = true;
private boolean onlyUseIpv6OnIpv6OnlyNetworks = false; private boolean onlyUseIpv6OnIpv6OnlyNetworks = false;
private int numOfQueriesBeforeBackoff = 3;
private boolean removeExpiredService; private boolean removeExpiredService;
private Network mNetwork; private Network mNetwork;
private String resolveInstanceName; private String resolveInstanceName;
@@ -218,6 +231,14 @@ public class MdnsSearchOptions implements Parcelable {
return this; return this;
} }
/**
* Sets if the query backoff mode should be turned on.
*/
public Builder setNumOfQueriesBeforeBackoff(int numOfQueriesBeforeBackoff) {
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
return this;
}
/** /**
* Sets if the service should be removed after TTL. * Sets if the service should be removed after TTL.
* *
@@ -258,7 +279,8 @@ public class MdnsSearchOptions implements Parcelable {
removeExpiredService, removeExpiredService,
mNetwork, mNetwork,
resolveInstanceName, resolveInstanceName,
onlyUseIpv6OnIpv6OnlyNetworks); onlyUseIpv6OnIpv6OnlyNetworks,
numOfQueriesBeforeBackoff);
} }
} }
} }

View File

@@ -50,6 +50,7 @@ import java.util.concurrent.ScheduledExecutorService;
*/ */
public class MdnsServiceTypeClient { public class MdnsServiceTypeClient {
private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
private static final int DEFAULT_MTU = 1500; private static final int DEFAULT_MTU = 1500;
private final String serviceType; private final String serviceType;
@@ -63,6 +64,7 @@ public class MdnsServiceTypeClient {
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners = private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>(); new ArrayMap<>();
// TODO: change instanceNameToResponse to TreeMap with case insensitive comparator. // TODO: change instanceNameToResponse to TreeMap with case insensitive comparator.
@GuardedBy("lock")
private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>(); private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
private final boolean removeServiceAfterTtlExpires = private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires(); MdnsConfigs.removeServiceAfterTtlExpires();
@@ -77,7 +79,14 @@ public class MdnsServiceTypeClient {
@GuardedBy("lock") @GuardedBy("lock")
@Nullable @Nullable
private Future<?> requestTaskFuture; private Future<?> nextQueryTaskFuture;
@GuardedBy("lock")
@Nullable
private QueryTask lastScheduledTask;
@GuardedBy("lock")
private long lastSentTime;
/** /**
* Constructor of {@link MdnsServiceTypeClient}. * Constructor of {@link MdnsServiceTypeClient}.
@@ -189,7 +198,7 @@ public class MdnsServiceTypeClient {
} }
} }
// Cancel the next scheduled periodical task. // Cancel the next scheduled periodical task.
if (requestTaskFuture != null) { if (nextQueryTaskFuture != null) {
cancelRequestTaskLocked(); cancelRequestTaskLocked();
} }
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
@@ -198,21 +207,40 @@ public class MdnsServiceTypeClient {
searchOptions.getSubtypes(), searchOptions.getSubtypes(),
searchOptions.isPassiveMode(), searchOptions.isPassiveMode(),
searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
currentSessionId, searchOptions.numOfQueriesBeforeBackoff(),
socketKey); socketKey);
final long now = clock.elapsedRealtime();
if (lastSentTime == 0) {
lastSentTime = now;
}
if (hadReply) { if (hadReply) {
requestTaskFuture = scheduleNextRunLocked(taskConfig); final QueryTaskConfig queryTaskConfig = taskConfig.getConfigForNextRun();
final long minRemainingTtl = getMinRemainingTtlLocked(now);
final long timeToRun = now + queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
nextQueryTaskFuture = scheduleNextRunLocked(queryTaskConfig,
minRemainingTtl, now, timeToRun, currentSessionId);
} else { } else {
requestTaskFuture = executor.submit(new QueryTask(taskConfig)); lastScheduledTask = new QueryTask(taskConfig,
now /* timeToRun */,
now + getMinRemainingTtlLocked(now)/* minTtlExpirationTimeWhenScheduled */,
currentSessionId);
nextQueryTaskFuture = executor.submit(lastScheduledTask);
} }
} }
} }
@GuardedBy("lock") @GuardedBy("lock")
private void cancelRequestTaskLocked() { private void cancelRequestTaskLocked() {
requestTaskFuture.cancel(true); final boolean canceled = nextQueryTaskFuture.cancel(true);
sharedLog.log("task canceled:" + canceled + ", current session: " + currentSessionId
+ " task hashcode: " + getHexString(nextQueryTaskFuture));
++currentSessionId; ++currentSessionId;
requestTaskFuture = null; nextQueryTaskFuture = null;
lastScheduledTask = null;
}
private static String getHexString(Object o) {
return Integer.toHexString(System.identityHashCode(o));
} }
private boolean responseMatchesOptions(@NonNull MdnsResponse response, private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@@ -247,7 +275,7 @@ public class MdnsServiceTypeClient {
if (listeners.remove(listener) == null) { if (listeners.remove(listener) == null) {
return listeners.isEmpty(); return listeners.isEmpty();
} }
if (listeners.isEmpty() && requestTaskFuture != null) { if (listeners.isEmpty() && nextQueryTaskFuture != null) {
cancelRequestTaskLocked(); cancelRequestTaskLocked();
} }
return listeners.isEmpty(); return listeners.isEmpty();
@@ -284,9 +312,9 @@ public class MdnsServiceTypeClient {
for (MdnsResponse response : allResponses) { for (MdnsResponse response : allResponses) {
if (modifiedResponse.contains(response)) { if (modifiedResponse.contains(response)) {
if (response.isGoodbye()) { if (response.isGoodbye()) {
onGoodbyeReceived(response.getServiceInstanceName()); onGoodbyeReceivedLocked(response.getServiceInstanceName());
} else { } else {
onResponseModified(response); onResponseModifiedLocked(response);
} }
} else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) { } else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
// If the response is not modified and already in the cache. The cache will // If the response is not modified and already in the cache. The cache will
@@ -294,6 +322,20 @@ public class MdnsServiceTypeClient {
instanceNameToResponse.put(response.getServiceInstanceName(), response); instanceNameToResponse.put(response.getServiceInstanceName(), response);
} }
} }
if (nextQueryTaskFuture != null && lastScheduledTask != null
&& lastScheduledTask.config.shouldUseQueryBackoff()) {
final long now = clock.elapsedRealtime();
final long minRemainingTtl = getMinRemainingTtlLocked(now);
final long timeToRun = calculateTimeToRun(lastScheduledTask,
lastScheduledTask.config, now,
minRemainingTtl, lastSentTime);
if (timeToRun > lastScheduledTask.timeToRun) {
QueryTaskConfig lastTaskConfig = lastScheduledTask.config;
cancelRequestTaskLocked();
nextQueryTaskFuture = scheduleNextRunLocked(lastTaskConfig, minRemainingTtl,
now, timeToRun, currentSessionId);
}
}
} }
} }
@@ -323,13 +365,14 @@ public class MdnsServiceTypeClient {
} }
} }
if (requestTaskFuture != null) { if (nextQueryTaskFuture != null) {
cancelRequestTaskLocked(); cancelRequestTaskLocked();
} }
} }
} }
private void onResponseModified(@NonNull MdnsResponse response) { @GuardedBy("lock")
private void onResponseModifiedLocked(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName(); final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse = final MdnsResponse currentResponse =
instanceNameToResponse.get(serviceInstanceName); instanceNameToResponse.get(serviceInstanceName);
@@ -375,7 +418,8 @@ public class MdnsServiceTypeClient {
} }
} }
private void onGoodbyeReceived(@Nullable String serviceInstanceName) { @GuardedBy("lock")
private void onGoodbyeReceivedLocked(@Nullable String serviceInstanceName) {
final MdnsResponse response = instanceNameToResponse.remove(serviceInstanceName); final MdnsResponse response = instanceNameToResponse.remove(serviceInstanceName);
if (response == null) { if (response == null) {
return; return;
@@ -427,32 +471,52 @@ public class MdnsServiceTypeClient {
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst(); MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
private final boolean usePassiveMode; private final boolean usePassiveMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks; private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
private final long sessionId; private final int numOfQueriesBeforeBackoff;
@VisibleForTesting @VisibleForTesting
int transactionId; final int transactionId;
@VisibleForTesting @VisibleForTesting
boolean expectUnicastResponse; final boolean expectUnicastResponse;
private int queriesPerBurst; private final int queriesPerBurst;
private int timeBetweenBurstsInMs; private final int timeBetweenBurstsInMs;
private int burstCounter; private final int burstCounter;
private int timeToRunNextTaskInMs; private final long delayUntilNextTaskWithoutBackoffMs;
private boolean isFirstBurst; private final boolean isFirstBurst;
private final long queryCount;
@NonNull private final SocketKey socketKey; @NonNull private final SocketKey socketKey;
QueryTaskConfig(@NonNull QueryTaskConfig other, long queryCount, int transactionId,
boolean expectUnicastResponse, boolean isFirstBurst, int burstCounter,
int queriesPerBurst, int timeBetweenBurstsInMs,
long delayUntilNextTaskWithoutBackoffMs) {
this.subtypes = new ArrayList<>(other.subtypes);
this.usePassiveMode = other.usePassiveMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = other.onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = other.numOfQueriesBeforeBackoff;
this.transactionId = transactionId;
this.expectUnicastResponse = expectUnicastResponse;
this.queriesPerBurst = queriesPerBurst;
this.timeBetweenBurstsInMs = timeBetweenBurstsInMs;
this.burstCounter = burstCounter;
this.delayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
this.isFirstBurst = isFirstBurst;
this.queryCount = queryCount;
this.socketKey = other.socketKey;
}
QueryTaskConfig(@NonNull Collection<String> subtypes, QueryTaskConfig(@NonNull Collection<String> subtypes,
boolean usePassiveMode, boolean usePassiveMode,
boolean onlyUseIpv6OnIpv6OnlyNetworks, boolean onlyUseIpv6OnIpv6OnlyNetworks,
long sessionId, int numOfQueriesBeforeBackoff,
@Nullable SocketKey socketKey) { @Nullable SocketKey socketKey) {
this.usePassiveMode = usePassiveMode; this.usePassiveMode = usePassiveMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks; this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.subtypes = new ArrayList<>(subtypes); this.subtypes = new ArrayList<>(subtypes);
this.queriesPerBurst = QUERIES_PER_BURST; this.queriesPerBurst = QUERIES_PER_BURST;
this.burstCounter = 0; this.burstCounter = 0;
this.transactionId = 1; this.transactionId = 1;
this.expectUnicastResponse = true; this.expectUnicastResponse = true;
this.isFirstBurst = true; this.isFirstBurst = true;
this.sessionId = sessionId;
// Config the scan frequency based on the scan mode. // Config the scan frequency based on the scan mode.
if (this.usePassiveMode) { if (this.usePassiveMode) {
// In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
@@ -467,42 +531,61 @@ public class MdnsServiceTypeClient {
this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS; this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
} }
this.socketKey = socketKey; this.socketKey = socketKey;
this.queryCount = 0;
this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
} }
QueryTaskConfig getConfigForNextRun() { QueryTaskConfig getConfigForNextRun() {
if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) { long newQueryCount = queryCount + 1;
transactionId = 1; int newTransactionId = transactionId + 1;
if (newTransactionId > UNSIGNED_SHORT_MAX_VALUE) {
newTransactionId = 1;
} }
boolean newExpectUnicastResponse = false;
boolean newIsFirstBurst = isFirstBurst;
int newQueriesPerBurst = queriesPerBurst;
int newBurstCounter = burstCounter + 1;
long newDelayUntilNextTaskWithoutBackoffMs = delayUntilNextTaskWithoutBackoffMs;
int newTimeBetweenBurstsInMs = timeBetweenBurstsInMs;
// Only the first query expects uni-cast response. // Only the first query expects uni-cast response.
expectUnicastResponse = false; if (newBurstCounter == queriesPerBurst) {
if (++burstCounter == queriesPerBurst) { newBurstCounter = 0;
burstCounter = 0;
if (alwaysAskForUnicastResponse) { if (alwaysAskForUnicastResponse) {
expectUnicastResponse = true; newExpectUnicastResponse = true;
} }
// In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and // In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and
// then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE // then in each TIME_BETWEEN_BURSTS interval, sends QUERIES_PER_BURST_PASSIVE_MODE
// queries. // queries.
if (isFirstBurst) { if (isFirstBurst) {
isFirstBurst = false; newIsFirstBurst = false;
if (usePassiveMode) { if (usePassiveMode) {
queriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE; newQueriesPerBurst = QUERIES_PER_BURST_PASSIVE_MODE;
} }
} }
// In active scan mode, sends a burst of QUERIES_PER_BURST queries, // In active scan mode, sends a burst of QUERIES_PER_BURST queries,
// TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and // TIME_BETWEEN_QUERIES_IN_BURST_MS apart, then waits for the scan interval, and
// then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and // then repeats. The scan interval starts as INITIAL_TIME_BETWEEN_BURSTS_MS and
// doubles until it maxes out at TIME_BETWEEN_BURSTS_MS. // doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
timeToRunNextTaskInMs = timeBetweenBurstsInMs; newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs;
if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) { if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2, newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
TIME_BETWEEN_BURSTS_MS); TIME_BETWEEN_BURSTS_MS);
} }
} else { } else {
timeToRunNextTaskInMs = TIME_BETWEEN_QUERIES_IN_BURST_MS; newDelayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
} }
return this; return new QueryTaskConfig(this, newQueryCount, newTransactionId,
newExpectUnicastResponse, newIsFirstBurst, newBurstCounter, newQueriesPerBurst,
newTimeBetweenBurstsInMs, newDelayUntilNextTaskWithoutBackoffMs);
}
private boolean shouldUseQueryBackoff() {
// Don't enable backoff mode during the burst or in the first burst
if (burstCounter != 0 || isFirstBurst) {
return false;
}
return queryCount > numOfQueriesBeforeBackoff;
} }
} }
@@ -532,9 +615,17 @@ public class MdnsServiceTypeClient {
private class QueryTask implements Runnable { private class QueryTask implements Runnable {
private final QueryTaskConfig config; private final QueryTaskConfig config;
private final long timeToRun;
private final long minTtlExpirationTimeWhenScheduled;
private final long sessionId;
QueryTask(@NonNull QueryTaskConfig config) { QueryTask(@NonNull QueryTaskConfig config, long timeToRun,
long minTtlExpirationTimeWhenScheduled,
long sessionId) {
this.config = config; this.config = config;
this.timeToRun = timeToRun;
this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled;
this.sessionId = sessionId;
} }
@Override @Override
@@ -573,13 +664,13 @@ public class MdnsServiceTypeClient {
if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) { if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
// In case that the task is not canceled successfully, use session ID to check // In case that the task is not canceled successfully, use session ID to check
// if this task should continue to schedule more. // if this task should continue to schedule more.
if (config.sessionId != currentSessionId) { if (sessionId != currentSessionId) {
return; return;
} }
} }
if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) { if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
if (requestTaskFuture == null) { if (nextQueryTaskFuture == null) {
// If requestTaskFuture is set to null, the task is cancelled. We can't use // If requestTaskFuture is set to null, the task is cancelled. We can't use
// isCancelled() here because this QueryTask is different from the future // isCancelled() here because this QueryTask is different from the future
// that is returned from executor.schedule(). See b/71646910. // that is returned from executor.schedule(). See b/71646910.
@@ -624,14 +715,72 @@ public class MdnsServiceTypeClient {
} }
} }
} }
requestTaskFuture = scheduleNextRunLocked(this.config); QueryTaskConfig nextRunConfig = this.config.getConfigForNextRun();
final long now = clock.elapsedRealtime();
lastSentTime = now;
final long minRemainingTtl = getMinRemainingTtlLocked(now);
final long timeToRun = calculateTimeToRun(this, nextRunConfig, now,
minRemainingTtl, lastSentTime);
nextQueryTaskFuture = scheduleNextRunLocked(nextRunConfig,
minRemainingTtl, now, timeToRun, lastScheduledTask.sessionId);
} }
} }
} }
private static long calculateTimeToRun(@NonNull QueryTask lastScheduledTask,
QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
if (!queryTaskConfig.shouldUseQueryBackoff()) {
return lastSentTime + baseDelayInMs;
}
if (minRemainingTtl <= 0) {
// There's no service, or there is an expired service. In any case, schedule for the
// minimum time, which is the base delay.
return lastSentTime + baseDelayInMs;
}
// If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
if (lastSentTime < now
&& lastScheduledTask.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
// Use the original scheduling time if the TTL has not changed, to avoid continuously
// rescheduling to 80% of the remaining TTL as time passes
return lastScheduledTask.timeToRun;
}
return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
}
@GuardedBy("lock")
private long getMinRemainingTtlLocked(long now) {
long minRemainingTtl = Long.MAX_VALUE;
for (MdnsResponse response : instanceNameToResponse.values()) {
if (!response.isComplete()) {
continue;
}
long remainingTtl =
response.getServiceRecord().getRemainingTTL(now);
// remainingTtl is <= 0 means the service expired.
if (remainingTtl <= 0) {
return 0;
}
if (remainingTtl < minRemainingTtl) {
minRemainingTtl = remainingTtl;
}
}
return minRemainingTtl == Long.MAX_VALUE ? 0 : minRemainingTtl;
}
@GuardedBy("lock")
@NonNull @NonNull
private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig lastRunConfig) { private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig nextRunConfig,
QueryTaskConfig config = lastRunConfig.getConfigForNextRun(); long minRemainingTtl,
return executor.schedule(new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS); long timeWhenScheduled, long timeToRun, long sessionId) {
lastScheduledTask = new QueryTask(nextRunConfig, timeToRun,
minRemainingTtl + timeWhenScheduled, sessionId);
// The timeWhenScheduled could be greater than the timeToRun if the Runnable is delayed.
long timeToNextTasksWithBackoffInMs = Math.max(timeToRun - timeWhenScheduled, 0);
sharedLog.log(
String.format("Next run: sessionId: %d, in %d ms", lastScheduledTask.sessionId,
timeToNextTasksWithBackoffInMs));
return executor.schedule(lastScheduledTask, timeToNextTasksWithBackoffInMs,
MILLISECONDS);
} }
} }

View File

@@ -288,6 +288,110 @@ public class MdnsServiceTypeClientTests {
verify(expectedSendFutures[5]).cancel(true); verify(expectedSendFutures[5]).cancel(true);
} }
@Test
public void sendQueries_activeScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(
false).setNumOfQueriesBeforeBackoff(11).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
// First burst, 3 queries.
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Second burst will be sent after initialTimeBetweenBurstsMs, 3 queries.
verifyAndSendQuery(
3, MdnsConfigs.initialTimeBetweenBurstsMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
4, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
5, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Third burst will be sent after initialTimeBetweenBurstsMs * 2, 3 queries.
verifyAndSendQuery(
6, MdnsConfigs.initialTimeBetweenBurstsMs() * 2, /* expectsUnicastResponse= */
false);
verifyAndSendQuery(
7, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
8, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// Forth burst will be sent after initialTimeBetweenBurstsMs * 4, 3 queries.
verifyAndSendQuery(
9, MdnsConfigs.initialTimeBetweenBurstsMs() * 4, /* expectsUnicastResponse= */
false);
verifyAndSendQuery(
10, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
11, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
// In backoff mode, the current scheduled task will be canceled and reschedule if the
// 0.8 * smallestRemainingTtl is larger than time to next run.
long currentTime = TEST_TTL / 2 + TEST_ELAPSED_REALTIME;
doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
client.processResponse(createResponse(
"service-instance-1", "192.0.2.123", 5353,
SERVICE_TYPE_LABELS,
Collections.emptyMap(), TEST_TTL), socketKey);
verifyAndSendQuery(12, (long) (TEST_TTL / 2 * 0.8), /* expectsUnicastResponse= */
false);
currentTime += (long) (TEST_TTL / 2 * 0.8);
doReturn(currentTime).when(mockDecoderClock).elapsedRealtime();
verifyAndSendQuery(
13, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
}
@Test
public void sendQueries_passiveScanWithQueryBackoff() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(
true).setNumOfQueriesBeforeBackoff(3).build();
client.startSendAndReceive(mockListenerOne, searchOptions);
verifyAndSendQuery(0, 0, /* expectsUnicastResponse= */ true);
verifyAndSendQuery(
1, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(
2, MdnsConfigs.timeBetweenQueriesInBurstMs(), /* expectsUnicastResponse= */ false);
verifyAndSendQuery(3, MdnsConfigs.timeBetweenBurstsMs(), /* expectsUnicastResponse= */
false);
assertEquals(4, currentThreadExecutor.getNumOfScheduledFuture());
// In backoff mode, the current scheduled task will be canceled and reschedule if the
// 0.8 * smallestRemainingTtl is larger than time to next run.
doReturn(TEST_ELAPSED_REALTIME + 20000).when(mockDecoderClock).elapsedRealtime();
client.processResponse(createResponse(
"service-instance-1", "192.0.2.123", 5353,
SERVICE_TYPE_LABELS,
Collections.emptyMap(), TEST_TTL), socketKey);
verify(expectedSendFutures[4]).cancel(true);
assertEquals(5, currentThreadExecutor.getNumOfScheduledFuture());
verifyAndSendQuery(4, 80000 /* timeInMs */, false /* expectsUnicastResponse */);
assertEquals(6, currentThreadExecutor.getNumOfScheduledFuture());
// Next run should also be scheduled in 0.8 * smallestRemainingTtl
verifyAndSendQuery(5, 80000 /* timeInMs */, false /* expectsUnicastResponse */);
assertEquals(7, currentThreadExecutor.getNumOfScheduledFuture());
// If the records is not refreshed, the current scheduled task will not be canceled.
doReturn(TEST_ELAPSED_REALTIME + 20001).when(mockDecoderClock).elapsedRealtime();
client.processResponse(createResponse(
"service-instance-1", "192.0.2.123", 5353,
SERVICE_TYPE_LABELS,
Collections.emptyMap(), TEST_TTL,
TEST_ELAPSED_REALTIME - 1), socketKey);
verify(expectedSendFutures[7], never()).cancel(true);
// In backoff mode, the current scheduled task will not be canceled if the
// 0.8 * smallestRemainingTtl is smaller than time to next run.
doReturn(TEST_ELAPSED_REALTIME).when(mockDecoderClock).elapsedRealtime();
client.processResponse(createResponse(
"service-instance-1", "192.0.2.123", 5353,
SERVICE_TYPE_LABELS,
Collections.emptyMap(), TEST_TTL), socketKey);
verify(expectedSendFutures[7], never()).cancel(true);
client.stopSendAndReceive(mockListenerOne);
verify(expectedSendFutures[7]).cancel(true);
}
@Test @Test
public void sendQueries_reentry_passiveScanMode() { public void sendQueries_reentry_passiveScanMode() {
MdnsSearchOptions searchOptions = MdnsSearchOptions searchOptions =
@@ -328,7 +432,8 @@ public class MdnsServiceTypeClientTests {
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build(); MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config = new QueryTaskConfig( QueryTaskConfig config = new QueryTaskConfig(
searchOptions.getSubtypes(), searchOptions.isPassiveMode(), searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 1, socketKey); false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// This is the first query. We will ask for unicast response. // This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse); assertTrue(config.expectUnicastResponse);
@@ -358,7 +463,8 @@ public class MdnsServiceTypeClientTests {
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build(); MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config = new QueryTaskConfig( QueryTaskConfig config = new QueryTaskConfig(
searchOptions.getSubtypes(), searchOptions.isPassiveMode(), searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
false /* onlyUseIpv6OnIpv6OnlyNetworks */, 1, socketKey); false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
socketKey);
// This is the first query. We will ask for unicast response. // This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse); assertTrue(config.expectUnicastResponse);
@@ -714,8 +820,10 @@ public class MdnsServiceTypeClientTests {
return mockPacketWriter; return mockPacketWriter;
} }
}; };
MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder().setRemoveExpiredService( MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
true).build(); .setRemoveExpiredService(true)
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
.build();
client.startSendAndReceive(mockListenerOne, searchOptions); client.startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable(); Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1248,13 +1356,16 @@ public class MdnsServiceTypeClientTests {
final String ipV4Address = "192.0.2.0"; final String ipV4Address = "192.0.2.0";
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder() final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
.setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
.setResolveInstanceName("instance1").build(); .setResolveInstanceName("instance1").build();
client.startSendAndReceive(mockListenerOne, resolveOptions); client.startSendAndReceive(mockListenerOne, resolveOptions);
// Ensure the first task is executed so it schedules a future task // Ensure the first task is executed so it schedules a future task
currentThreadExecutor.getAndClearSubmittedFuture().get( currentThreadExecutor.getAndClearSubmittedFuture().get(
TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
client.startSendAndReceive(mockListenerTwo, MdnsSearchOptions.getDefaultOptions()); client.startSendAndReceive(mockListenerTwo,
MdnsSearchOptions.newBuilder().setNumOfQueriesBeforeBackoff(
Integer.MAX_VALUE).build());
// Filing the second request cancels the first future // Filing the second request cancels the first future
verify(expectedSendFutures[0]).cancel(true); verify(expectedSendFutures[0]).cancel(true);
@@ -1317,7 +1428,7 @@ public class MdnsServiceTypeClientTests {
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse, private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
boolean multipleSocketDiscovery) { boolean multipleSocketDiscovery) {
assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs); assertEquals(timeInMs, currentThreadExecutor.getAndClearLastScheduledDelayInMs());
currentThreadExecutor.getAndClearLastScheduledRunnable().run(); currentThreadExecutor.getAndClearLastScheduledRunnable().run();
if (expectsUnicastResponse) { if (expectsUnicastResponse) {
verify(mockSocketClient).sendPacketRequestingUnicastResponse( verify(mockSocketClient).sendPacketRequestingUnicastResponse(
@@ -1406,6 +1517,10 @@ public class MdnsServiceTypeClientTests {
lastSubmittedFuture = null; lastSubmittedFuture = null;
return val; return val;
} }
public int getNumOfScheduledFuture() {
return futureIndex - 1;
}
} }
private MdnsPacket createResponse( private MdnsPacket createResponse(
@@ -1424,7 +1539,7 @@ public class MdnsServiceTypeClientTests {
textAttributes, ptrTtlMillis); textAttributes, ptrTtlMillis);
} }
// Creates a mDNS response.
private MdnsPacket createResponse( private MdnsPacket createResponse(
@NonNull String serviceInstanceName, @NonNull String serviceInstanceName,
@Nullable String host, @Nullable String host,
@@ -1432,6 +1547,19 @@ public class MdnsServiceTypeClientTests {
@NonNull String[] type, @NonNull String[] type,
@NonNull Map<String, String> textAttributes, @NonNull Map<String, String> textAttributes,
long ptrTtlMillis) { long ptrTtlMillis) {
return createResponse(serviceInstanceName, host, port, type, textAttributes, ptrTtlMillis,
TEST_ELAPSED_REALTIME);
}
// Creates a mDNS response.
private MdnsPacket createResponse(
@NonNull String serviceInstanceName,
@Nullable String host,
int port,
@NonNull String[] type,
@NonNull Map<String, String> textAttributes,
long ptrTtlMillis,
long receiptTimeMillis) {
final ArrayList<MdnsRecord> answerRecords = new ArrayList<>(); final ArrayList<MdnsRecord> answerRecords = new ArrayList<>();
@@ -1442,7 +1570,7 @@ public class MdnsServiceTypeClientTests {
final String[] serviceName = serviceNameList.toArray(new String[0]); final String[] serviceName = serviceNameList.toArray(new String[0]);
final MdnsPointerRecord pointerRecord = new MdnsPointerRecord( final MdnsPointerRecord pointerRecord = new MdnsPointerRecord(
type, type,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */, receiptTimeMillis,
false /* cacheFlush */, false /* cacheFlush */,
ptrTtlMillis, ptrTtlMillis,
serviceName); serviceName);
@@ -1451,7 +1579,7 @@ public class MdnsServiceTypeClientTests {
// Set SRV record. // Set SRV record.
final MdnsServiceRecord serviceRecord = new MdnsServiceRecord( final MdnsServiceRecord serviceRecord = new MdnsServiceRecord(
serviceName, serviceName,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */, receiptTimeMillis,
false /* cacheFlush */, false /* cacheFlush */,
TEST_TTL, TEST_TTL,
0 /* servicePriority */, 0 /* servicePriority */,
@@ -1465,7 +1593,7 @@ public class MdnsServiceTypeClientTests {
final InetAddress addr = InetAddresses.parseNumericAddress(host); final InetAddress addr = InetAddresses.parseNumericAddress(host);
final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord( final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord(
new String[] {"hostname"} /* name */, new String[] {"hostname"} /* name */,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */, receiptTimeMillis,
false /* cacheFlush */, false /* cacheFlush */,
TEST_TTL, TEST_TTL,
addr); addr);
@@ -1479,7 +1607,7 @@ public class MdnsServiceTypeClientTests {
} }
final MdnsTextRecord textRecord = new MdnsTextRecord( final MdnsTextRecord textRecord = new MdnsTextRecord(
serviceName, serviceName,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */, receiptTimeMillis,
false /* cacheFlush */, false /* cacheFlush */,
TEST_TTL, TEST_TTL,
textEntries); textEntries);