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