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.readParcelable(null),
source.readString(),
(source.dataAvail() > 0) ? source.readBoolean() : false);
source.readBoolean(),
source.readInt());
}
@Override
@@ -62,9 +63,9 @@ public class MdnsSearchOptions implements Parcelable {
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 @@ public class MdnsSearchOptions implements Parcelable {
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 @@ public class MdnsSearchOptions implements Parcelable {
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 @@ public class MdnsSearchOptions implements Parcelable {
out.writeParcelable(mNetwork, 0);
out.writeString(resolveInstanceName);
out.writeBoolean(onlyUseIpv6OnIpv6OnlyNetworks);
out.writeInt(numOfQueriesBeforeBackoff);
}
/** A builder to create {@link MdnsSearchOptions}. */
@@ -166,6 +178,7 @@ public class MdnsSearchOptions implements Parcelable {
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;
@@ -218,6 +231,14 @@ public class MdnsSearchOptions implements Parcelable {
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.
*
@@ -258,7 +279,8 @@ public class MdnsSearchOptions implements Parcelable {
removeExpiredService,
mNetwork,
resolveInstanceName,
onlyUseIpv6OnIpv6OnlyNetworks);
onlyUseIpv6OnIpv6OnlyNetworks,
numOfQueriesBeforeBackoff);
}
}
}

View File

@@ -50,6 +50,7 @@ import java.util.concurrent.ScheduledExecutorService;
*/
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
@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 @@ public class MdnsServiceTypeClient {
}
}
// 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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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();
}
}
}
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 @@ public class MdnsServiceTypeClient {
}
}
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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 @@ public class MdnsServiceTypeClient {
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 @@ 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
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);
}
}

View File

@@ -288,6 +288,110 @@ public class MdnsServiceTypeClientTests {
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
public void sendQueries_reentry_passiveScanMode() {
MdnsSearchOptions searchOptions =
@@ -328,7 +432,8 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
lastSubmittedFuture = null;
return val;
}
public int getNumOfScheduledFuture() {
return futureIndex - 1;
}
}
private MdnsPacket createResponse(
@@ -1424,7 +1539,7 @@ public class MdnsServiceTypeClientTests {
textAttributes, ptrTtlMillis);
}
// Creates a mDNS response.
private MdnsPacket createResponse(
@NonNull String serviceInstanceName,
@Nullable String host,
@@ -1432,6 +1547,19 @@ public class MdnsServiceTypeClientTests {
@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 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
// Set SRV record.
final MdnsServiceRecord serviceRecord = new MdnsServiceRecord(
serviceName,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
receiptTimeMillis,
false /* cacheFlush */,
TEST_TTL,
0 /* servicePriority */,
@@ -1465,7 +1593,7 @@ public class MdnsServiceTypeClientTests {
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 @@ public class MdnsServiceTypeClientTests {
}
final MdnsTextRecord textRecord = new MdnsTextRecord(
serviceName,
TEST_ELAPSED_REALTIME /* receiptTimeMillis */,
receiptTimeMillis,
false /* cacheFlush */,
TEST_TTL,
textEntries);