Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 28321bb7 authored by Xin Guan's avatar Xin Guan
Browse files

Revert^2 Fix job count out of quota.

Bug: 300862949
Test: atest FrameworksMockingServicesTests:QuotaControllerTest
      atest CtsJobSchedulerTestCases
Change-Id: Ib19343900096aa6afa3bdb610ac2d722ce23feb0
parent 0fc10250
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -28,3 +28,13 @@ flag {
    description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low"
    bug: "299329948"
}

flag {
    name: "count_quota_fix"
    namespace: "backstage_power"
    description: "Fix job count quota check"
    bug: "300862949"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}
+20 −0
Original line number Diff line number Diff line
@@ -1650,6 +1650,16 @@ class JobConcurrencyManager {
                    continue;
                }

                if (Flags.countQuotaFix() && !nextPending.isReady()) {
                    // This could happen when the constraints for the job have been marked
                    // as unsatisfiled but hasn't been removed from the pending queue yet.
                    if (DEBUG) {
                        Slog.w(TAG, "Pending+not ready job: " + nextPending);
                    }
                    pendingJobQueue.remove(nextPending);
                    continue;
                }

                if (DEBUG && isSimilarJobRunningLocked(nextPending)) {
                    Slog.w(TAG, "Already running similar job to: " + nextPending);
                }
@@ -1737,6 +1747,16 @@ class JobConcurrencyManager {
                    continue;
                }

                if (Flags.countQuotaFix() && !nextPending.isReady()) {
                    // This could happen when the constraints for the job have been marked
                    // as unsatisfiled but hasn't been removed from the pending queue yet.
                    if (DEBUG) {
                        Slog.w(TAG, "Pending+not ready job: " + nextPending);
                    }
                    pendingJobQueue.remove(nextPending);
                    continue;
                }

                if (DEBUG && isSimilarJobRunningLocked(nextPending)) {
                    Slog.w(TAG, "Already running similar job to: " + nextPending);
                }
