Merge "Remove synchronized lock in MdnsServiceTypeClient" into main

This commit is contained in:
Paul Hu
2023-07-12 09:48:48 +00:00
committed by Gerrit Code Review
3 changed files with 248 additions and 216 deletions

View File

@@ -17,7 +17,6 @@
package com.android.server.connectivity.mdns; package com.android.server.connectivity.mdns;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -106,12 +105,11 @@ public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<Str
// Incompatible return type for override of Callable#call(). // Incompatible return type for override of Callable#call().
@SuppressWarnings("nullness:override.return.invalid") @SuppressWarnings("nullness:override.return.invalid")
@Override @Override
@Nullable
public Pair<Integer, List<String>> call() { public Pair<Integer, List<String>> call() {
try { try {
MdnsSocketClientBase requestSender = weakRequestSender.get(); MdnsSocketClientBase requestSender = weakRequestSender.get();
if (requestSender == null) { if (requestSender == null) {
return null; return Pair.create(-1, new ArrayList<>());
} }
int numQuestions = 0; int numQuestions = 0;
@@ -158,7 +156,7 @@ public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<Str
if (numQuestions == 0) { if (numQuestions == 0) {
// No query to send // No query to send
return null; return Pair.create(-1, new ArrayList<>());
} }
// Header. // Header.
@@ -197,7 +195,7 @@ public class EnqueueMdnsQueryCallable implements Callable<Pair<Integer, List<Str
} catch (IOException e) { } catch (IOException e) {
LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.", LOGGER.e(String.format("Failed to create mDNS packet for subtype: %s.",
TextUtils.join(",", subtypes)), e); TextUtils.join(",", subtypes)), e);
return null; return Pair.create(-1, new ArrayList<>());
} }
} }

View File

