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

Commit 5871539d authored by Kweku Adams's avatar Kweku Adams
Browse files

Make sure out-of-quota EJs stop in time.

If an app runs out of EJ quota while one of its EJs is running, then the
running EJ should only be allowed to run for the minimum execution limit
(currently 3 minutes). Once that execution time has transpired, the
out-of-quota EJ should be stopped.

Bug: 171305774
Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/job
Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/job
Test: atest CtsJobSchedulerTestCases
Change-Id: Iacabac599939204783e417b700a754ffd0a5759c
parent db348d44
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -891,7 +891,7 @@ class JobConcurrencyManager {
        }

        // Only expedited jobs can replace expedited jobs.
        if (js.shouldTreatAsExpeditedJob()) {
        if (js.shouldTreatAsExpeditedJob() || js.startedAsExpeditedJob) {
            // Keep fg/bg user distinction.
            if (workType == WORK_TYPE_BGUSER_IMPORTANT || workType == WORK_TYPE_BGUSER) {
                // Let any important bg user job replace a bg user expedited job.
+5 −1
Original line number Diff line number Diff line
@@ -2187,6 +2187,10 @@ public class JobSchedulerService extends com.android.server.SystemService
     */
    @VisibleForTesting
    boolean isReadyToBeExecutedLocked(JobStatus job) {
        return isReadyToBeExecutedLocked(job, true);
    }

    boolean isReadyToBeExecutedLocked(JobStatus job, boolean rejectActive) {
        final boolean jobReady = job.isReady();

        if (DEBUG) {
@@ -2225,7 +2229,7 @@ public class JobSchedulerService extends com.android.server.SystemService
        }

        final boolean jobPending = mPendingJobs.contains(job);
        final boolean jobActive = isCurrentlyActiveLocked(job);
        final boolean jobActive = rejectActive && isCurrentlyActiveLocked(job);

        if (DEBUG) {
            Slog.v(TAG, "isReadyToBeExecutedLocked: " + job.toShortString()
+52 −1
Original line number Diff line number Diff line
@@ -151,6 +151,14 @@ public final class JobServiceContext implements ServiceConnection {
    /** The absolute maximum amount of time the job can run */
    private long mMaxExecutionTimeMillis;

    /**
     * The stop reason for a pending cancel. If there's not pending cancel, then the value should be
     * {@link JobParameters#STOP_REASON_UNDEFINED}.
     */
    private int mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
    private int mPendingLegacyStopReason;
    private String mPendingDebugStopReason;

    // Debugging: reason this job was last stopped.
    public String mStoppedReason;

@@ -328,6 +336,7 @@ public final class JobServiceContext implements ServiceConnection {
            mAvailable = false;
            mStoppedReason = null;
            mStoppedTime = 0;
            job.startedAsExpeditedJob = job.shouldTreatAsExpeditedJob();
            return true;
        }
    }
@@ -625,6 +634,19 @@ public final class JobServiceContext implements ServiceConnection {
            }
            return;
        }
        if (mRunningJob.startedAsExpeditedJob
                && stopReasonCode == JobParameters.STOP_REASON_QUOTA) {
            // EJs should be able to run for at least the min upper limit regardless of quota.
            final long earliestStopTimeElapsed =
                    mExecutionStartTimeElapsed + mMinExecutionGuaranteeMillis;
            final long nowElapsed = sElapsedRealtimeClock.millis();
            if (nowElapsed < earliestStopTimeElapsed) {
                mPendingStopReason = stopReasonCode;
                mPendingLegacyStopReason = legacyStopReason;
                mPendingDebugStopReason = debugReason;
                return;
            }
        }
        mParams.setStopReason(stopReasonCode, legacyStopReason, debugReason);
        if (legacyStopReason == JobParameters.REASON_PREEMPT) {
            mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
@@ -777,6 +799,23 @@ public final class JobServiceContext implements ServiceConnection {
                closeAndCleanupJobLocked(true /* needsReschedule */, "timed out while stopping");
                break;
            case VERB_EXECUTING:
                if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
                    if (mService.isReadyToBeExecutedLocked(mRunningJob, false)) {
                        // Job became ready again while we were waiting to stop it (for example,
                        // the device was temporarily taken off the charger). Ignore the pending
                        // stop and see what the manager says.
                        mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
                        mPendingLegacyStopReason = 0;
                        mPendingDebugStopReason = null;
                    } else {
                        Slog.i(TAG, "JS was waiting to stop this job."
                                + " Sending onStop: " + getRunningJobNameLocked());
                        mParams.setStopReason(mPendingStopReason, mPendingLegacyStopReason,
                                mPendingDebugStopReason);
                        sendStopMessageLocked(mPendingDebugStopReason);
                        break;
                    }
                }
                final long latestStopTimeElapsed =
                        mExecutionStartTimeElapsed + mMaxExecutionTimeMillis;
                final long nowElapsed = sElapsedRealtimeClock.millis();
@@ -886,6 +925,9 @@ public final class JobServiceContext implements ServiceConnection {
        mCancelled = false;
        service = null;
        mAvailable = true;
        mPendingStopReason = JobParameters.STOP_REASON_UNDEFINED;
        mPendingLegacyStopReason = 0;
        mPendingDebugStopReason = null;
        removeOpTimeOutLocked();
        mCompletedListener.onJobCompletedLocked(completedJob, legacyStopReason, reschedule);
        mJobConcurrencyManager.onJobCompletedLocked(this, completedJob, workType);
@@ -972,7 +1014,16 @@ public final class JobServiceContext implements ServiceConnection {
            pw.print(", ");
            TimeUtils.formatDuration(
                    (mExecutionStartTimeElapsed + mMaxExecutionTimeMillis) - nowElapsed, pw);
            pw.println("]");
            pw.print("]");
            if (mPendingStopReason != JobParameters.STOP_REASON_UNDEFINED) {
                pw.print(" Pending stop because ");
                pw.print(mPendingStopReason);
                pw.print("/");
                pw.print(mPendingLegacyStopReason);
                pw.print("/");
                pw.print(mPendingDebugStopReason);
            }
            pw.println();
            pw.decreaseIndent();
        }
    }
+14 −4
Original line number Diff line number Diff line
@@ -325,6 +325,12 @@ public final class JobStatus {
    /** The evaluated priority of the job when it started running. */
    public int lastEvaluatedPriority;

    /**
     * Whether or not this particular JobStatus instance was treated as an EJ when it started
     * running. This isn't copied over when a job is rescheduled.
     */
    public boolean startedAsExpeditedJob = false;

    // If non-null, this is work that has been enqueued for the job.
    public ArrayList<JobWorkItem> pendingWork;

@@ -1112,18 +1118,19 @@ public final class JobStatus {
     */
    public boolean canRunInDoze() {
        return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
                || (shouldTreatAsExpeditedJob()
                || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
                && (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0);
    }

    boolean canRunInBatterySaver() {
        return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0
                || (shouldTreatAsExpeditedJob()
                || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob)
                && (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0);
    }

    boolean shouldIgnoreNetworkBlocking() {
        return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0 || shouldTreatAsExpeditedJob();
        return (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0
                || (shouldTreatAsExpeditedJob() || startedAsExpeditedJob);
    }

    /** @return true if the constraint was changed, false otherwise. */
@@ -2022,7 +2029,10 @@ public final class JobStatus {
        pw.println(serviceInfo != null);
        if ((getFlags() & JobInfo.FLAG_EXPEDITED) != 0) {
            pw.print("readyWithinExpeditedQuota: ");
            pw.println(mReadyWithinExpeditedQuota);
            pw.print(mReadyWithinExpeditedQuota);
            pw.print(" (started as EJ: ");
            pw.print(startedAsExpeditedJob);
            pw.println(")");
        }
        pw.decreaseIndent();

+7 −11
Original line number Diff line number Diff line
@@ -849,10 +849,9 @@ public final class QuotaController extends StateController {
            return true;
        }
        // A job is within quota if one of the following is true:
        //   1. it's already running (already executing expedited jobs should be allowed to finish)
        //   2. the app is currently in the foreground
        //   3. the app overall is within its quota
        //   4. It's on the temp allowlist (or within the grace period)
        //   1. the app is currently in the foreground
        //   2. the app overall is within its quota
        //   3. It's on the temp allowlist (or within the grace period)
        if (isTopStartedJobLocked(jobStatus) || isUidInForeground(jobStatus.getSourceUid())) {
            return true;
        }
@@ -873,13 +872,6 @@ public final class QuotaController extends StateController {
            return true;
        }

        Timer ejTimer = mEJPkgTimers.get(jobStatus.getSourceUserId(),
                jobStatus.getSourcePackageName());
        // Any already executing expedited jobs should be allowed to finish.
        if (ejTimer != null && ejTimer.isRunning(jobStatus)) {
            return true;
        }

        return 0 < getRemainingEJExecutionTimeLocked(
                jobStatus.getSourceUserId(), jobStatus.getSourcePackageName());
    }
@@ -4153,6 +4145,8 @@ public final class QuotaController extends StateController {
                pw.print(", ");
                if (js.shouldTreatAsExpeditedJob()) {
                    pw.print("within EJ quota");
                } else if (js.startedAsExpeditedJob) {
                    pw.print("out of EJ quota");
                } else if (js.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)) {
                    pw.print("within regular quota");
                } else {
@@ -4163,6 +4157,8 @@ public final class QuotaController extends StateController {
                    pw.print(getRemainingEJExecutionTimeLocked(
                            js.getSourceUserId(), js.getSourcePackageName()));
                    pw.print("ms remaining in EJ quota");
                } else if (js.startedAsExpeditedJob) {
                    pw.print("should be stopped after min execution time");
                } else {
                    pw.print(getRemainingExecutionTimeLocked(js));
                    pw.print("ms remaining in quota");
Loading