• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity.mdns;
18 
19 import static com.android.server.connectivity.mdns.MdnsSearchOptions.AGGRESSIVE_QUERY_MODE;
20 import static com.android.server.connectivity.mdns.MdnsSearchOptions.PASSIVE_QUERY_MODE;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 
25 import com.android.internal.annotations.VisibleForTesting;
26 
27 /**
28  * The query scheduler class for calculating next query tasks parameters.
29  * <p>
30  * The class is not thread-safe and needs to be used on a consistent thread.
31  */
32 public class MdnsQueryScheduler {
33 
34     @VisibleForTesting
35     // RFC 6762 5.2: The interval between the first two queries MUST be at least one second.
36     static final int INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS = 1000;
37     private static final int INITIAL_TIME_BETWEEN_BURSTS_MS =
38             (int) MdnsConfigs.initialTimeBetweenBurstsMs();
39     private static final int MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS =
40             (int) MdnsConfigs.timeBetweenBurstsMs();
41     private static final int QUERIES_PER_BURST = (int) MdnsConfigs.queriesPerBurst();
42     private static final int TIME_BETWEEN_QUERIES_IN_BURST_MS =
43             (int) MdnsConfigs.timeBetweenQueriesInBurstMs();
44     private static final int QUERIES_PER_BURST_PASSIVE_MODE =
45             (int) MdnsConfigs.queriesPerBurstPassive();
46     @VisibleForTesting
47     // Basically this tries to send one query per typical DTIM interval 100ms, to maximize the
48     // chances that a query will be received if devices are using a DTIM multiplier (in which case
49     // they only listen once every [multiplier] DTIM intervals).
50     static final int TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS = 100;
51     static final int MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS = 60000;
52 
53     /**
54      * The argument for tracking the query tasks status.
55      */
56     public static class ScheduledQueryTaskArgs {
57         public final QueryTaskConfig config;
58         public final long timeToRun;
59         public final long minTtlExpirationTimeWhenScheduled;
60         public final long sessionId;
61 
ScheduledQueryTaskArgs(@onNull QueryTaskConfig config, long timeToRun, long minTtlExpirationTimeWhenScheduled, long sessionId)62         ScheduledQueryTaskArgs(@NonNull QueryTaskConfig config, long timeToRun,
63                 long minTtlExpirationTimeWhenScheduled, long sessionId) {
64             this.config = config;
65             this.timeToRun = timeToRun;
66             this.minTtlExpirationTimeWhenScheduled = minTtlExpirationTimeWhenScheduled;
67             this.sessionId = sessionId;
68         }
69     }
70 
71     @Nullable
72     private ScheduledQueryTaskArgs mLastScheduledQueryTaskArgs;
73 
MdnsQueryScheduler()74     public MdnsQueryScheduler() {
75     }
76 
77     /**
78      * Cancel the scheduled run. The method needed to be called when the scheduled task need to
79      * be canceled and rescheduling is not need.
80      */
cancelScheduledRun()81     public void cancelScheduledRun() {
82         mLastScheduledQueryTaskArgs = null;
83     }
84 
85     /**
86      * Calculates ScheduledQueryTaskArgs for rescheduling the current task. Returns null if the
87      * rescheduling is not necessary.
88      */
89     @Nullable
maybeRescheduleCurrentRun( long now, long minRemainingTtl, long lastSentTime, long sessionId, int numOfQueriesBeforeBackoff)90     public ScheduledQueryTaskArgs maybeRescheduleCurrentRun(
91             long now,
92             long minRemainingTtl,
93             long lastSentTime,
94             long sessionId,
95             int numOfQueriesBeforeBackoff) {
96         if (mLastScheduledQueryTaskArgs == null) {
97             return null;
98         }
99         final QueryTaskConfig lastConfig = mLastScheduledQueryTaskArgs.config;
100         if (!shouldUseQueryBackoff(lastConfig.queryIndex, lastConfig.queryMode,
101                 numOfQueriesBeforeBackoff)) {
102             return null;
103         }
104 
105         final long timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs,
106                 lastConfig.queryIndex, lastConfig.queryMode, now, minRemainingTtl, lastSentTime,
107                 numOfQueriesBeforeBackoff, false /* forceEnableBackoff */);
108 
109         if (timeToRun <= mLastScheduledQueryTaskArgs.timeToRun) {
110             return null;
111         }
112 
113         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(lastConfig,
114                 timeToRun,
115                 minRemainingTtl + now,
116                 sessionId);
117         return mLastScheduledQueryTaskArgs;
118     }
119 
120     /**
121      *  Calculates the ScheduledQueryTaskArgs for the next run.
122      */
123     @NonNull
scheduleNextRun( @onNull QueryTaskConfig currentConfig, long minRemainingTtl, long now, long lastSentTime, long sessionId, int queryMode, int numOfQueriesBeforeBackoff, boolean forceEnableBackoff)124     public ScheduledQueryTaskArgs scheduleNextRun(
125             @NonNull QueryTaskConfig currentConfig,
126             long minRemainingTtl,
127             long now,
128             long lastSentTime,
129             long sessionId,
130             int queryMode,
131             int numOfQueriesBeforeBackoff,
132             boolean forceEnableBackoff) {
133         final int newQueryIndex = currentConfig.getConfigForNextRun(queryMode).queryIndex;
134         long timeToRun;
135         if (mLastScheduledQueryTaskArgs == null && !forceEnableBackoff) {
136             timeToRun = now + getDelayBeforeTaskWithoutBackoff(
137                     newQueryIndex, queryMode);
138         } else {
139             timeToRun = calculateTimeToRun(mLastScheduledQueryTaskArgs, newQueryIndex,
140                     queryMode, now, minRemainingTtl, lastSentTime,
141                     numOfQueriesBeforeBackoff, forceEnableBackoff);
142         }
143         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(
144                 currentConfig.getConfigForNextRun(queryMode),
145                 timeToRun, minRemainingTtl + now,
146                 sessionId);
147         return mLastScheduledQueryTaskArgs;
148     }
149 
150     /**
151      *  Calculates the ScheduledQueryTaskArgs for the initial run.
152      */
scheduleFirstRun(@onNull QueryTaskConfig taskConfig, long now, long minRemainingTtl, long currentSessionId)153     public ScheduledQueryTaskArgs scheduleFirstRun(@NonNull QueryTaskConfig taskConfig,
154             long now, long minRemainingTtl, long currentSessionId) {
155         mLastScheduledQueryTaskArgs = new ScheduledQueryTaskArgs(taskConfig, now /* timeToRun */,
156                 now + minRemainingTtl/* minTtlExpirationTimeWhenScheduled */,
157                 currentSessionId);
158         return mLastScheduledQueryTaskArgs;
159     }
160 
calculateTimeToRun(@ullable ScheduledQueryTaskArgs taskArgs, int queryIndex, int queryMode, long now, long minRemainingTtl, long lastSentTime, int numOfQueriesBeforeBackoff, boolean forceEnableBackoff)161     private static long calculateTimeToRun(@Nullable ScheduledQueryTaskArgs taskArgs,
162             int queryIndex, int queryMode, long now, long minRemainingTtl, long lastSentTime,
163             int numOfQueriesBeforeBackoff, boolean forceEnableBackoff) {
164         final long baseDelayInMs = getDelayBeforeTaskWithoutBackoff(queryIndex, queryMode);
165         if (!(forceEnableBackoff
166                 || shouldUseQueryBackoff(queryIndex, queryMode, numOfQueriesBeforeBackoff))) {
167             return lastSentTime + baseDelayInMs;
168         }
169         if (minRemainingTtl <= 0) {
170             // There's no service, or there is an expired service. In any case, schedule for the
171             // minimum time, which is the base delay.
172             return lastSentTime + baseDelayInMs;
173         }
174         // If the next TTL expiration time hasn't changed, then use previous calculated timeToRun.
175         if (lastSentTime < now && taskArgs != null
176                 && taskArgs.minTtlExpirationTimeWhenScheduled == now + minRemainingTtl) {
177             // Use the original scheduling time if the TTL has not changed, to avoid continuously
178             // rescheduling to 80% of the remaining TTL as time passes
179             return taskArgs.timeToRun;
180         }
181         return Math.max(now + (long) (0.8 * minRemainingTtl), lastSentTime + baseDelayInMs);
182     }
183 
getBurstIndex(int queryIndex, int queryMode)184     private static int getBurstIndex(int queryIndex, int queryMode) {
185         if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
186             // In passive mode, after the first burst of QUERIES_PER_BURST queries, subsequent
187             // bursts have QUERIES_PER_BURST_PASSIVE_MODE queries.
188             final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
189             return 1 + (queryIndexAfterFirstBurst / QUERIES_PER_BURST_PASSIVE_MODE);
190         } else {
191             return queryIndex / QUERIES_PER_BURST;
192         }
193     }
194 
getQueryIndexInBurst(int queryIndex, int queryMode)195     private static int getQueryIndexInBurst(int queryIndex, int queryMode) {
196         if (queryMode == PASSIVE_QUERY_MODE && queryIndex >= QUERIES_PER_BURST) {
197             final int queryIndexAfterFirstBurst = queryIndex - QUERIES_PER_BURST;
198             return queryIndexAfterFirstBurst % QUERIES_PER_BURST_PASSIVE_MODE;
199         } else {
200             return queryIndex % QUERIES_PER_BURST;
201         }
202     }
203 
isFirstBurst(int queryIndex, int queryMode)204     private static boolean isFirstBurst(int queryIndex, int queryMode) {
205         return getBurstIndex(queryIndex, queryMode) == 0;
206     }
207 
isFirstQueryInBurst(int queryIndex, int queryMode)208     static boolean isFirstQueryInBurst(int queryIndex, int queryMode) {
209         return getQueryIndexInBurst(queryIndex, queryMode) == 0;
210     }
211 
getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode)212     private static long getDelayBeforeTaskWithoutBackoff(int queryIndex, int queryMode) {
213         final int burstIndex = getBurstIndex(queryIndex, queryMode);
214         final int queryIndexInBurst = getQueryIndexInBurst(queryIndex, queryMode);
215         if (queryIndexInBurst == 0) {
216             return getTimeToBurstMs(burstIndex, queryMode);
217         } else if (queryIndexInBurst == 1 && queryMode == AGGRESSIVE_QUERY_MODE) {
218             // In aggressive mode, the first 2 queries are sent without delay.
219             return 0;
220         }
221         return queryMode == AGGRESSIVE_QUERY_MODE
222                 ? TIME_BETWEEN_RETRANSMISSION_QUERIES_IN_BURST_MS
223                 : TIME_BETWEEN_QUERIES_IN_BURST_MS;
224     }
225 
226     /**
227      * Shifts a value left by the specified number of bits, coercing to at most maxValue.
228      *
229      * <p>This allows calculating min(value*2^shift, maxValue) without overflow.
230      */
boundedLeftShift(int value, int shift, int maxValue)231     private static int boundedLeftShift(int value, int shift, int maxValue) {
232         // There must be at least one leading zero for positive values, so the maximum left shift
233         // without overflow is the number of leading zeros minus one.
234         final int maxShift = Integer.numberOfLeadingZeros(value) - 1;
235         if (shift > maxShift) {
236             // The shift would overflow positive integers, so is greater than maxValue.
237             return maxValue;
238         }
239         return Math.min(value << shift, maxValue);
240     }
241 
getTimeToBurstMs(int burstIndex, int queryMode)242     private static int getTimeToBurstMs(int burstIndex, int queryMode) {
243         if (burstIndex == 0) {
244             // No delay before the first burst
245             return 0;
246         }
247         switch (queryMode) {
248             case PASSIVE_QUERY_MODE:
249                 return MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS;
250             case AGGRESSIVE_QUERY_MODE:
251                 return boundedLeftShift(INITIAL_AGGRESSIVE_TIME_BETWEEN_BURSTS_MS,
252                         burstIndex - 1,
253                         MAX_TIME_BETWEEN_AGGRESSIVE_BURSTS_MS);
254             default: // ACTIVE_QUERY_MODE
255                 return boundedLeftShift(INITIAL_TIME_BETWEEN_BURSTS_MS,
256                         burstIndex - 1,
257                         MAX_TIME_BETWEEN_ACTIVE_PASSIVE_BURSTS_MS);
258         }
259     }
260 
261     /**
262      * Determine if the query backoff should be used.
263      */
shouldUseQueryBackoff(int queryIndex, int queryMode, int numOfQueriesBeforeBackoff)264     public static boolean shouldUseQueryBackoff(int queryIndex, int queryMode,
265             int numOfQueriesBeforeBackoff) {
266         // Don't enable backoff mode during the burst or in the first burst
267         if (!isFirstQueryInBurst(queryIndex, queryMode) || isFirstBurst(queryIndex, queryMode)) {
268             return false;
269         }
270         return queryIndex > numOfQueriesBeforeBackoff;
271     }
272 }
273