@@ -56,6 +56,7 @@ public class MdnsServiceTypeClient {
private static final int DEFAULT_MTU = 1500; private static final int DEFAULT_MTU = 1500;
@VisibleForTesting @VisibleForTesting
static final int EVENT_START_QUERYTASK = 1; static final int EVENT_START_QUERYTASK = 1;
static final int EVENT_QUERY_RESULT = 2;
private final String serviceType; private final String serviceType;
private final String[] serviceTypeLabels; private final String[] serviceTypeLabels;
@@ -66,11 +67,9 @@ public class MdnsServiceTypeClient {
@NonNull private final SharedLog sharedLog; @NonNull private final SharedLog sharedLog;
@NonNull private final Handler handler; @NonNull private final Handler handler;
@NonNull private final Dependencies dependencies; @NonNull private final Dependencies dependencies;
private final Object lock = new Object();
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();
@@ -83,11 +82,8 @@ public class MdnsServiceTypeClient {
// new subtypes. It stays the same between packets for same subtypes. // new subtypes. It stays the same between packets for same subtypes.
private long currentSessionId = 0; private long currentSessionId = 0;
@GuardedBy("lock")
@Nullable @Nullable
private QueryTask lastScheduledTask; private ScheduledQueryTaskArgs lastScheduledQueryTaskArgs;
@GuardedBy("lock")
private long lastSentTime; private long lastSentTime;
private class QueryTaskHandler extends Handler { private class QueryTaskHandler extends Handler {
@@ -98,9 +94,48 @@ public class MdnsServiceTypeClient {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
switch (msg.what) { switch (msg.what) {
case EVENT_START_QUERYTASK: case EVENT_START_QUERYTASK: {
handleStartQueryTask((QueryTask) msg.obj); final ScheduledQueryTaskArgs taskArgs = (ScheduledQueryTaskArgs) msg.obj;
// QueryTask should be run immediately after being created (not be scheduled in
// advance). Because the result of "makeResponsesForResolve" depends on answers
// that were received before it is called, so to take into account all answers
// before sending the query, it needs to be called just before sending it.
final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
final QueryTask queryTask = new QueryTask(taskArgs, servicesToResolve,
servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
executor.submit(queryTask);
break; break;
}
case EVENT_QUERY_RESULT: {
final QuerySentResult sentResult = (QuerySentResult) msg.obj;
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 (sentResult.taskArgs.sessionId != currentSessionId) {
break;
}
}
if ((sentResult.transactionId != -1)) {
for (int i = 0; i < listeners.size(); i++) {
listeners.keyAt(i).onDiscoveryQuerySent(
sentResult.subTypes, sentResult.transactionId);
}
}
tryRemoveServiceAfterTtlExpires();
final QueryTaskConfig nextRunConfig =
sentResult.taskArgs.config.getConfigForNextRun();
final long now = clock.elapsedRealtime();
lastSentTime = now;
final long minRemainingTtl = getMinRemainingTtl(now);
final long timeToRun = calculateTimeToRun(lastScheduledQueryTaskArgs,
nextRunConfig, now, minRemainingTtl, lastSentTime);
scheduleNextRun(nextRunConfig, minRemainingTtl, now, timeToRun,
lastScheduledQueryTaskArgs.sessionId);
break;
}
default: default:
sharedLog.e("Unrecognized event " + msg.what); sharedLog.e("Unrecognized event " + msg.what);
break; break;
@@ -134,6 +169,13 @@ public class MdnsServiceTypeClient {
public boolean hasMessages(@NonNull Handler handler, int what) { public boolean hasMessages(@NonNull Handler handler, int what) {
return handler.hasMessages(what); return handler.hasMessages(what);
} }
/**
* @see Handler#post(Runnable)
*/
public void sendMessage(@NonNull Handler handler, @NonNull Message message) {
handler.sendMessage(message);
}
} }
/** /**
@@ -236,62 +278,57 @@ public class MdnsServiceTypeClient {
@NonNull MdnsServiceBrowserListener listener, @NonNull MdnsServiceBrowserListener listener,
@NonNull MdnsSearchOptions searchOptions) { @NonNull MdnsSearchOptions searchOptions) {
ensureRunningOnHandlerThread(handler); ensureRunningOnHandlerThread(handler);
synchronized (lock) { this.searchOptions = searchOptions;
this.searchOptions = searchOptions; boolean hadReply = false;
boolean hadReply = false; if (listeners.put(listener, searchOptions) == null) {
if (listeners.put(listener, searchOptions) == null) { for (MdnsResponse existingResponse : instanceNameToResponse.values()) {
for (MdnsResponse existingResponse : instanceNameToResponse.values()) { if (!responseMatchesOptions(existingResponse, searchOptions)) continue;
if (!responseMatchesOptions(existingResponse, searchOptions)) continue; final MdnsServiceInfo info =
final MdnsServiceInfo info = buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels);
buildMdnsServiceInfoFromResponse(existingResponse, serviceTypeLabels); listener.onServiceNameDiscovered(info);
listener.onServiceNameDiscovered(info); if (existingResponse.isComplete()) {
if (existingResponse.isComplete()) { listener.onServiceFound(info);
listener.onServiceFound(info); hadReply = true;
hadReply = true;
}
} }
} }
// Remove the next scheduled periodical task. }
removeScheduledTaskLock(); // Remove the next scheduled periodical task.
// Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not removeScheduledTask();
// interested anymore. // Keep tracking the ScheduledFuture for the task so we can cancel it if caller is not
final QueryTaskConfig taskConfig = new QueryTaskConfig( // interested anymore.
searchOptions.getSubtypes(), final QueryTaskConfig taskConfig = new QueryTaskConfig(
searchOptions.isPassiveMode(), searchOptions.getSubtypes(),
searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(), searchOptions.isPassiveMode(),
searchOptions.numOfQueriesBeforeBackoff(), searchOptions.onlyUseIpv6OnIpv6OnlyNetworks(),
socketKey); searchOptions.numOfQueriesBeforeBackoff(),
final long now = clock.elapsedRealtime(); socketKey);
if (lastSentTime == 0) { final long now = clock.elapsedRealtime();
lastSentTime = now; if (lastSentTime == 0) {
} lastSentTime = now;
if (hadReply) { }
final QueryTaskConfig queryTaskConfig = taskConfig.getConfigForNextRun(); if (hadReply) {
final long minRemainingTtl = getMinRemainingTtlLocked(now); final QueryTaskConfig queryTaskConfig = taskConfig.getConfigForNextRun();
final long timeToRun = now + queryTaskConfig.delayUntilNextTaskWithoutBackoffMs; final long minRemainingTtl = getMinRemainingTtl(now);
scheduleNextRunLocked( final long timeToRun = now + queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
queryTaskConfig, minRemainingTtl, now, timeToRun, currentSessionId); scheduleNextRun(
} else { queryTaskConfig, minRemainingTtl, now, timeToRun, currentSessionId);
lastScheduledTask = new QueryTask(taskConfig, } else {
now /* timeToRun */, final List<MdnsResponse> servicesToResolve = makeResponsesForResolve(socketKey);
now + getMinRemainingTtlLocked(now)/* minTtlExpirationTimeWhenScheduled */, lastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(taskConfig, now /* timeToRun */,
currentSessionId); now + getMinRemainingTtl(now)/* minTtlExpirationTimeWhenScheduled */,
handleStartQueryTask(lastScheduledTask); currentSessionId);
} final QueryTask queryTask = new QueryTask(lastScheduledQueryTaskArgs, servicesToResolve,
servicesToResolve.size() < listeners.size() /* sendDiscoveryQueries */);
executor.submit(queryTask);
} }
} }
@GuardedBy("lock") private void removeScheduledTask() {
private void removeScheduledTaskLock() {
dependencies.removeMessages(handler, EVENT_START_QUERYTASK); dependencies.removeMessages(handler, EVENT_START_QUERYTASK);
sharedLog.log("Remove EVENT_START_QUERYTASK" sharedLog.log("Remove EVENT_START_QUERYTASK"
+ ", current session: " + currentSessionId); + ", current session: " + currentSessionId);
++currentSessionId; ++currentSessionId;
lastScheduledTask = null; lastScheduledQueryTaskArgs = null;
}
private void handleStartQueryTask(@NonNull QueryTask task) {
executor.submit(task);
} }
private boolean responseMatchesOptions(@NonNull MdnsResponse response, private boolean responseMatchesOptions(@NonNull MdnsResponse response,
@@ -323,15 +360,13 @@ public class MdnsServiceTypeClient {
*/ */
public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) { public boolean stopSendAndReceive(@NonNull MdnsServiceBrowserListener listener) {
ensureRunningOnHandlerThread(handler); ensureRunningOnHandlerThread(handler);
synchronized (lock) { if (listeners.remove(listener) == null) {
if (listeners.remove(listener) == null) {
return listeners.isEmpty();
}
if (listeners.isEmpty()) {
removeScheduledTaskLock();
}
return listeners.isEmpty(); return listeners.isEmpty();
} }
if (listeners.isEmpty()) {
removeScheduledTask();
}
return listeners.isEmpty();
} }
/** /**
@@ -340,51 +375,48 @@ public class MdnsServiceTypeClient {
public synchronized void processResponse(@NonNull MdnsPacket packet, public synchronized void processResponse(@NonNull MdnsPacket packet,
@NonNull SocketKey socketKey) { @NonNull SocketKey socketKey) {
ensureRunningOnHandlerThread(handler); ensureRunningOnHandlerThread(handler);
synchronized (lock) { // Augment the list of current known responses, and generated responses for resolve
// Augment the list of current known responses, and generated responses for resolve // requests if there is no known response
// requests if there is no known response final List<MdnsResponse> currentList = new ArrayList<>(instanceNameToResponse.values());
final List<MdnsResponse> currentList = new ArrayList<>(instanceNameToResponse.values()); List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey);
List<MdnsResponse> additionalResponses = makeResponsesForResolve(socketKey); for (MdnsResponse additionalResponse : additionalResponses) {
for (MdnsResponse additionalResponse : additionalResponses) { if (!instanceNameToResponse.containsKey(
if (!instanceNameToResponse.containsKey( additionalResponse.getServiceInstanceName())) {
additionalResponse.getServiceInstanceName())) { currentList.add(additionalResponse);
currentList.add(additionalResponse);
}
} }
final Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult = }
responseDecoder.augmentResponses(packet, currentList, final Pair<ArraySet<MdnsResponse>, ArrayList<MdnsResponse>> augmentedResult =
socketKey.getInterfaceIndex(), socketKey.getNetwork()); responseDecoder.augmentResponses(packet, currentList,
socketKey.getInterfaceIndex(), socketKey.getNetwork());
final ArraySet<MdnsResponse> modifiedResponse = augmentedResult.first; final ArraySet<MdnsResponse> modifiedResponse = augmentedResult.first;
final ArrayList<MdnsResponse> allResponses = augmentedResult.second; final ArrayList<MdnsResponse> allResponses = augmentedResult.second;
for (MdnsResponse response : allResponses) { for (MdnsResponse response : allResponses) {
if (modifiedResponse.contains(response)) { if (modifiedResponse.contains(response)) {
if (response.isGoodbye()) { if (response.isGoodbye()) {
onGoodbyeReceivedLocked(response.getServiceInstanceName()); onGoodbyeReceived(response.getServiceInstanceName());
} else { } else {
onResponseModifiedLocked(response); onResponseModified(response);
}
} else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
instanceNameToResponse.put(response.getServiceInstanceName(), response);
} }
} else if (instanceNameToResponse.containsKey(response.getServiceInstanceName())) {
// If the response is not modified and already in the cache. The cache will
// need to be updated to refresh the last receipt time.
instanceNameToResponse.put(response.getServiceInstanceName(), response);
} }
if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK) }
&& lastScheduledTask != null if (dependencies.hasMessages(handler, EVENT_START_QUERYTASK)
&& lastScheduledTask.config.shouldUseQueryBackoff()) { && lastScheduledQueryTaskArgs != null
final long now = clock.elapsedRealtime(); && lastScheduledQueryTaskArgs.config.shouldUseQueryBackoff()) {
final long minRemainingTtl = getMinRemainingTtlLocked(now); final long now = clock.elapsedRealtime();
final long timeToRun = calculateTimeToRun(lastScheduledTask, final long minRemainingTtl = getMinRemainingTtl(now);
lastScheduledTask.config, now, final long timeToRun = calculateTimeToRun(lastScheduledQueryTaskArgs,
minRemainingTtl, lastSentTime); lastScheduledQueryTaskArgs.config, now,
if (timeToRun > lastScheduledTask.timeToRun) { minRemainingTtl, lastSentTime);
QueryTaskConfig lastTaskConfig = lastScheduledTask.config; if (timeToRun > lastScheduledQueryTaskArgs.timeToRun) {
removeScheduledTaskLock(); QueryTaskConfig lastTaskConfig = lastScheduledQueryTaskArgs.config;
scheduleNextRunLocked( removeScheduledTask();
lastTaskConfig, minRemainingTtl, now, timeToRun, currentSessionId); scheduleNextRun(lastTaskConfig, minRemainingTtl, now, timeToRun, currentSessionId);
}
} }
} }
} }
@@ -399,29 +431,26 @@ public class MdnsServiceTypeClient {
/** Notify all services are removed because the socket is destroyed. */ /** Notify all services are removed because the socket is destroyed. */
public void notifySocketDestroyed() { public void notifySocketDestroyed() {
ensureRunningOnHandlerThread(handler); ensureRunningOnHandlerThread(handler);
synchronized (lock) { for (MdnsResponse response : instanceNameToResponse.values()) {
for (MdnsResponse response : instanceNameToResponse.values()) { final String name = response.getServiceInstanceName();
final String name = response.getServiceInstanceName(); if (name == null) continue;
if (name == null) continue; for (int i = 0; i < listeners.size(); i++) {
for (int i = 0; i < listeners.size(); i++) { if (!responseMatchesOptions(response, listeners.valueAt(i))) continue;
if (!responseMatchesOptions(response, listeners.valueAt(i))) continue; final MdnsServiceBrowserListener listener = listeners.keyAt(i);
final MdnsServiceBrowserListener listener = listeners.keyAt(i); final MdnsServiceInfo serviceInfo =
final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(response, serviceTypeLabels);
buildMdnsServiceInfoFromResponse(response, serviceTypeLabels); if (response.isComplete()) {
if (response.isComplete()) { sharedLog.log("Socket destroyed. onServiceRemoved: " + name);
sharedLog.log("Socket destroyed. onServiceRemoved: " + name); listener.onServiceRemoved(serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
listener.onServiceNameRemoved(serviceInfo);
} }
sharedLog.log("Socket destroyed. onServiceNameRemoved: " + name);
listener.onServiceNameRemoved(serviceInfo);
} }
removeScheduledTaskLock();
} }
removeScheduledTask();
} }
@GuardedBy("lock") private void onResponseModified(@NonNull MdnsResponse response) {
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);
@@ -467,8 +496,7 @@ public class MdnsServiceTypeClient {
} }
} }
@GuardedBy("lock") private void onGoodbyeReceived(@Nullable String serviceInstanceName) {
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;
@@ -660,34 +688,80 @@ public class MdnsServiceTypeClient {
return resolveResponses; return resolveResponses;
} }
// A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task. private void tryRemoveServiceAfterTtlExpires() {
private class QueryTask implements Runnable { if (!shouldRemoveServiceAfterTtlExpires()) return;
Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
while (iter.hasNext()) {
MdnsResponse existingResponse = iter.next();
if (existingResponse.hasServiceRecord()
&& existingResponse.getServiceRecord()
.getRemainingTTL(clock.elapsedRealtime()) == 0) {
iter.remove();
for (int i = 0; i < listeners.size(); i++) {
if (!responseMatchesOptions(existingResponse, listeners.valueAt(i))) {
continue;
}
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
if (existingResponse.getServiceInstanceName() != null) {
final MdnsServiceInfo serviceInfo = buildMdnsServiceInfoFromResponse(
existingResponse, serviceTypeLabels);
if (existingResponse.isComplete()) {
sharedLog.log("TTL expired. onServiceRemoved: " + serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
sharedLog.log("TTL expired. onServiceNameRemoved: " + serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
}
}
}
private static class ScheduledQueryTaskArgs {
private final QueryTaskConfig config; private final QueryTaskConfig config;
private final long timeToRun; private final long timeToRun;
private final long minTtlExpirationTimeWhenScheduled; private final long minTtlExpirationTimeWhenScheduled;
private final long sessionId; private final long sessionId;
QueryTask(@NonNull QueryTaskConfig config, long timeToRun, ScheduledQueryTaskArgs(@NonNull QueryTaskConfig config, long timeToRun,
long minTtlExpirationTimeWhenScheduled, long minTtlExpirationTimeWhenScheduled, long sessionId) {
long sessionId) {
this.config = config; this.config = config;
this.timeToRun = timeToRun; this.timeToRun = timeToRun;
this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled; this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled;
this.sessionId = sessionId; this.sessionId = sessionId;
} }
}
private static class QuerySentResult {
private final int transactionId;
private final List<String> subTypes = new ArrayList<>();
private final ScheduledQueryTaskArgs taskArgs;
QuerySentResult(int transactionId, @NonNull List<String> subTypes,
@NonNull ScheduledQueryTaskArgs taskArgs) {
this.transactionId = transactionId;
this.subTypes.addAll(subTypes);
this.taskArgs = taskArgs;
}
}
// A FutureTask that enqueues a single query, and schedule a new FutureTask for the next task.
private class QueryTask implements Runnable {
private final ScheduledQueryTaskArgs taskArgs;
private final List<MdnsResponse> servicesToResolve = new ArrayList<>();
private final boolean sendDiscoveryQueries;
QueryTask(@NonNull ScheduledQueryTaskArgs taskArgs,
@NonNull List<MdnsResponse> servicesToResolve, boolean sendDiscoveryQueries) {
this.taskArgs = taskArgs;
this.servicesToResolve.addAll(servicesToResolve);
this.sendDiscoveryQueries = sendDiscoveryQueries;
}
@Override @Override
public void run() { public void run() {
final List<MdnsResponse> servicesToResolve;
final boolean sendDiscoveryQueries;
synchronized (lock) {
// The listener is requesting to resolve a service that has no info in
// cache. Use the provided name to generate a minimal response, so other records are
// queried to complete it.
servicesToResolve = makeResponsesForResolve(config.socketKey);
sendDiscoveryQueries = servicesToResolve.size() < listeners.size();
}
Pair<Integer, List<String>> result; Pair<Integer, List<String>> result;
try { try {
result = result =
@@ -695,80 +769,27 @@ public class MdnsServiceTypeClient {
socketClient, socketClient,
createMdnsPacketWriter(), createMdnsPacketWriter(),
serviceType, serviceType,
config.subtypes, taskArgs.config.subtypes,
config.expectUnicastResponse, taskArgs.config.expectUnicastResponse,
config.transactionId, taskArgs.config.transactionId,
config.socketKey, taskArgs.config.socketKey,
config.onlyUseIpv6OnIpv6OnlyNetworks, taskArgs.config.onlyUseIpv6OnIpv6OnlyNetworks,
sendDiscoveryQueries, sendDiscoveryQueries,
servicesToResolve, servicesToResolve,
clock) clock)
.call(); .call();
} catch (RuntimeException e) { } catch (RuntimeException e) {
sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s", sharedLog.e(String.format("Failed to run EnqueueMdnsQueryCallable for subtype: %s",
TextUtils.join(",", config.subtypes)), e); TextUtils.join(",", taskArgs.config.subtypes)), e);
result = null; result = Pair.create(-1, new ArrayList<>());
}
synchronized (lock) {
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 (sessionId != currentSessionId) {
return;
}
}
if ((result != null)) {
for (int i = 0; i < listeners.size(); i++) {
listeners.keyAt(i).onDiscoveryQuerySent(result.second, result.first);
}
}
if (shouldRemoveServiceAfterTtlExpires()) {
Iterator<MdnsResponse> iter = instanceNameToResponse.values().iterator();
while (iter.hasNext()) {
MdnsResponse existingResponse = iter.next();
if (existingResponse.hasServiceRecord()
&& existingResponse
.getServiceRecord()
.getRemainingTTL(clock.elapsedRealtime())
== 0) {
iter.remove();
for (int i = 0; i < listeners.size(); i++) {
if (!responseMatchesOptions(existingResponse,
listeners.valueAt(i))) {
continue;
}
final MdnsServiceBrowserListener listener = listeners.keyAt(i);
if (existingResponse.getServiceInstanceName() != null) {
final MdnsServiceInfo serviceInfo =
buildMdnsServiceInfoFromResponse(
existingResponse, serviceTypeLabels);
if (existingResponse.isComplete()) {
sharedLog.log("TTL expired. onServiceRemoved: "
+ serviceInfo);
listener.onServiceRemoved(serviceInfo);
}
sharedLog.log("TTL expired. onServiceNameRemoved: "
+ serviceInfo);
listener.onServiceNameRemoved(serviceInfo);
}
}
}
}
}
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);
scheduleNextRunLocked(nextRunConfig, minRemainingTtl, now, timeToRun,
lastScheduledTask.sessionId);
} }
dependencies.sendMessage(
handler, handler.obtainMessage(EVENT_QUERY_RESULT,
new QuerySentResult(result.first, result.second, taskArgs)));
} }
} }
private static long calculateTimeToRun(@NonNull QueryTask lastScheduledTask, private static long calculateTimeToRun(@NonNull ScheduledQueryTaskArgs taskArgs,
QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) { QueryTaskConfig queryTaskConfig, long now, long minRemainingTtl, long lastSentTime) {
final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs; final long baseDelayInMs = queryTaskConfig.delayUntilNextTaskWithoutBackoffMs;
if (!queryTaskConfig.shouldUseQueryBackoff()) { if (!queryTaskConfig.shouldUseQueryBackoff()) {
@@ -781,16 +802,15 @@ public class MdnsServiceTypeClient {
} }
// If the next TTL expiration time hasn't changed, then use previous calculated timeToRun. // If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
if (lastSentTime < now if (lastSentTime < now
&& lastScheduledTask.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) { && taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
// Use the original scheduling time if the TTL has not changed, to avoid continuously // Use the original scheduling time if the TTL has not changed, to avoid continuously
// rescheduling to 80% of the remaining TTL as time passes // rescheduling to 80% of the remaining TTL as time passes
return lastScheduledTask.timeToRun; return taskArgs.timeToRun;
} }
return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs); return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
} }
@GuardedBy("lock") private long getMinRemainingTtl(long now) {
private long getMinRemainingTtlLocked(long now) {
long minRemainingTtl = Long.MAX_VALUE; long minRemainingTtl = Long.MAX_VALUE;
for (MdnsResponse response : instanceNameToResponse.values()) { for (MdnsResponse response : instanceNameToResponse.values()) {
if (!response.isComplete()) { if (!response.isComplete()) {
@@ -811,19 +831,18 @@ public class MdnsServiceTypeClient {
@GuardedBy("lock") @GuardedBy("lock")
@NonNull @NonNull
private void scheduleNextRunLocked(@NonNull QueryTaskConfig nextRunConfig, private void scheduleNextRun(@NonNull QueryTaskConfig nextRunConfig,
long minRemainingTtl, long minRemainingTtl,
long timeWhenScheduled, long timeToRun, long sessionId) { long timeWhenScheduled, long timeToRun, long sessionId) {
lastScheduledTask = new QueryTask(nextRunConfig, timeToRun, lastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(nextRunConfig, timeToRun,
minRemainingTtl + timeWhenScheduled, sessionId); minRemainingTtl + timeWhenScheduled, sessionId);
// The timeWhenScheduled could be greater than the timeToRun if the Runnable is delayed. // The timeWhenScheduled could be greater than the timeToRun if the Runnable is delayed.
long timeToNextTasksWithBackoffInMs = Math.max(timeToRun - timeWhenScheduled, 0); long timeToNextTasksWithBackoffInMs = Math.max(timeToRun - timeWhenScheduled, 0);
sharedLog.log( sharedLog.log(String.format("Next run: sessionId: %d, in %d ms",
String.format("Next run: sessionId: %d, in %d ms", lastScheduledTask.sessionId, lastScheduledQueryTaskArgs.sessionId, timeToNextTasksWithBackoffInMs));
timeToNextTasksWithBackoffInMs));
dependencies.sendMessageDelayed( dependencies.sendMessageDelayed(
handler, handler,
handler.obtainMessage(EVENT_START_QUERYTASK, lastScheduledTask), handler.obtainMessage(EVENT_START_QUERYTASK, lastScheduledQueryTaskArgs),
timeToNextTasksWithBackoffInMs); timeToNextTasksWithBackoffInMs);
} }
} }

View File

@@ -204,6 +204,13 @@ public class MdnsServiceTypeClientTests {
return true; return true;
}).when(mockDeps).sendMessageDelayed(any(Handler.class), any(Message.class), anyLong()); }).when(mockDeps).sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());
doAnswer(inv -> {
final Handler handler = (Handler) inv.getArguments()[0];
final Message message = (Message) inv.getArguments()[1];
runOnHandler(() -> handler.dispatchMessage(message));
return true;
}).when(mockDeps).sendMessage(any(Handler.class), any(Message.class));
client = client =
new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor, new MdnsServiceTypeClient(SERVICE_TYPE, mockSocketClient, currentThreadExecutor,
mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps) { mockDecoderClock, socketKey, mockSharedLog, thread.getLooper(), mockDeps) {
@@ -925,6 +932,7 @@ public class MdnsServiceTypeClientTests {
// Simulate the case where the response is under TTL. // Simulate the case where the response is under TTL.
doReturn(TEST_ELAPSED_REALTIME + TEST_TTL - 1L).when(mockDecoderClock).elapsedRealtime(); doReturn(TEST_ELAPSED_REALTIME + TEST_TTL - 1L).when(mockDecoderClock).elapsedRealtime();
firstMdnsTask.run(); firstMdnsTask.run();
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
// Verify removed callback was not called. // Verify removed callback was not called.
verifyServiceRemovedNoCallback(mockListenerOne); verifyServiceRemovedNoCallback(mockListenerOne);
@@ -932,6 +940,7 @@ public class MdnsServiceTypeClientTests {
// Simulate the case where the response is after TTL. // Simulate the case where the response is after TTL.
doReturn(TEST_ELAPSED_REALTIME + TEST_TTL + 1L).when(mockDecoderClock).elapsedRealtime(); doReturn(TEST_ELAPSED_REALTIME + TEST_TTL + 1L).when(mockDecoderClock).elapsedRealtime();
firstMdnsTask.run(); firstMdnsTask.run();
verify(mockDeps, times(2)).sendMessage(any(), any(Message.class));
// Verify removed callback was called. // Verify removed callback was called.
verifyServiceRemovedCallback( verifyServiceRemovedCallback(
@@ -1118,6 +1127,7 @@ public class MdnsServiceTypeClientTests {
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse( inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
srvTxtQueryCaptor.capture(), srvTxtQueryCaptor.capture(),
eq(socketKey), eq(false)); eq(socketKey), eq(false));
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage); assertNotNull(delayMessage);
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse( final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
@@ -1210,6 +1220,7 @@ public class MdnsServiceTypeClientTests {
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse( inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingUnicastResponse(
srvTxtQueryCaptor.capture(), srvTxtQueryCaptor.capture(),
eq(socketKey), eq(false)); eq(socketKey), eq(false));
verify(mockDeps, times(1)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage); assertNotNull(delayMessage);
final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse( final MdnsPacket srvTxtQueryPacket = MdnsPacket.parse(
@@ -1249,6 +1260,7 @@ public class MdnsServiceTypeClientTests {
// Advance time so 75% of TTL passes and re-execute // Advance time so 75% of TTL passes and re-execute
doReturn(TEST_ELAPSED_REALTIME + (long) (TEST_TTL * 0.75)) doReturn(TEST_ELAPSED_REALTIME + (long) (TEST_TTL * 0.75))
.when(mockDecoderClock).elapsedRealtime(); .when(mockDecoderClock).elapsedRealtime();
verify(mockDeps, times(2)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage); assertNotNull(delayMessage);
dispatchMessage(); dispatchMessage();
currentThreadExecutor.getAndClearLastScheduledRunnable().run(); currentThreadExecutor.getAndClearLastScheduledRunnable().run();
@@ -1260,12 +1272,13 @@ public class MdnsServiceTypeClientTests {
inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse( inOrder.verify(mockSocketClient, times(2)).sendPacketRequestingMulticastResponse(
renewalQueryCaptor.capture(), renewalQueryCaptor.capture(),
eq(socketKey), eq(false)); eq(socketKey), eq(false));
verify(mockDeps, times(3)).sendMessage(any(), any(Message.class));
assertNotNull(delayMessage);
inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt()); inOrder.verify(mockListenerOne).onDiscoveryQuerySent(any(), anyInt());
final MdnsPacket renewalPacket = MdnsPacket.parse( final MdnsPacket renewalPacket = MdnsPacket.parse(
new MdnsPacketReader(renewalQueryCaptor.getValue())); new MdnsPacketReader(renewalQueryCaptor.getValue()));
assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_ANY, serviceName)); assertTrue(hasQuestion(renewalPacket, MdnsRecord.TYPE_ANY, serviceName));
inOrder.verifyNoMoreInteractions(); inOrder.verifyNoMoreInteractions();
assertNotNull(delayMessage);
long updatedReceiptTime = TEST_ELAPSED_REALTIME + TEST_TTL; long updatedReceiptTime = TEST_ELAPSED_REALTIME + TEST_TTL;
final MdnsPacket refreshedSrvTxtResponse = new MdnsPacket( final MdnsPacket refreshedSrvTxtResponse = new MdnsPacket(
@@ -1545,6 +1558,8 @@ public class MdnsServiceTypeClientTests {
expectedIPv6Packets[index], socketKey, false); expectedIPv6Packets[index], socketKey, false);
} }
} }
verify(mockDeps, times(index + 1))
.sendMessage(any(Handler.class), any(Message.class));
// Verify the task has been scheduled. // Verify the task has been scheduled.
verify(mockDeps, times(scheduledCount)) verify(mockDeps, times(scheduledCount))
.sendMessageDelayed(any(Handler.class), any(Message.class), anyLong()); .sendMessageDelayed(any(Handler.class), any(Message.class), anyLong());