Recycle userIds after hitting the limit
Previously the system had to be rebooted to allow reusing IDs of removed
users.
Now removed userIds will be recycled after hitting MAX_USER_ID limit.
In this stage all mRemovingUserIds are released, with the exception of ids
stored in an LRU queue - mRecentlyRemovedIds
Added debug flag RELEASE_DELETED_USER_ID to allow reusing of userIds
immediately after the user is deleted.
Bug: 28822373
Change-Id: Ibde562c69efc1533dbca2f1f8d919bee7473644f
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 5176c06..c1cb032 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -113,6 +113,7 @@
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.LinkedList;
import java.util.List;
/**
@@ -130,6 +131,8 @@
private static final String LOG_TAG = "UserManagerService";
static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
private static final boolean DBG_WITH_STACKTRACE = false; // DO NOT SUBMIT WITH TRUE
+ // Can be used for manual testing of id recycling
+ private static final boolean RELEASE_DELETED_USER_ID = false; // DO NOT SUBMIT WITH TRUE
private static final String TAG_NAME = "name";
private static final String TAG_ACCOUNT = "account";
@@ -183,9 +186,15 @@
| UserInfo.FLAG_GUEST
| UserInfo.FLAG_DEMO;
- private static final int MIN_USER_ID = 10;
+ @VisibleForTesting
+ static final int MIN_USER_ID = 10;
// We need to keep process uid within Integer.MAX_VALUE.
- private static final int MAX_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;
+ @VisibleForTesting
+ static final int MAX_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;
+
+ // Max size of the queue of recently removed users
+ @VisibleForTesting
+ static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100;
private static final int USER_VERSION = 6;
@@ -312,10 +321,17 @@
/**
* Set of user IDs being actively removed. Removed IDs linger in this set
* for several seconds to work around a VFS caching issue.
+ * Use {@link #addRemovingUserIdLocked(int)} to add elements to this array
*/
@GuardedBy("mUsersLock")
private final SparseBooleanArray mRemovingUserIds = new SparseBooleanArray();
+ /**
+ * Queue of recently removed userIds. Used for recycling of userIds
+ */
+ @GuardedBy("mUsersLock")
+ private final LinkedList<Integer> mRecentlyRemovedIds = new LinkedList<>();
+
@GuardedBy("mUsersLock")
private int[] mUserIds;
@GuardedBy("mPackagesLock")
@@ -401,9 +417,10 @@
}
}
+ // TODO b/28848102 Add support for test dependencies injection
@VisibleForTesting
- UserManagerService(File dataDir) {
- this(null, null, new Object(), dataDir);
+ UserManagerService(Context context) {
+ this(context, null, new Object(), context.getCacheDir());
}
/**
@@ -472,7 +489,7 @@
UserInfo ui = mUsers.valueAt(i).info;
if ((ui.partial || ui.guestToRemove || ui.isEphemeral()) && i != 0) {
partials.add(ui);
- mRemovingUserIds.append(ui.id, true);
+ addRemovingUserIdLocked(ui.id);
ui.partial = true;
}
}
@@ -1791,11 +1808,7 @@
}
// Create the system user
UserInfo system = new UserInfo(UserHandle.USER_SYSTEM, null, null, flags);
- UserData userData = new UserData();
- userData.info = system;
- synchronized (mUsersLock) {
- mUsers.put(system.id, userData);
- }
+ UserData userData = putUserInfo(system);
mNextSerialNumber = MIN_USER_ID;
mUserVersion = USER_VERSION;
@@ -2335,6 +2348,23 @@
return userInfo;
}
+ @VisibleForTesting
+ UserData putUserInfo(UserInfo userInfo) {
+ final UserData userData = new UserData();
+ userData.info = userInfo;
+ synchronized (mUsers) {
+ mUsers.put(userInfo.id, userData);
+ }
+ return userData;
+ }
+
+ @VisibleForTesting
+ void removeUserInfo(int userId) {
+ synchronized (mUsers) {
+ mUsers.remove(userId);
+ }
+ }
+
/**
* @hide
*/
@@ -2451,10 +2481,7 @@
return false;
}
- // We remember deleted user IDs to prevent them from being
- // reused during the current boot; they can still be reused
- // after a reboot.
- mRemovingUserIds.put(userHandle, true);
+ addRemovingUserIdLocked(userHandle);
}
try {
@@ -2501,6 +2528,19 @@
}
}
+ @VisibleForTesting
+ void addRemovingUserIdLocked(int userId) {
+ // We remember deleted user IDs to prevent them from being
+ // reused during the current boot; they can still be reused
+ // after a reboot or recycling of userIds.
+ mRemovingUserIds.put(userId, true);
+ mRecentlyRemovedIds.add(userId);
+ // Keep LRU queue of recently removed IDs for recycling
+ if (mRecentlyRemovedIds.size() > MAX_RECENTLY_REMOVED_IDS_SIZE) {
+ mRecentlyRemovedIds.removeFirst();
+ }
+ }
+
void finishRemoveUser(final int userHandle) {
if (DBG) Slog.i(LOG_TAG, "finishRemoveUser " + userHandle);
// Let other services shutdown any activity and clean up their state before completely
@@ -2586,6 +2626,11 @@
AtomicFile userFile = new AtomicFile(new File(mUsersDir, userHandle + XML_SUFFIX));
userFile.delete();
updateUserIds();
+ if (RELEASE_DELETED_USER_ID) {
+ synchronized (mUsers) {
+ mRemovingUserIds.delete(userHandle);
+ }
+ }
}
private void sendProfileRemovedBroadcast(int parentUserId, int removedUserId) {
@@ -2966,20 +3011,39 @@
/**
* Returns the next available user id, filling in any holes in the ids.
- * TODO: May not be a good idea to recycle ids, in case it results in confusion
- * for data and battery stats collection, or unexpected cross-talk.
*/
- private int getNextAvailableId() {
+ @VisibleForTesting
+ int getNextAvailableId() {
+ int nextId;
synchronized (mUsersLock) {
- int i = MIN_USER_ID;
- while (i < MAX_USER_ID) {
- if (mUsers.indexOfKey(i) < 0 && !mRemovingUserIds.get(i)) {
- return i;
+ nextId = scanNextAvailableIdLocked();
+ if (nextId >= 0) {
+ return nextId;
+ }
+ // All ids up to MAX_USER_ID were used. Remove all mRemovingUserIds,
+ // except most recently removed
+ if (mRemovingUserIds.size() > 0) {
+ Slog.i(LOG_TAG, "All available IDs are used. Recycling LRU ids.");
+ mRemovingUserIds.clear();
+ for (Integer recentlyRemovedId : mRecentlyRemovedIds) {
+ mRemovingUserIds.put(recentlyRemovedId, true);
}
- i++;
+ nextId = scanNextAvailableIdLocked();
}
}
- throw new IllegalStateException("No user id available!");
+ if (nextId < 0) {
+ throw new IllegalStateException("No user id available!");
+ }
+ return nextId;
+ }
+
+ private int scanNextAvailableIdLocked() {
+ for (int i = MIN_USER_ID; i < MAX_USER_ID; i++) {
+ if (mUsers.indexOfKey(i) < 0 && !mRemovingUserIds.get(i)) {
+ return i;
+ }
+ }
+ return -1;
}
private String packageToRestrictionsFileName(String packageName) {
@@ -3284,6 +3348,10 @@
synchronized (mUsersLock) {
pw.println();
pw.println(" Device managed: " + mIsDeviceManaged);
+ if (mRemovingUserIds.size() > 0) {
+ pw.println();
+ pw.println(" Recently removed userIds: " + mRecentlyRemovedIds);
+ }
}
synchronized (mUserStates) {
pw.println(" Started users state: " + mUserStates);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java
new file mode 100644
index 0000000..35967fb
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceIdRecyclingTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.pm;
+
+import android.content.pm.UserInfo;
+import android.os.Looper;
+import android.os.UserManagerInternal;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.filters.MediumTest;
+
+import com.android.server.LocalServices;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.LinkedHashSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * <p>Run with:<pre>
+ * m FrameworksServicesTests &&
+ * adb install \
+ * -r out/target/product/hammerhead/data/app/FrameworksServicesTests/FrameworksServicesTests.apk &&
+ * adb shell am instrument -e class com.android.server.pm.UserManagerServiceIdRecyclingTest \
+ * -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
+ * </pre>
+ */
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class UserManagerServiceIdRecyclingTest {
+ private UserManagerService mUserManagerService;
+
+ @Before
+ public void setup() {
+ // Currently UserManagerService cannot be instantiated twice inside a VM without a cleanup
+ // TODO: Remove once UMS supports proper dependency injection
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+ LocalServices.removeServiceForTest(UserManagerInternal.class);
+ mUserManagerService = new UserManagerService(InstrumentationRegistry.getContext());
+ }
+
+ @Test
+ public void testUserCreateRecycleIdsAddAllThenRemove() {
+ // Add max possible users
+ for (int i = UserManagerService.MIN_USER_ID; i < UserManagerService.MAX_USER_ID; i++) {
+ int userId = mUserManagerService.getNextAvailableId();
+ assertEquals(i, userId);
+ mUserManagerService.putUserInfo(newUserInfo(userId));
+ }
+
+ assertNoNextIdAvailable("All ids should be assigned");
+ // Now remove RECENTLY_REMOVED_IDS_MAX_SIZE users in the middle
+ int startFrom = UserManagerService.MIN_USER_ID + 10000 /* arbitrary number */;
+ int lastId = startFrom + UserManagerService.MAX_RECENTLY_REMOVED_IDS_SIZE;
+ for (int i = startFrom; i < lastId; i++) {
+ removeUser(i);
+ assertNoNextIdAvailable("There is not enough recently removed users. "
+ + "Next id should not be available. Failed at u" + i);
+ }
+
+ // Now remove first user
+ removeUser(UserManagerService.MIN_USER_ID);
+
+ // Released UserIDs should be returned in the FIFO order
+ int nextId = mUserManagerService.getNextAvailableId();
+ assertEquals(startFrom, nextId);
+ }
+
+ @Test
+ public void testUserCreateRecycleIdsOverflow() {
+ LinkedHashSet<Integer> queue = new LinkedHashSet<>();
+ // Make sure we can generate more than 2x ids without issues
+ for (int i = 0; i < UserManagerService.MAX_USER_ID * 2; i++) {
+ int userId = mUserManagerService.getNextAvailableId();
+ assertTrue("Returned id should not be recent. Id=" + userId + ". Recents=" + queue,
+ queue.add(userId));
+ if (queue.size() > UserManagerService.MAX_RECENTLY_REMOVED_IDS_SIZE) {
+ queue.remove(queue.iterator().next());
+ }
+ mUserManagerService.putUserInfo(newUserInfo(userId));
+ removeUser(userId);
+ }
+ }
+
+ private void removeUser(int userId) {
+ mUserManagerService.removeUserInfo(userId);
+ mUserManagerService.addRemovingUserIdLocked(userId);
+ }
+
+ private void assertNoNextIdAvailable(String message) {
+ try {
+ mUserManagerService.getNextAvailableId();
+ fail(message);
+ } catch (IllegalStateException e) {
+ //OK
+ }
+ }
+
+ private static UserInfo newUserInfo(int userId) {
+ return new UserInfo(userId, "User " + userId, 0);
+ }
+}
+