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