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
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
index 98c80ee..f09596d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsSearchOptions.java
@@ -50,7 +50,8 @@
source.readBoolean(),
source.readParcelable(null),
source.readString(),
- (source.dataAvail() > 0) ? source.readBoolean() : false);
+ source.readBoolean(),
+ source.readInt());
}
@Override
@@ -62,9 +63,9 @@
private final List<String> subtypes;
@Nullable
private final String resolveInstanceName;
-
private final boolean isPassiveMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
+ private final int numOfQueriesBeforeBackoff;
private final boolean removeExpiredService;
// The target network for searching. Null network means search on all possible interfaces.
@Nullable private final Network mNetwork;
@@ -76,13 +77,15 @@
boolean removeExpiredService,
@Nullable Network network,
@Nullable String resolveInstanceName,
- boolean onlyUseIpv6OnIpv6OnlyNetworks) {
+ boolean onlyUseIpv6OnIpv6OnlyNetworks,
+ int numOfQueriesBeforeBackoff) {
this.subtypes = new ArrayList<>();
if (subtypes != null) {
this.subtypes.addAll(subtypes);
}
this.isPassiveMode = isPassiveMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+ this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.removeExpiredService = removeExpiredService;
mNetwork = network;
this.resolveInstanceName = resolveInstanceName;
@@ -122,6 +125,14 @@
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. */
public boolean removeExpiredService() {
return removeExpiredService;
@@ -159,6 +170,7 @@
out.writeParcelable(mNetwork, 0);
out.writeString(resolveInstanceName);
out.writeBoolean(onlyUseIpv6OnIpv6OnlyNetworks);
+ out.writeInt(numOfQueriesBeforeBackoff);
}
/** A builder to create {@link MdnsSearchOptions}. */
@@ -166,6 +178,7 @@
private final Set<String> subtypes;
private boolean isPassiveMode = true;
private boolean onlyUseIpv6OnIpv6OnlyNetworks = false;
+ private int numOfQueriesBeforeBackoff = 3;
private boolean removeExpiredService;
private Network mNetwork;
private String resolveInstanceName;
@@ -219,6 +232,14 @@
}
/**
+ * 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.
*
* @param removeExpiredService If set to {@code true}, the service will be removed after TTL
@@ -258,7 +279,8 @@
removeExpiredService,
mNetwork,
resolveInstanceName,
- onlyUseIpv6OnIpv6OnlyNetworks);
+ onlyUseIpv6OnIpv6OnlyNetworks,
+ numOfQueriesBeforeBackoff);
}
}
}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 48e4724..8d5949c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -50,6 +50,7 @@
*/
public class MdnsServiceTypeClient {
+ private static final String TAG = MdnsServiceTypeClient.class.getSimpleName();
private static final int DEFAULT_MTU = 1500;
private final String serviceType;
@@ -63,6 +64,7 @@
private final ArrayMap<MdnsServiceBrowserListener, MdnsSearchOptions> listeners =
new ArrayMap<>();
// TODO: change instanceNameToResponse to TreeMap with case insensitive comparator.
+ @GuardedBy("lock")
private final Map<String, MdnsResponse> instanceNameToResponse = new HashMap<>();
private final boolean removeServiceAfterTtlExpires =
MdnsConfigs.removeServiceAfterTtlExpires();
@@ -77,7 +79,14 @@
@GuardedBy("lock")
@Nullable
- private Future<?> requestTaskFuture;
+ private Future<?> nextQueryTaskFuture;
+
+ @GuardedBy("lock")
+ @Nullable
+ private QueryTask lastScheduledTask;
+
+ @GuardedBy("lock")
+ private long lastSentTime;
/**
* Constructor of {@link MdnsServiceTypeClient}.
@@ -189,7 +198,7 @@
}
}
// Cancel the next scheduled periodical task.
- if (requestTaskFuture != null) {
+ if (nextQueryTaskFuture != null) {
cancelRequestTaskLocked();
}
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
@@ -198,21 +207,40 @@
searchOptions.getSubtypes(),
searchOptions.isPassiveMode(),
searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
- currentSessionId,
+ searchOptions.numOfQueriesBeforeBackoff(),
socketKey);
+ final long now = clock.elapsedRealtime();
+ if (lastSentTime == 0) {
+ lastSentTime = now;
+ }
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 {
- requestTaskFuture = executor.submit(new QueryTask(taskConfig));
+ lastScheduledTask = new QueryTask(taskConfig,
+ now /* timeToRun */,
+ now + getMinRemainingTtlLocked(now)/* minTtlExpirationTimeWhenScheduled */,
+ currentSessionId);
+ nextQueryTaskFuture = executor.submit(lastScheduledTask);
}
}
}
@GuardedBy("lock")
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;
- requestTaskFuture = null;
+ nextQueryTaskFuture = null;
+ lastScheduledTask = null;
+ }
+
+ private static String getHexString(Object o) {
+ return Integer.toHexString(System.identityHashCode(o));
}
private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@@ -247,7 +275,7 @@
if (listeners.remove(listener) == null) {
return listeners.isEmpty();
}
- if (listeners.isEmpty() && requestTaskFuture != null) {
+ if (listeners.isEmpty() && nextQueryTaskFuture != null) {
cancelRequestTaskLocked();
}
return listeners.isEmpty();
@@ -284,9 +312,9 @@
for (MdnsResponse response : allResponses) {
if (modifiedResponse.contains(response)) {
if (response.isGoodbye()) {
- onGoodbyeReceived(response.getServiceInstanceName());
+ onGoodbyeReceivedLocked(response.getServiceInstanceName());
} else {
- onResponseModified(response);
+ onResponseModifiedLocked(response);
}
} else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
// If the response is not modified and already in the cache. The cache will
@@ -294,6 +322,20 @@
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 @@
}
}
- if (requestTaskFuture != null) {
+ if (nextQueryTaskFuture != null) {
cancelRequestTaskLocked();
}
}
}
- private void onResponseModified(@NonNull MdnsResponse response) {
+ @GuardedBy("lock")
+ private void onResponseModifiedLocked(@NonNull MdnsResponse response) {
final String serviceInstanceName = response.getServiceInstanceName();
final MdnsResponse currentResponse =
instanceNameToResponse.get(serviceInstanceName);
@@ -375,7 +418,8 @@
}
}
- private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
+ @GuardedBy("lock")
+ private void onGoodbyeReceivedLocked(@Nullable String serviceInstanceName) {
final MdnsResponse response = instanceNameToResponse.remove(serviceInstanceName);
if (response == null) {
return;
@@ -427,32 +471,52 @@
MdnsConfigs.alwaysAskForUnicastResponseInEachBurst();
private final boolean usePassiveMode;
private final boolean onlyUseIpv6OnIpv6OnlyNetworks;
- private final long sessionId;
+ private final int numOfQueriesBeforeBackoff;
@VisibleForTesting
- int transactionId;
+ final int transactionId;
@VisibleForTesting
- boolean expectUnicastResponse;
- private int queriesPerBurst;
- private int timeBetweenBurstsInMs;
- private int burstCounter;
- private int timeToRunNextTaskInMs;
- private boolean isFirstBurst;
+ final boolean expectUnicastResponse;
+ private final int queriesPerBurst;
+ private final int timeBetweenBurstsInMs;
+ private final int burstCounter;
+ private final long delayUntilNextTaskWithoutBackoffMs;
+ private final boolean isFirstBurst;
+ private final long queryCount;
@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,
boolean usePassiveMode,
boolean onlyUseIpv6OnIpv6OnlyNetworks,
- long sessionId,
+ int numOfQueriesBeforeBackoff,
@Nullable SocketKey socketKey) {
this.usePassiveMode = usePassiveMode;
this.onlyUseIpv6OnIpv6OnlyNetworks = onlyUseIpv6OnIpv6OnlyNetworks;
+ this.numOfQueriesBeforeBackoff = numOfQueriesBeforeBackoff;
this.subtypes = new ArrayList<>(subtypes);
this.queriesPerBurst = QUERIES_PER_BURST;
this.burstCounter = 0;
this.transactionId = 1;
this.expectUnicastResponse = true;
this.isFirstBurst = true;
- this.sessionId = sessionId;
// Config the scan frequency based on the scan mode.
if (this.usePassiveMode) {
// In passive scan mode, sends a single burst of QUERIES_PER_BURST queries, and then
@@ -467,42 +531,61 @@
this.timeBetweenBurstsInMs = INITIAL_TIME_BETWEEN_BURSTS_MS;
}
this.socketKey = socketKey;
+ this.queryCount = 0;
+ this.delayUntilNextTaskWithoutBackoffMs = TIME_BETWEEN_QUERIES_IN_BURST_MS;
}
QueryTaskConfig getConfigForNextRun() {
- if (++transactionId > UNSIGNED_SHORT_MAX_VALUE) {
- transactionId = 1;
+ long newQueryCount = queryCount + 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.
- expectUnicastResponse = false;
- if (++burstCounter == queriesPerBurst) {
- burstCounter = 0;
+ if (newBurstCounter == queriesPerBurst) {
+ newBurstCounter = 0;
if (alwaysAskForUnicastResponse) {
- expectUnicastResponse = true;
+ newExpectUnicastResponse = true;
}
// 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
// queries.
if (isFirstBurst) {
- isFirstBurst = false;
+ newIsFirstBurst = false;
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,
// 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
// doubles until it maxes out at TIME_BETWEEN_BURSTS_MS.
- timeToRunNextTaskInMs = timeBetweenBurstsInMs;
+ newDelayUntilNextTaskWithoutBackoffMs = timeBetweenBurstsInMs;
if (timeBetweenBurstsInMs < TIME_BETWEEN_BURSTS_MS) {
- timeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
+ newTimeBetweenBurstsInMs = Math.min(timeBetweenBurstsInMs * 2,
TIME_BETWEEN_BURSTS_MS);
}
} 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 @@
private class QueryTask implements Runnable {
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.timeToRun = timeToRun;
+ this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled;
+ this.sessionId = sessionId;
}
@Override
@@ -573,13 +664,13 @@
if (MdnsConfigs.useSessionIdToScheduleMdnsTask()) {
// In case that the task is not canceled successfully, use session ID to check
// if this task should continue to schedule more.
- if (config.sessionId != currentSessionId) {
+ if (sessionId != currentSessionId) {
return;
}
}
if (MdnsConfigs.shouldCancelScanTaskWhenFutureIsNull()) {
- if (requestTaskFuture == null) {
+ if (nextQueryTaskFuture == null) {
// If requestTaskFuture is set to null, the task is cancelled. We can't use
// isCancelled() here because this QueryTask is different from the future
// that is returned from executor.schedule(). See b/71646910.
@@ -624,14 +715,72 @@
}
}
}
- 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
- private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig lastRunConfig) {
- QueryTaskConfig config = lastRunConfig.getConfigForNextRun();
- return executor.schedule(new QueryTask(config), config.timeToRunNextTaskInMs, MILLISECONDS);
+ private Future<?> scheduleNextRunLocked(@NonNull QueryTaskConfig nextRunConfig,
+ long minRemainingTtl,
+ 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);
}
}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 03e893f..0a299bf 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -289,6 +289,110 @@
}
@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
public void sendQueries_reentry_passiveScanMode() {
MdnsSearchOptions searchOptions =
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(true).build();
@@ -328,7 +432,8 @@
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config = new QueryTaskConfig(
searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 1, socketKey);
+ false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
+ socketKey);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -358,7 +463,8 @@
MdnsSearchOptions.newBuilder().addSubtype("12345").setIsPassiveMode(false).build();
QueryTaskConfig config = new QueryTaskConfig(
searchOptions.getSubtypes(), searchOptions.isPassiveMode(),
- false /* onlyUseIpv6OnIpv6OnlyNetworks */, 1, socketKey);
+ false /* onlyUseIpv6OnIpv6OnlyNetworks */, 3 /* numOfQueriesBeforeBackoff */,
+ socketKey);
// This is the first query. We will ask for unicast response.
assertTrue(config.expectUnicastResponse);
@@ -714,8 +820,10 @@
return mockPacketWriter;
}
};
- MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder().setRemoveExpiredService(
- true).build();
+ MdnsSearchOptions searchOptions = MdnsSearchOptions.newBuilder()
+ .setRemoveExpiredService(true)
+ .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
+ .build();
client.startSendAndReceive(mockListenerOne, searchOptions);
Runnable firstMdnsTask = currentThreadExecutor.getAndClearSubmittedRunnable();
@@ -1248,13 +1356,16 @@
final String ipV4Address = "192.0.2.0";
final MdnsSearchOptions resolveOptions = MdnsSearchOptions.newBuilder()
+ .setNumOfQueriesBeforeBackoff(Integer.MAX_VALUE)
.setResolveInstanceName("instance1").build();
client.startSendAndReceive(mockListenerOne, resolveOptions);
// Ensure the first task is executed so it schedules a future task
currentThreadExecutor.getAndClearSubmittedFuture().get(
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
verify(expectedSendFutures[0]).cancel(true);
@@ -1317,7 +1428,7 @@
private void verifyAndSendQuery(int index, long timeInMs, boolean expectsUnicastResponse,
boolean multipleSocketDiscovery) {
- assertEquals(currentThreadExecutor.getAndClearLastScheduledDelayInMs(), timeInMs);
+ assertEquals(timeInMs, currentThreadExecutor.getAndClearLastScheduledDelayInMs());
currentThreadExecutor.getAndClearLastScheduledRunnable().run();
if (expectsUnicastResponse) {
verify(mockSocketClient).sendPacketRequestingUnicastResponse(
@@ -1406,6 +1517,10 @@
lastSubmittedFuture = null;
return val;
}
+
+ public int getNumOfScheduledFuture() {
+ return futureIndex - 1;
+ }
}
private MdnsPacket createResponse(
@@ -1424,7 +1539,7 @@
textAttributes, ptrTtlMillis);
}
- // Creates a mDNS response.
+
private MdnsPacket createResponse(
@NonNull String serviceInstanceName,
@Nullable String host,
@@ -1432,6 +1547,19 @@
@NonNull String[] type,
@NonNull Map<String, String> textAttributes,
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<>();
@@ -1442,7 +1570,7 @@
final String[] serviceName = serviceNameList.toArray(new String[0]);
final MdnsPointerRecord pointerRecord = new MdnsPointerRecord(
type,
- TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+ receiptTimeMillis,
false /* cacheFlush */,
ptrTtlMillis,
serviceName);
@@ -1451,7 +1579,7 @@
// Set SRV record.
final MdnsServiceRecord serviceRecord = new MdnsServiceRecord(
serviceName,
- TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+ receiptTimeMillis,
false /* cacheFlush */,
TEST_TTL,
0 /* servicePriority */,
@@ -1465,7 +1593,7 @@
final InetAddress addr = InetAddresses.parseNumericAddress(host);
final MdnsInetAddressRecord inetAddressRecord = new MdnsInetAddressRecord(
new String[] {"hostname"} /* name */,
- TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+ receiptTimeMillis,
false /* cacheFlush */,
TEST_TTL,
addr);
@@ -1479,7 +1607,7 @@
}
final MdnsTextRecord textRecord = new MdnsTextRecord(
serviceName,
- TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
+ receiptTimeMillis,
false /* cacheFlush */,
TEST_TTL,
textEntries);