• 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.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