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.healthconnect;
18 
19 import android.annotation.NonNull;
20 import android.util.Log;
21 
22 import com.android.internal.annotations.GuardedBy;
23 
24 import java.util.Map;
25 import java.util.Queue;
26 import java.util.concurrent.ConcurrentSkipListMap;
27 import java.util.concurrent.LinkedBlockingQueue;
28 
29 /**
30  * A scheduler class to run the tasks in a Round Robin fashion based on client package names.
31  *
32  * @hide
33  */
34 public final class HealthConnectRoundRobinScheduler {
35     private static final String TAG = "HealthConnectScheduler";
36     private final ConcurrentSkipListMap<Integer, Queue<Runnable>> mTasks =
37             new ConcurrentSkipListMap<>();
38     private final Object mLock = new Object();
39 
40     @GuardedBy("mLock")
41     private boolean mPauseScheduler;
42 
43     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
44     @GuardedBy("mLock")
45     private Integer mLastKeyUsed;
46 
resume()47     void resume() {
48         synchronized (mLock) {
49             mPauseScheduler = false;
50         }
51     }
52 
53     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
addTask(int uid, Runnable task)54     void addTask(int uid, Runnable task) {
55         synchronized (mLock) {
56             // If the scheduler is currently paused (this can happen if the platform is doing a user
57             // switch), ignore this request. This most likely means that we won't be able to deliver
58             // the result back anyway.
59             if (mPauseScheduler) {
60                 Log.e(TAG, "Unable to schedule task for uid: " + uid);
61                 return;
62             }
63 
64             mTasks.putIfAbsent(uid, new LinkedBlockingQueue<>());
65             mTasks.get(uid).add(task);
66         }
67     }
68 
69     @NonNull
getNextTask()70     Runnable getNextTask() {
71         synchronized (mLock) {
72             if (mLastKeyUsed == null) {
73                 mLastKeyUsed = mTasks.firstKey();
74             }
75 
76             Map.Entry<Integer, Queue<Runnable>> entry = mTasks.higherEntry(mLastKeyUsed);
77             while (entry != null && entry.getValue().isEmpty()) {
78                 mTasks.remove(entry.getKey());
79                 entry = mTasks.higherEntry(entry.getKey());
80             }
81 
82             if (entry == null) {
83                 // Reached the end, no tasks found. Reset to first entry and try again.
84                 entry = mTasks.firstEntry();
85                 while (entry != null && entry.getValue().isEmpty()) {
86                     mTasks.remove(entry.getKey());
87                     entry = mTasks.higherEntry(entry.getKey());
88                 }
89             }
90 
91             if (entry != null) {
92                 mLastKeyUsed = entry.getKey();
93                 return entry.getValue().poll();
94             }
95 
96             throw new InternalError("Task scheduled but none found");
97         }
98     }
99 
killTasksAndPauseScheduler()100     void killTasksAndPauseScheduler() {
101         synchronized (mLock) {
102             mPauseScheduler = true;
103             mTasks.clear();
104         }
105     }
106 }
107