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

Commit 0d5bb03f authored by Sanath Kumar's avatar Sanath Kumar Committed by Android (Google) Code Review
Browse files

Merge "Implement new stop reason for maybe abandoned jobs" into main

parents 0086838a 35afa1ac
Loading
Loading
Loading
Loading
+44 −5
Original line number Diff line number Diff line
@@ -618,6 +618,26 @@ public final class JobServiceContext implements ServiceConnection {
        return mRunningJob;
    }

    @VisibleForTesting
    void setRunningJobLockedForTest(JobStatus job) {
        mRunningJob = job;
    }

    @VisibleForTesting
    void setJobParamsLockedForTest(JobParameters params) {
        mParams = params;
    }

    @VisibleForTesting
    void setRunningCallbackLockedForTest(JobCallback callback) {
        mRunningCallback = callback;
    }

    @VisibleForTesting
    void setPendingStopReasonLockedForTest(int stopReason) {
        mPendingStopReason = stopReason;
    }

    @JobConcurrencyManager.WorkType
    int getRunningJobWorkType() {
        return mRunningJobWorkType;
@@ -786,6 +806,7 @@ public final class JobServiceContext implements ServiceConnection {
                executing = getRunningJobLocked();
            }
            if (executing != null && jobId == executing.getJobId()) {
                executing.setAbandoned(true);
                final StringBuilder stateSuffix = new StringBuilder();
                stateSuffix.append(TRACE_ABANDONED_JOB);
                stateSuffix.append(executing.getBatteryName());
@@ -1364,8 +1385,9 @@ public final class JobServiceContext implements ServiceConnection {
    }

    /** Process MSG_TIMEOUT here. */
    @VisibleForTesting
    @GuardedBy("mLock")
    private void handleOpTimeoutLocked() {
    void handleOpTimeoutLocked() {
        switch (mVerb) {
            case VERB_BINDING:
                // The system may have been too busy. Don't drop the job or trigger an ANR.
@@ -1427,9 +1449,25 @@ public final class JobServiceContext implements ServiceConnection {
                    // Not an error - client ran out of time.
                    Slog.i(TAG, "Client timed out while executing (no jobFinished received)."
                            + " Sending onStop: " + getRunningJobNameLocked());
                    mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT,
                            JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out");
                    sendStopMessageLocked("timeout while executing");

                    final JobStatus executing = getRunningJobLocked();
                    int stopReason = JobParameters.STOP_REASON_TIMEOUT;
                    int internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT;
                    final StringBuilder stopMessage = new StringBuilder("timeout while executing");
                    final StringBuilder debugStopReason = new StringBuilder("client timed out");

                    if (android.app.job.Flags.handleAbandonedJobs()
                            && executing != null && executing.isAbandoned()) {
                        final String abandonedMessage = " and maybe abandoned";
                        stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED;
                        internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED;
                        stopMessage.append(abandonedMessage);
                        debugStopReason.append(abandonedMessage);
                    }

                    mParams.setStopReason(stopReason,
                                    internalStopReason, debugStopReason.toString());
                    sendStopMessageLocked(stopMessage.toString());
                } else if (nowElapsed >= earliestStopTimeElapsed) {
                    // We've given the app the minimum execution time. See if we should stop it or
                    // let it continue running
@@ -1479,8 +1517,9 @@ public final class JobServiceContext implements ServiceConnection {
     * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING ->
     * VERB_STOPPING.
     */
    @VisibleForTesting
    @GuardedBy("mLock")
    private void sendStopMessageLocked(@Nullable String reason) {
    void sendStopMessageLocked(@Nullable String reason) {
        removeOpTimeOutLocked();
        if (mVerb != VERB_EXECUTING) {
            Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob);
+19 −0
Original line number Diff line number Diff line
@@ -575,6 +575,13 @@ public final class JobStatus {
    /** The system trace tag for this job. */
    private String mSystemTraceTag;

    /**
     * Job maybe abandoned by not calling
     * {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} while
     * the strong reference to {@link android.app.job.JobParameters} is lost
     */
    private boolean mIsAbandoned;

    /**
     * Core constructor for JobStatus instances.  All other ctors funnel down to this one.
     *
@@ -725,6 +732,8 @@ public final class JobStatus {
        updateNetworkBytesLocked();

        updateMediaBackupExemptionStatus();

        mIsAbandoned = false;
    }

    /** Copy constructor: used specifically when cloning JobStatus objects for persistence,
@@ -1061,6 +1070,16 @@ public final class JobStatus {
        return job.getTraceTag();
    }

    /** Returns if the job maybe abandoned */
    public boolean isAbandoned() {
        return mIsAbandoned;
    }

    /** Set the job maybe abandoned state*/
    public void setAbandoned(boolean abandoned) {
        mIsAbandoned = abandoned;
    }

    /** Returns a trace tag using debug information provided by job scheduler service. */
    @NonNull
    public String computeSystemTraceTag() {
+270 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.job;

import static android.app.job.Flags.FLAG_HANDLE_ABANDONED_JOBS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
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 org.junit.Assert.assertEquals;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.app.AppGlobals;
import android.app.job.JobParameters;
import android.content.Context;
import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemClock;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;

import com.android.internal.app.IBatteryStats;
import com.android.server.job.JobServiceContext.JobCallback;
import com.android.server.job.controllers.JobStatus;

import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;

import java.time.Clock;
import java.time.Duration;
import java.time.ZoneOffset;

public class JobServiceContextTest {
    private static final String TAG = JobServiceContextTest.class.getSimpleName();
    @ClassRule
    public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule();
    @Rule
    public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule();
    @Mock
    private JobSchedulerService mMockJobSchedulerService;
    @Mock
    private JobConcurrencyManager mMockConcurrencyManager;
    @Mock
    private JobNotificationCoordinator mMockNotificationCoordinator;
    @Mock
    private IBatteryStats.Stub mMockBatteryStats;
    @Mock
    private JobPackageTracker mMockJobPackageTracker;
    @Mock
    private Looper mMockLooper;
    @Mock
    private Context mMockContext;
    @Mock
    private JobStatus mMockJobStatus;
    @Mock
    private JobParameters mMockJobParameters;
    @Mock
    private JobCallback mMockJobCallback;
    private MockitoSession mMockingSession;
    private JobServiceContext mJobServiceContext;
    private Object mLock;

    @Before
    public void setUp() throws Exception {
        mMockingSession =
                mockitoSession()
                        .initMocks(this)
                        .mockStatic(AppGlobals.class)
                        .strictness(Strictness.LENIENT)
                        .startMocking();
        JobSchedulerService.sElapsedRealtimeClock =
                Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC);
        doReturn(mock(PowerManager.class)).when(mMockContext).getSystemService(PowerManager.class);
        doReturn(mMockContext).when(mMockJobSchedulerService).getContext();
        mLock = new Object();
        doReturn(mLock).when(mMockJobSchedulerService).getLock();
        mJobServiceContext =
                new JobServiceContext(
                        mMockJobSchedulerService,
                        mMockConcurrencyManager,
                        mMockNotificationCoordinator,
                        mMockBatteryStats,
                        mMockJobPackageTracker,
                        mMockLooper);
        spyOn(mJobServiceContext);
        mJobServiceContext.setJobParamsLockedForTest(mMockJobParameters);
    }

    @After
    public void tearDown() throws Exception {
        if (mMockingSession != null) {
            mMockingSession.finishMocking();
        }
    }

    private Clock getAdvancedClock(Clock clock, long incrementMs) {
        return Clock.offset(clock, Duration.ofMillis(incrementMs));
    }

    private void advanceElapsedClock(long incrementMs) {
        JobSchedulerService.sElapsedRealtimeClock =
                getAdvancedClock(JobSchedulerService.sElapsedRealtimeClock, incrementMs);
    }

    /**
     * Test that Abandoned jobs that are timed out are stopped with the correct stop reason
     */
    @Test
    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_TimeoutAbandonedJob() {
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());

        advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes
        mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);

        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
        doReturn(true).when(mMockJobStatus).isAbandoned();
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;

        mJobServiceContext.handleOpTimeoutLocked();

        String stopMessage = captor.getValue();
        assertEquals("timeout while executing and maybe abandoned", stopMessage);
        verify(mMockJobParameters)
                .setStopReason(
                        JobParameters.STOP_REASON_TIMEOUT_ABANDONED,
                        JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED,
                        "client timed out and maybe abandoned");
    }

    /**
     * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason
     */
    @Test
    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_TimeoutNoAbandonedJob() {
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());

        advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes
        mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);

        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
        doReturn(false).when(mMockJobStatus).isAbandoned();
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;

        mJobServiceContext.handleOpTimeoutLocked();

        String stopMessage = captor.getValue();
        assertEquals("timeout while executing", stopMessage);
        verify(mMockJobParameters)
                .setStopReason(
                        JobParameters.STOP_REASON_TIMEOUT,
                        JobParameters.INTERNAL_STOP_REASON_TIMEOUT,
                        "client timed out");
    }

    /**
     * Test that abandoned jobs that are timed out while the flag is disabled
     * are stopped with the correct stop reason
     */
    @Test
    @DisableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_TimeoutAbandonedJob_flagHandleAbandonedJobsDisabled() {
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;
        ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
        doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture());

        advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes
        mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED);

        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
        doReturn(true).when(mMockJobStatus).isAbandoned();
        mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING;

        mJobServiceContext.handleOpTimeoutLocked();

        String stopMessage = captor.getValue();
        assertEquals("timeout while executing", stopMessage);
        verify(mMockJobParameters)
                .setStopReason(
                        JobParameters.STOP_REASON_TIMEOUT,
                        JobParameters.INTERNAL_STOP_REASON_TIMEOUT,
                        "client timed out");
    }

    /**
     * Test that the JobStatus is marked as abandoned when the JobServiceContext
     * receives a MSG_HANDLE_ABANDONED_JOB message
     */
    @Test
    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_HandleAbandonedJob() {
        final int jobId = 123;
        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
        mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback);
        doReturn(jobId).when(mMockJobStatus).getJobId();

        mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId);

        verify(mMockJobStatus).setAbandoned(true);
    }

    /**
     * Test that the JobStatus is not marked as abandoned when the
     * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the
     * JobServiceContext is not running a job
     */
    @Test
    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_HandleAbandonedJob_notRunningJob() {
        final int jobId = 123;
        mJobServiceContext.setRunningJobLockedForTest(null);
        mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback);

        mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId);

        verify(mMockJobStatus, never()).setAbandoned(true);
    }

    /**
     * Test that the JobStatus is not marked as abandoned when the
     * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the
     * JobServiceContext is running a job with a different jobId
     */
    @Test
    @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS)
    public void testJobServiceContext_HandleAbandonedJob_differentJobId() {
        final int jobId = 123;
        final int differentJobId = 456;
        mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus);
        mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback);
        doReturn(differentJobId).when(mMockJobStatus).getJobId();

        mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId);

        verify(mMockJobStatus, never()).setAbandoned(true);
    }

}