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.appsearch; 18 19 import android.annotation.NonNull; 20 import android.text.TextUtils; 21 import android.util.ArrayMap; 22 import android.util.Log; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.server.appsearch.external.localstorage.stats.CallStats; 26 27 import java.util.Map; 28 import java.util.Objects; 29 30 /** 31 * Class containing configs for AppSearch task queue's rate limit. 32 * 33 * <p>Task queue total capacity is the total cost of tasks that AppSearch can accept onto its task 34 * queue from all packages. This is configured with an integer value. 35 * 36 * <p>Task queue per-package capacity is the total cost of tasks that AppSearch can accept onto its 37 * task queue from a single calling package. This config is passed in as a percentage of the total 38 * capacity. 39 * 40 * <p>Each AppSearch API call has an associated integer cost that is configured by the API costs 41 * string. API costs must be positive. The API costs string uses API_ENTRY_DELIMITER (';') to 42 * separate API entries and has a string API name followed by API_COST_DELIMITER (':') and the 43 * integer cost to define each entry. If an API's cost is not specified in the string, its cost is 44 * set to DEFAULT_API_COST. e.g. A valid API cost string: "putDocument:5;query:1;setSchema:10". 45 * 46 * <p>If an API call has a higher cost, this means that the API consumes more of the task queue 47 * budget and fewer number of tasks can be placed on the task queue. An incoming API call from a 48 * calling package is dropped when the rate limit is exceeded, which happens when either: 1. Total 49 * cost of all API calls currently on the task queue + cost of incoming API call > task queue total 50 * capacity. OR 2. Total cost of all API calls currently on the task queue from the calling package 51 * + cost of incoming API call > task queue per-package capacity. 52 */ 53 public final class AppSearchRateLimitConfig { 54 @VisibleForTesting public static final int DEFAULT_API_COST = 1; 55 56 /** 57 * Creates an instance of {@link AppSearchRateLimitConfig}. 58 * 59 * @param totalCapacity configures total cost of tasks that AppSearch can accept onto its task 60 * queue from all packages. 61 * @param perPackageCapacityPercentage configures total cost of tasks that AppSearch can accept 62 * onto its task queue from a single calling package, as a percentage of totalCapacity. 63 * @param apiCostsString configures costs for each {@link CallStats.CallType}. The string should 64 * use API_ENTRY_DELIMITER (';') to separate entries, with each entry defined by the string 65 * API name followed by API_COST_DELIMITER (':'). e.g. "putDocument:5;query:1;setSchema:10" 66 */ create( int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString)67 public static AppSearchRateLimitConfig create( 68 int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString) { 69 Objects.requireNonNull(apiCostsString); 70 Map<Integer, Integer> apiCostsMap = createApiCostsMap(apiCostsString); 71 return new AppSearchRateLimitConfig( 72 totalCapacity, perPackageCapacityPercentage, apiCostsString, apiCostsMap); 73 } 74 75 // Truncated as logging tag is allowed to be at most 23 characters. 76 private static final String TAG = "AppSearchRateLimitConfi"; 77 78 private static final String API_ENTRY_DELIMITER = ";"; 79 private static final String API_COST_DELIMITER = ":"; 80 81 private final int mTaskQueueTotalCapacity; 82 private final int mTaskQueuePerPackageCapacity; 83 private final String mApiCostsString; 84 // Mapping of @CallStats.CallType -> cost 85 private final Map<Integer, Integer> mTaskQueueApiCosts; 86 AppSearchRateLimitConfig( int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString, @NonNull Map<Integer, Integer> apiCostsMap)87 private AppSearchRateLimitConfig( 88 int totalCapacity, 89 float perPackageCapacityPercentage, 90 @NonNull String apiCostsString, 91 @NonNull Map<Integer, Integer> apiCostsMap) { 92 mTaskQueueTotalCapacity = totalCapacity; 93 mTaskQueuePerPackageCapacity = (int) (totalCapacity * perPackageCapacityPercentage); 94 mApiCostsString = Objects.requireNonNull(apiCostsString); 95 mTaskQueueApiCosts = Objects.requireNonNull(apiCostsMap); 96 } 97 98 /** 99 * Returns an AppSearchRateLimitConfig instance given the input capacities and ApiCosts. This 100 * may be the same instance if there are no changes in these configs. 101 * 102 * @param totalCapacity configures total cost of tasks that AppSearch can accept onto its task 103 * queue from all packages. 104 * @param perPackageCapacityPercentage configures total cost of tasks that AppSearch can accept 105 * onto its task queue from a single calling package, as a percentage of totalCapacity. 106 * @param apiCostsString configures costs for each {@link CallStats.CallType}. The string should 107 * use API_ENTRY_DELIMITER (';') to separate entries, with each entry defined by the string 108 * API name followed by API_COST_DELIMITER (':'). e.g. "putDocument:5;query:1;setSchema:10" 109 */ rebuildIfNecessary( int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString)110 public AppSearchRateLimitConfig rebuildIfNecessary( 111 int totalCapacity, float perPackageCapacityPercentage, @NonNull String apiCostsString) { 112 int perPackageCapacity = (int) (totalCapacity * perPackageCapacityPercentage); 113 if (totalCapacity != mTaskQueueTotalCapacity 114 || perPackageCapacity != mTaskQueuePerPackageCapacity 115 || !Objects.equals(apiCostsString, mApiCostsString)) { 116 return AppSearchRateLimitConfig.create( 117 totalCapacity, perPackageCapacityPercentage, apiCostsString); 118 } 119 return this; 120 } 121 122 /** Returns the task queue total capacity. */ getTaskQueueTotalCapacity()123 public int getTaskQueueTotalCapacity() { 124 return mTaskQueueTotalCapacity; 125 } 126 127 /** Returns the per-package task queue capacity. */ getTaskQueuePerPackageCapacity()128 public int getTaskQueuePerPackageCapacity() { 129 return mTaskQueuePerPackageCapacity; 130 } 131 132 /** 133 * Returns the cost of an API type. 134 * 135 * <p>The range of the cost should be [0, taskQueueTotalCapacity]. Default API cost of 1 will be 136 * returned if the cost has not been configured for an API call. 137 */ getApiCost(@allStats.CallType int apiType)138 public int getApiCost(@CallStats.CallType int apiType) { 139 return mTaskQueueApiCosts.getOrDefault(apiType, DEFAULT_API_COST); 140 } 141 142 /** Returns an API costs map based on apiCostsString. */ createApiCostsMap(@onNull String apiCostsString)143 private static Map<Integer, Integer> createApiCostsMap(@NonNull String apiCostsString) { 144 if (TextUtils.getTrimmedLength(apiCostsString) == 0) { 145 return new ArrayMap<>(); 146 } 147 String[] entries = apiCostsString.split(API_ENTRY_DELIMITER); 148 Map<Integer, Integer> apiCostsMap = new ArrayMap<>(entries.length); 149 for (int i = 0; i < entries.length; ++i) { 150 String entry = entries[i]; 151 int costDelimiterIndex = entry.indexOf(API_COST_DELIMITER); 152 if (costDelimiterIndex < 0 || costDelimiterIndex >= entry.length() - 1) { 153 Log.e(TAG, "No cost specified in entry: " + entry); 154 continue; 155 } 156 String apiName = entry.substring(0, costDelimiterIndex); 157 int apiCost; 158 try { 159 apiCost = 160 Integer.parseInt( 161 entry, costDelimiterIndex + 1, entry.length(), /* radix= */ 10); 162 } catch (NumberFormatException e) { 163 Log.e(TAG, "Invalid cost for API cost entry: " + entry); 164 continue; 165 } 166 if (apiCost < 0) { 167 Log.e(TAG, "API cost must be positive. Invalid entry: " + entry); 168 continue; 169 } 170 @CallStats.CallType int apiType = CallStats.getApiCallTypeFromName(apiName); 171 if (apiType == CallStats.CALL_TYPE_UNKNOWN) { 172 Log.e(TAG, "Invalid API name for entry: " + entry); 173 continue; 174 } 175 apiCostsMap.put(apiType, apiCost); 176 } 177 return apiCostsMap; 178 } 179 } 180