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 package com.android.car.internal;
17 
18 import android.annotation.Nullable;
19 import android.car.builtin.os.HandlerHelper;
20 import android.car.builtin.util.Slogf;
21 import android.os.Handler;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.util.LongSparseArray;
25 
26 import com.android.internal.annotations.GuardedBy;
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.util.ArrayList;
30 import java.util.List;
31 
32 /**
33  * A pending request pool with timeout. The request uses {@code long} request ID.
34  *
35  * Note that this pool is not thread-safe. Caller must use lock to guard this object.
36  *
37  * @param <T> The request info type.
38  *
39  * @hide
40  */
41 public final class LongPendingRequestPool<T extends LongRequestIdWithTimeout> {
42     private static final String TAG = LongPendingRequestPool.class.getSimpleName();
43     private static final int REQUESTS_TIMEOUT_MESSAGE_TYPE = 1;
44 
45     private final TimeoutCallback mTimeoutCallback;
46     private final Handler mTimeoutHandler;
47     private final Object mRequestIdsLock = new Object();
48     // A map to store system uptime in ms to the request IDs that will timeout at this time.
49     @GuardedBy("mRequestIdsLock")
50     private final LongSparseArray<List<Long>> mTimeoutUptimeMsToRequestIds =
51             new LongSparseArray<>();
52     private final LongSparseArray<T> mRequestIdToRequestInfo = new LongSparseArray<>();
53 
54     private class MessageHandler implements Handler.Callback {
55         @Override
handleMessage(Message msg)56         public boolean handleMessage(Message msg) {
57             if (msg.what != REQUESTS_TIMEOUT_MESSAGE_TYPE) {
58                 Slogf.e(TAG, "Received unexpected msg with what: %d", msg.what);
59                 return true;
60             }
61             List<Long> requestIdsCopy;
62             synchronized (mRequestIdsLock) {
63                 Long timeoutUptimeMs = (Long) msg.obj;
64                 List<Long> requestIds = mTimeoutUptimeMsToRequestIds.get(timeoutUptimeMs);
65                 if (requestIds == null) {
66                     Slogf.d(TAG, "handle a timeout msg, but all requests that should timeout "
67                             + "has been removed");
68                     return true;
69                 }
70                 requestIdsCopy = new ArrayList<>(requestIds);
71                 mTimeoutUptimeMsToRequestIds.remove(timeoutUptimeMs);
72             }
73             mTimeoutCallback.onRequestsTimeout(requestIdsCopy);
74             return true;
75         }
76     }
77 
78     /**
79      * Interface for timeout callback.
80      */
81     public interface TimeoutCallback {
82         /**
83          * Callback called when requests timeout.
84          *
85          * Note that caller must obtain lock to the request pool, call
86          * {@link PendingRequestPool#getRequestIfFound} to check the request has not been removed,
87          * and call {@link PendingRequestPool#removeRequest}, then release the lock.
88          *
89          * Similarly when a request is finished normally, caller should follow the same process.
90          */
onRequestsTimeout(List<Long> requestIds)91         void onRequestsTimeout(List<Long> requestIds);
92     }
93 
LongPendingRequestPool(Looper looper, TimeoutCallback callback)94     public LongPendingRequestPool(Looper looper, TimeoutCallback callback) {
95         mTimeoutCallback = callback;
96         mTimeoutHandler = new Handler(looper, new MessageHandler());
97     }
98 
99     /**
100      * Get the number of pending requests.
101      */
size()102     public int size() {
103         return mRequestIdToRequestInfo.size();
104     }
105 
106     /**
107      * Get the pending request ID at the specific index.
108      */
keyAt(int index)109     public long keyAt(int index) {
110         return mRequestIdToRequestInfo.keyAt(index);
111     }
112 
113     /**
114      * Get the pending request info at the specific index.
115      */
valueAt(int index)116     public T valueAt(int index) {
117         return mRequestIdToRequestInfo.valueAt(index);
118     }
119 
120     /**
121      * Add a list of new pending requests to the pool.
122      *
123      * If the request timeout before being removed from the pool, the timeout callback will be
124      * called. The caller must remove the request from the pool during the callback.
125      */
addPendingRequests(List<T> requestsInfo)126     public void addPendingRequests(List<T> requestsInfo) {
127         LongSparseArray<Message> messagesToSend = new LongSparseArray<>();
128         synchronized (mRequestIdsLock) {
129             for (T requestInfo : requestsInfo) {
130                 long requestId = requestInfo.getRequestId();
131                 long timeoutUptimeMs = requestInfo.getTimeoutUptimeMs();
132                 mRequestIdToRequestInfo.put(requestId, requestInfo);
133                 if (mTimeoutUptimeMsToRequestIds.get(timeoutUptimeMs) == null) {
134                     mTimeoutUptimeMsToRequestIds.put(timeoutUptimeMs, new ArrayList<>());
135                 }
136                 List<Long> requestIds = mTimeoutUptimeMsToRequestIds.get(timeoutUptimeMs);
137                 requestIds.add(requestId);
138 
139                 if (requestIds.size() == 1) {
140                     // This is the first time we register a timeout handler for the specific
141                     // {@code timeoutUptimeMs}.
142                     Message message = new Message();
143                     message.what = REQUESTS_TIMEOUT_MESSAGE_TYPE;
144                     message.obj = Long.valueOf(timeoutUptimeMs);
145                     messagesToSend.put(timeoutUptimeMs, message);
146                 }
147             }
148         }
149         for (int i = 0; i < messagesToSend.size(); i++) {
150             mTimeoutHandler.sendMessageAtTime(messagesToSend.valueAt(i), messagesToSend.keyAt(i));
151         }
152     }
153 
154     /**
155      * Get the pending request info for the specific request ID if found.
156      */
getRequestIfFound(long requestId)157     public @Nullable T getRequestIfFound(long requestId) {
158         return mRequestIdToRequestInfo.get(requestId);
159     }
160 
161     /**
162      * Remove the pending request and the timeout handler for it.
163      */
removeRequest(long requestId)164     public void removeRequest(long requestId) {
165         synchronized (mRequestIdsLock) {
166             T requestInfo = mRequestIdToRequestInfo.get(requestId);
167             if (requestInfo == null) {
168                 Slogf.w(TAG, "No pending requests for request ID: %d", requestId);
169                 return;
170             }
171             mRequestIdToRequestInfo.remove(requestId);
172             long timeoutUptimeMs = requestInfo.getTimeoutUptimeMs();
173             List<Long> requestIds = mTimeoutUptimeMsToRequestIds.get(timeoutUptimeMs);
174             if (requestIds == null) {
175                 Slogf.w(TAG, "No pending request that will timeout at: %d ms", timeoutUptimeMs);
176                 return;
177             }
178             if (!requestIds.remove(Long.valueOf(requestId))) {
179                 Slogf.w(TAG, "No pending request for request ID: %d, timeout at: %d ms",
180                         requestId, timeoutUptimeMs);
181                 return;
182             }
183             if (requestIds.isEmpty()) {
184                 mTimeoutUptimeMsToRequestIds.remove(timeoutUptimeMs);
185                 HandlerHelper.removeEqualMessages(mTimeoutHandler, REQUESTS_TIMEOUT_MESSAGE_TYPE,
186                         Long.valueOf(timeoutUptimeMs));
187             }
188         }
189     }
190 
191     /**
192      * Returns whether the pool is empty and all stored data are cleared.
193      */
194     @VisibleForTesting
isEmpty()195     public boolean isEmpty() {
196         synchronized (mRequestIdsLock) {
197             return mTimeoutUptimeMsToRequestIds.size() == 0 && mRequestIdToRequestInfo.size() == 0;
198         }
199     }
200 }
201