Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JobScheduler posting Jobs twice (not expected)

I am writing a tutorial on JobScheduler and I find a strange behavior. I ask for 3 differents jobs to be scheduled in 1 second (.setOverrideDeadline(1000)) but they are all submitted and ran twice... So here the code:

public class MyApplication extends Application {
    private static final int JOB_ID_HanlderThread = 100;
    private static final int JOB_ID_ExecutorService = 200;
    private static final int JOB_ID_AsyncTask = 300;
    JobScheduler mJobScheduler;
    ExecutorService myExecutorServiceForJobs=null;
    private static MyApplication INSTANCE;
    public static MyApplication getInstance(){
        return INSTANCE;
    }


    /**
     * Called when the application is starting, before any activity, service,
     * or receiver objects (excluding content providers) have been created.
     * Implementations should be as quick as possible (for example using
     * lazy initialization of state) since the time spent in this function
     * directly impacts the performance of starting the first activity,
     * service, or receiver in a process.
     * If you override this method, be sure to call super.onCreate().
     */
    @Override
    public void onCreate() {
        Log.e("MyApplication", "*********************** onCreate *****************************");
        super.onCreate();
        //use only for the ExceutorService case
        INSTANCE=this;
        //instanciate your JobScheduler
        mJobScheduler= (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
        Log.e("MyApplication", "onCreate: JobScheduler instanciate");

        //this first example use the HandlerThread (no need of executor service)
        //---------------------------------------------------------------------
        //define your JobServices here
        JobInfo.Builder builder = new JobInfo.Builder(JOB_ID_HanlderThread,
                new ComponentName( getPackageName(),
                        MyJobServiceUsingHandlerThread.class.getName() ) );
        //begin in one second
        builder.setOverrideDeadline(1000);
        int returnedValue;
        //the return value is failure(0) or success(1) not the JobId if success (Javadoc wrong)
        returnedValue=mJobScheduler.schedule( builder.build() );
        //launch it
        if( returnedValue <= 0 ) {
            //If something goes wrong (manage exception/error is better than logging them)
            Log.e("MyApplication", "onCreate: JobScheduler launch the task failure");
        }else{
            //nothing goes wrong
            Log.e("MyApplication", "onCreate: JobScheduler launch the task suceess JOB_ID_HanlderThread "+returnedValue);
        }

        //this second example use ExecutorService
        //---------------------------------------
        //then again define your Job and launch it
        JobInfo.Builder builder1 = new JobInfo.Builder(JOB_ID_ExecutorService,
                new ComponentName( getPackageName(),
                        MyJobServiceUsingExecutor.class.getName() ) );
        //begin in one second
        builder1.setOverrideDeadline(1000);
        //launch it
        returnedValue=mJobScheduler.schedule( builder1.build() );
        if( returnedValue <= 0 ) {
            //If something goes wrong (manage exception/error is better than logging them)
            Log.e("MyApplication", "onCreate: JobScheduler launch the task failure");
        }else{
            //nothing goes wrong
            Log.e("MyApplication", "onCreate: JobScheduler launch the task suceess JOB_ID_ExecutorService "+returnedValue);
        }

        //this third example use AsyncTask
        //--------------------------------
        //then again define your Job and launch it
        JobInfo.Builder builder2 = new JobInfo.Builder(JOB_ID_AsyncTask,
                new ComponentName( getPackageName(),
                        MyJobServiceUsingAsyncTask.class.getName() ) );
        //begin in one second
        builder2.setOverrideDeadline(1000);
        //launch it
        returnedValue=mJobScheduler.schedule( builder2.build() );
        if( returnedValue <= 0 ) {
            //If something goes wrong (manage exception/error is better than logging them)
            Log.e("MyApplication", "onCreate: JobScheduler launch the task failure");
        }else{
            //nothing goes wrong
            Log.e("MyApplication", "onCreate: JobScheduler launch the task suceess JOB_ID_AsyncTask "+returnedValue);
        }
    }   

Using this code I expect my jobs run once, but if I look at the log I obtain:

10-20 06:45:13.118 13041-13041/? E/MyApplication: *********************** onCreate *****************************
10-20 06:45:13.122 13041-13041/? E/MyApplication: onCreate: JobScheduler instanciate
10-20 06:45:13.126 13041-13041/? E/MyApplication: onCreate: JobScheduler launch the task suceess JOB_ID_HanlderThread 1
10-20 06:45:13.127 13041-13041/? E/MyApplication: onCreate: JobScheduler launch the task suceess JOB_ID_ExecutorService 1
10-20 06:45:13.130 13041-13041/? E/MyApplication: onCreate: JobScheduler launch the task suceess JOB_ID_AsyncTask 1
10-20 06:45:13.559 13041-13041/? E/MyJobServiceHandler: onStartJob called <--------------------------------
10-20 06:45:13.572 13041-13041/? E/MyJobServiceExecutor: onStartJob called <--------------------------------
10-20 06:45:14.133 13041-13041/? E/MyJobServiceAsync: onStartJob called <--------------------------------
10-20 06:45:14.141 13041-13041/? E/MyJobServiceAsync: onStartJob called <--------------------------------
10-20 06:45:18.571 13041-13066/? E/MyHandler: The work is done in a separate thread called MyJobServiceUsingHandlerThread
10-20 06:45:18.573 13041-13041/? E/MyJobServiceHandler: onDestroy called, Looper is dead  <******************************************
10-20 06:45:18.574 13041-13041/? E/MyJobServiceHandler: onStartJob called <--------------------------------
10-20 06:45:18.576 13041-13067/? E/MyRunnable: The work is done in a separate thread called MyJobServiceUsingExecutorService
10-20 06:45:18.577 13041-13041/? E/MyJobServiceExecutor: onDestroy called, executor service is dead  <******************************************
10-20 06:45:18.577 13041-13041/? E/MyApplication: killMyExecutorServiceForJob called
10-20 06:45:18.577 13041-13041/? E/MyApplication: myExecutorServiceForJobs isShutDown
10-20 06:45:18.580 13041-13041/? E/MyJobServiceExecutor: onStartJob called <--------------------------------
10-20 06:45:19.145 13041-13070/? E/MyAsyncTask: The work is done in a separate thread called AsyncTask #1
10-20 06:45:19.145 13041-13041/? E/MyAsyncTask: The work is finished  <******************************************
10-20 06:45:23.576 13041-13075/? E/MyHandler: The work is done in a separate thread called MyJobServiceUsingHandlerThread
10-20 06:45:23.577 13041-13041/? E/MyJobServiceHandler: onDestroy called, Looper is dead  <******************************************
10-20 06:45:23.582 13041-13076/? E/MyRunnable: The work is done in a separate thread called MyJobServiceUsingExecutorService
10-20 06:45:23.584 13041-13041/? E/MyJobServiceExecutor: onDestroy called, executor service is dead  <******************************************
10-20 06:45:23.584 13041-13041/? E/MyApplication: killMyExecutorServiceForJob called
10-20 06:45:23.584 13041-13041/? E/MyApplication: myExecutorServiceForJobs isShutDown
10-20 06:45:24.147 13041-13077/? E/MyAsyncTask: The work is done in a separate thread called AsyncTask #2
10-20 06:45:24.148 13041-13041/? E/MyAsyncTask: The work is finished  <******************************************

What I do in the tutorial is I run one Job with HandlerThread, an other one with ExecutorService and the last using AsyncTask to explain how to do the work in a background thread. I show this different technics because there can be use cases where you want to enqueue your jobs in the same thread (HandlerThread) or manage a pool of Thread (ExecutorService) or just use unmanaged threads (AsyncTask).

I define thos Jobs and schedule them in MyApplication:onCreate method. To have a deeper look at the code, I put it on GitHub here :https://github.com/MathiasSeguy-Android2EE/JobSchedulerForGitHub

like image 536
Mathias Seguy Android2ee Avatar asked Oct 20 '15 11:10

Mathias Seguy Android2ee


1 Answers

Thanks for this - I worked on the JobScheduler. Based on your app (thanks!) I managed to repro this pretty easily and track down the cause of the bug.

tl;dr, This is a case which will not happen very often outside of a tutorial app. To work around it in your tutorial, increase the deadline on your job to larger than the amount of time each of your background threads run.

What is happening is that you schedule your jobs in succession, and the JobScheduler runs them pretty much immediately as they are scheduled. However, one second later (the one second is the part that will not happen for a "real" app) the override deadline alarm fires, and the jobscheduler decides very aggressively that any job whose deadline has expired needs to be run again (the API contract states that "deadline expiry" trumps all other considerations), so it puts it into the pending queue. As soon as the executing job is finished, the pending queue is checked, and there's a job there, so it's run.

So, the job will fire 2x if the deadline expires while the job is running. Ensure that the deadline expires either before the job runs (which results in the job running) or after (alarm won't actually land b/c the job's already finished), and everything works as intended.

I've fixed this in Android N (unfortunately M has already shipped), and added a CTS test to ensure it stays fixed. Thanks for bringing it to our attention

like image 117
Matthew Williams Avatar answered Nov 11 '22 17:11

Matthew Williams