+80 −25
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@ import com.android.server.AppSchedulingModuleThread;
import com.android.server.LocalServices;
import com.android.server.PowerAllowlistInternal;
import com.android.server.job.ConstantsProto;
import com.android.server.job.Flags;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateControllerProto;
import com.android.server.usage.AppStandbyInternal;
@@ -512,7 +513,7 @@ public final class QuotaController extends StateController {

    /** An app has reached its quota. The message should contain a {@link UserPackage} object. */
    @VisibleForTesting
    static final int MSG_REACHED_QUOTA = 0;
    static final int MSG_REACHED_TIME_QUOTA = 0;
    /** Drop any old timing sessions. */
    private static final int MSG_CLEAN_UP_SESSIONS = 1;
    /** Check if a package is now within its quota. */
@@ -524,7 +525,7 @@ public final class QuotaController extends StateController {
     * object.
     */
    @VisibleForTesting
    static final int MSG_REACHED_EJ_QUOTA = 4;
    static final int MSG_REACHED_EJ_TIME_QUOTA = 4;
    /**
     * Process a new {@link UsageEvents.Event}. The event will be the message's object and the
     * userId will the first arg.
@@ -533,6 +534,11 @@ public final class QuotaController extends StateController {
    /** A UID's free quota grace period has ended. */
    @VisibleForTesting
    static final int MSG_END_GRACE_PERIOD = 6;
    /**
     * An app has reached its job count quota. The message should contain a {@link UserPackage}
     * object.
     */
    static final int MSG_REACHED_COUNT_QUOTA = 7;

    public QuotaController(@NonNull JobSchedulerService service,
            @NonNull BackgroundJobsController backgroundJobsController,
@@ -874,12 +880,14 @@ public final class QuotaController extends StateController {
    }

    @VisibleForTesting
    @GuardedBy("mLock")
    boolean isWithinQuotaLocked(@NonNull final JobStatus jobStatus) {
        final int standbyBucket = jobStatus.getEffectiveStandbyBucket();
        // A job is within quota if one of the following is true:
        //   1. it was started while the app was in the TOP state
        //   2. the app is currently in the foreground
        //   3. the app overall is within its quota
        if (!Flags.countQuotaFix()) {
            return jobStatus.shouldTreatAsUserInitiatedJob()
                    || isTopStartedJobLocked(jobStatus)
                    || isUidInForeground(jobStatus.getSourceUid())
@@ -887,6 +895,33 @@ public final class QuotaController extends StateController {
                    jobStatus.getSourceUserId(), jobStatus.getSourcePackageName(), standbyBucket);
        }

        if (jobStatus.shouldTreatAsUserInitiatedJob()
                || isTopStartedJobLocked(jobStatus)
                || isUidInForeground(jobStatus.getSourceUid())) {
            return true;
        }

        if (standbyBucket == NEVER_INDEX) return false;

        if (isQuotaFreeLocked(standbyBucket)) return true;

        final ExecutionStats stats = getExecutionStatsLocked(jobStatus.getSourceUserId(),
                jobStatus.getSourcePackageName(), standbyBucket);
        if (!(getRemainingExecutionTimeLocked(stats) > 0)) {
            // Out of execution time quota.
            return false;
        }

        if (standbyBucket != RESTRICTED_INDEX && mService.isCurrentlyRunningLocked(jobStatus)) {
            // Running job is considered as within quota except for the restricted one, which
            // requires additional constraints.
            return true;
        }

        // Check if the app is within job count quota.
        return isUnderJobCountQuotaLocked(stats) && isUnderSessionCountQuotaLocked(stats);
    }

    @GuardedBy("mLock")
    private boolean isQuotaFreeLocked(final int standbyBucket) {
        // Quota constraint is not enforced while charging.
@@ -909,12 +944,11 @@ public final class QuotaController extends StateController {
        ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
        // TODO: use a higher minimum remaining time for jobs with MINIMUM priority
        return getRemainingExecutionTimeLocked(stats) > 0
                && isUnderJobCountQuotaLocked(stats, standbyBucket)
                && isUnderSessionCountQuotaLocked(stats, standbyBucket);
                && isUnderJobCountQuotaLocked(stats)
                && isUnderSessionCountQuotaLocked(stats);
    }

    private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats,
            final int standbyBucket) {
    private boolean isUnderJobCountQuotaLocked(@NonNull ExecutionStats stats) {
        final long now = sElapsedRealtimeClock.millis();
        final boolean isUnderAllowedTimeQuota =
                (stats.jobRateLimitExpirationTimeElapsed <= now
@@ -923,8 +957,7 @@ public final class QuotaController extends StateController {
                && stats.bgJobCountInWindow < stats.jobCountLimit;
    }

    private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats,
            final int standbyBucket) {
    private boolean isUnderSessionCountQuotaLocked(@NonNull ExecutionStats stats) {
        final long now = sElapsedRealtimeClock.millis();
        final boolean isUnderAllowedTimeQuota = (stats.sessionRateLimitExpirationTimeElapsed <= now
                || stats.sessionCountInRateLimitingWindow < mMaxSessionCountPerRateLimitingWindow);
@@ -1449,6 +1482,9 @@ public final class QuotaController extends StateController {
                stats.jobCountInRateLimitingWindow = 0;
            }
            stats.jobCountInRateLimitingWindow += count;
            if (Flags.countQuotaFix()) {
                stats.bgJobCountInWindow += count;
            }
        }
    }

@@ -1683,10 +1719,11 @@ public final class QuotaController extends StateController {
                    changedJobs.add(js);
                }
            } else if (realStandbyBucket != EXEMPTED_INDEX && realStandbyBucket != ACTIVE_INDEX
                    && realStandbyBucket == js.getEffectiveStandbyBucket()) {
                    && realStandbyBucket == js.getEffectiveStandbyBucket()
                    && !(Flags.countQuotaFix() && mService.isCurrentlyRunningLocked(js))) {
                // An app in the ACTIVE bucket may be out of quota while the job could be in quota
                // for some reason. Therefore, avoid setting the real value here and check each job
                // individually.
                // individually. Running job need to determine its own quota status as well.
                if (setConstraintSatisfied(js, nowElapsed, realInQuota, isWithinEJQuota)) {
                    changedJobs.add(js);
                }
@@ -1805,9 +1842,8 @@ public final class QuotaController extends StateController {
        }

        ExecutionStats stats = getExecutionStatsLocked(userId, packageName, standbyBucket);
        final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats, standbyBucket);
        final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
                standbyBucket);
        final boolean isUnderJobCountQuota = isUnderJobCountQuotaLocked(stats);
        final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats);
        final long remainingEJQuota = getRemainingEJExecutionTimeLocked(userId, packageName);

        final boolean inRegularQuota =
@@ -2126,6 +2162,13 @@ public final class QuotaController extends StateController {
                mBgJobCount++;
                if (mRegularJobTimer) {
                    incrementJobCountLocked(mPkg.userId, mPkg.packageName, 1);
                    if (Flags.countQuotaFix()) {
                        final ExecutionStats stats = getExecutionStatsLocked(mPkg.userId,
                                mPkg.packageName, jobStatus.getEffectiveStandbyBucket(), false);
                        if (!isUnderJobCountQuotaLocked(stats)) {
                            mHandler.obtainMessage(MSG_REACHED_COUNT_QUOTA, mPkg).sendToTarget();
                        }
                    }
                }
                if (mRunningBgJobs.size() == 1) {
                    // Started tracking the first job.
@@ -2257,7 +2300,6 @@ public final class QuotaController extends StateController {
                    // repeatedly plugged in and unplugged, or an app changes foreground state
                    // very frequently, the job count for a package may be artificially high.
                    mBgJobCount = mRunningBgJobs.size();

                    if (mRegularJobTimer) {
                        incrementJobCountLocked(mPkg.userId, mPkg.packageName, mBgJobCount);
                        // Starting the timer means that all cached execution stats are now
@@ -2284,7 +2326,8 @@ public final class QuotaController extends StateController {
                    return;
                }
                Message msg = mHandler.obtainMessage(
                        mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg);
                        mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA,
                        mPkg);
                final long timeRemainingMs = mRegularJobTimer
                        ? getTimeUntilQuotaConsumedLocked(mPkg.userId, mPkg.packageName)
                        : getTimeUntilEJQuotaConsumedLocked(mPkg.userId, mPkg.packageName);
@@ -2301,7 +2344,7 @@ public final class QuotaController extends StateController {

        private void cancelCutoff() {
            mHandler.removeMessages(
                    mRegularJobTimer ? MSG_REACHED_QUOTA : MSG_REACHED_EJ_QUOTA, mPkg);
                    mRegularJobTimer ? MSG_REACHED_TIME_QUOTA : MSG_REACHED_EJ_TIME_QUOTA, mPkg);
        }

        public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
@@ -2557,7 +2600,7 @@ public final class QuotaController extends StateController {
                    break;
                default:
                    if (DEBUG) {
                        Slog.d(TAG, "Dropping event " + event.getEventType());
                        Slog.d(TAG, "Dropping usage event " + event.getEventType());
                    }
                    break;
            }
@@ -2666,7 +2709,7 @@ public final class QuotaController extends StateController {
        public void handleMessage(Message msg) {
            synchronized (mLock) {
                switch (msg.what) {
                    case MSG_REACHED_QUOTA: {
                    case MSG_REACHED_TIME_QUOTA: {
                        UserPackage pkg = (UserPackage) msg.obj;
                        if (DEBUG) {
                            Slog.d(TAG, "Checking if " + pkg + " has reached its quota.");
@@ -2685,7 +2728,7 @@ public final class QuotaController extends StateController {
                            // This could potentially happen if an old session phases out while a
                            // job is currently running.
                            // Reschedule message
                            Message rescheduleMsg = obtainMessage(MSG_REACHED_QUOTA, pkg);
                            Message rescheduleMsg = obtainMessage(MSG_REACHED_TIME_QUOTA, pkg);
                            timeRemainingMs = getTimeUntilQuotaConsumedLocked(pkg.userId,
                                    pkg.packageName);
                            if (DEBUG) {
@@ -2695,7 +2738,7 @@ public final class QuotaController extends StateController {
                        }
                        break;
                    }
                    case MSG_REACHED_EJ_QUOTA: {
                    case MSG_REACHED_EJ_TIME_QUOTA: {
                        UserPackage pkg = (UserPackage) msg.obj;
                        if (DEBUG) {
                            Slog.d(TAG, "Checking if " + pkg + " has reached its EJ quota.");
@@ -2713,7 +2756,7 @@ public final class QuotaController extends StateController {
                            // This could potentially happen if an old session phases out while a
                            // job is currently running.
                            // Reschedule message
                            Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_QUOTA, pkg);
                            Message rescheduleMsg = obtainMessage(MSG_REACHED_EJ_TIME_QUOTA, pkg);
                            timeRemainingMs = getTimeUntilEJQuotaConsumedLocked(
                                    pkg.userId, pkg.packageName);
                            if (DEBUG) {
@@ -2723,6 +2766,18 @@ public final class QuotaController extends StateController {
                        }
                        break;
                    }
                    case MSG_REACHED_COUNT_QUOTA: {
                        UserPackage pkg = (UserPackage) msg.obj;
                        if (DEBUG) {
                            Slog.d(TAG, pkg + " has reached its count quota.");
                        }

                        mStateChangedListener.onControllerStateChanged(
                                maybeUpdateConstraintForPkgLocked(
                                        sElapsedRealtimeClock.millis(),
                                        pkg.userId, pkg.packageName));
                        break;
                    }
                    case MSG_CLEAN_UP_SESSIONS:
                        if (DEBUG) {
                            Slog.d(TAG, "Cleaning up timing sessions.");
+81 −4
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
@@ -75,6 +76,9 @@ import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.DeviceConfig;
import android.util.ArraySet;
import android.util.SparseBooleanArray;
@@ -98,6 +102,7 @@ import com.android.server.usage.AppStandbyInternal;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -154,6 +159,10 @@ public class QuotaControllerTest {
    @Mock
    private UsageStatsManagerInternal mUsageStatsManager;

    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

    private JobStore mJobStore;

    @Before
@@ -1978,7 +1987,7 @@ public class QuotaControllerTest {
    }

    @Test
    public void testIsWithinQuotaLocked_UnderDuration_OverJobCount() {
    public void testIsWithinQuotaLocked_UnderDuration_OverJobCountRateLimitWindow() {
        setDischarging();
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
@@ -2021,7 +2030,7 @@ public class QuotaControllerTest {
    }

    @Test
    public void testIsWithinQuotaLocked_OverDuration_OverJobCount() {
    public void testIsWithinQuotaLocked_OverDuration_OverJobCountRateLimitWindow() {
        setDischarging();
        final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        final int jobCount = mQcConstants.MAX_JOB_COUNT_PER_RATE_LIMITING_WINDOW;
@@ -2166,6 +2175,74 @@ public class QuotaControllerTest {
        }
    }

    @Test
    @RequiresFlagsEnabled(FLAG_COUNT_QUOTA_FIX)
    public void testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow() {
        setDischarging();

        JobStatus jobRunning = createJobStatus(
                "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 1);
        JobStatus jobPending = createJobStatus(
                "testIsWithinQuotaLocked_UnderDuration_OverJobCountInWindow", 2);
        setStandbyBucket(WORKING_INDEX, jobRunning, jobPending);

        setDeviceConfigInt(QcConstants.KEY_MAX_JOB_COUNT_WORKING, 10);

        long now = JobSchedulerService.sElapsedRealtimeClock.millis();
        mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE,
                createTimingSession(now - (HOUR_IN_MILLIS), 5 * MINUTE_IN_MILLIS, 9), false);

        final ExecutionStats stats;
        synchronized (mQuotaController.mLock) {
            stats = mQuotaController.getExecutionStatsLocked(
                    SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX);
            assertTrue(mQuotaController
                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
            assertEquals(10, stats.jobCountLimit);
            assertEquals(9, stats.bgJobCountInWindow);
        }

        when(mJobSchedulerService.isCurrentlyRunningLocked(jobRunning)).thenReturn(true);
        when(mJobSchedulerService.isCurrentlyRunningLocked(jobPending)).thenReturn(false);

        InOrder inOrder = inOrder(mJobSchedulerService);
        trackJobs(jobRunning, jobPending);
        // UID in the background.
        setProcessState(ActivityManager.PROCESS_STATE_SERVICE);
        // Start the job.
        synchronized (mQuotaController.mLock) {
            mQuotaController.prepareForExecutionLocked(jobRunning);
        }

        advanceElapsedClock(MINUTE_IN_MILLIS);
        // Wait for some extra time to allow for job processing.
        ArraySet<JobStatus> expected = new ArraySet<>();
        expected.add(jobPending);
        inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1))
                .onControllerStateChanged(eq(expected));

        synchronized (mQuotaController.mLock) {
            assertTrue(mQuotaController.isWithinQuotaLocked(jobRunning));
            assertTrue(jobRunning.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
            assertTrue(jobRunning.isReady());
            assertFalse(mQuotaController.isWithinQuotaLocked(jobPending));
            assertFalse(jobPending.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
            assertFalse(jobPending.isReady());
            assertEquals(10, stats.bgJobCountInWindow);
        }

        advanceElapsedClock(MINUTE_IN_MILLIS);
        synchronized (mQuotaController.mLock) {
            mQuotaController.maybeStopTrackingJobLocked(jobRunning, null);
        }

        synchronized (mQuotaController.mLock) {
            assertFalse(mQuotaController
                    .isWithinQuotaLocked(SOURCE_USER_ID, SOURCE_PACKAGE, WORKING_INDEX));
            assertEquals(10, stats.bgJobCountInWindow);
        }
    }

    @Test
    public void testIsWithinQuotaLocked_TimingSession() {
        setDischarging();
@@ -4651,7 +4728,7 @@ public class QuotaControllerTest {
        // Handler is told to check when the quota will be consumed, not when the initial
        // remaining time is over.
        verify(handler, atLeast(1)).sendMessageDelayed(
                argThat(msg -> msg.what == QuotaController.MSG_REACHED_QUOTA),
                argThat(msg -> msg.what == QuotaController.MSG_REACHED_TIME_QUOTA),
                eq(10 * SECOND_IN_MILLIS));
        verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));

@@ -6618,7 +6695,7 @@ public class QuotaControllerTest {
        // Handler is told to check when the quota will be consumed, not when the initial
        // remaining time is over.
        verify(handler, atLeast(1)).sendMessageDelayed(
                argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_QUOTA),
                argThat(msg -> msg.what == QuotaController.MSG_REACHED_EJ_TIME_QUOTA),
                eq(10 * SECOND_IN_MILLIS));
        verify(handler, never()).sendMessageDelayed(any(), eq(remainingTimeMs));
    }