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