JobStore.java revision 7060b04f6d92351b67222e636ab378a0273bf3e7
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server.job;
18
19import android.content.ComponentName;
20import android.app.job.JobInfo;
21import android.content.Context;
22import android.os.Environment;
23import android.os.Handler;
24import android.os.PersistableBundle;
25import android.os.SystemClock;
26import android.os.UserHandle;
27import android.util.AtomicFile;
28import android.util.ArraySet;
29import android.util.Pair;
30import android.util.Slog;
31import android.util.Xml;
32
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.util.FastXmlSerializer;
35import com.android.server.IoThread;
36import com.android.server.job.controllers.JobStatus;
37
38import java.io.ByteArrayOutputStream;
39import java.io.File;
40import java.io.FileInputStream;
41import java.io.FileNotFoundException;
42import java.io.FileOutputStream;
43import java.io.IOException;
44import java.util.ArrayList;
45import java.util.Iterator;
46import java.util.List;
47
48import org.xmlpull.v1.XmlPullParser;
49import org.xmlpull.v1.XmlPullParserException;
50import org.xmlpull.v1.XmlSerializer;
51
52/**
53 * Maintain a list of classes, and accessor methods/logic for these jobs.
54 * This class offers the following functionality:
55 *     - When a job is added, it will determine if the job requirements have changed (update) and
56 *       whether the controllers need to be updated.
57 *     - Persists JobInfos, figures out when to to rewrite the JobInfo to disk.
58 *     - Handles rescheduling of jobs.
59 *       - When a periodic job is executed and must be re-added.
60 *       - When a job fails and the client requests that it be retried with backoff.
61 *       - This class <strong>is not</strong> thread-safe.
62 *
63 * Note on locking:
64 *      All callers to this class must <strong>lock on the class object they are calling</strong>.
65 *      This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
66 *      and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
67 *      object.
68 */
69public class JobStore {
70    private static final String TAG = "JobStore";
71    private static final boolean DEBUG = JobSchedulerService.DEBUG;
72
73    /** Threshold to adjust how often we want to write to the db. */
74    private static final int MAX_OPS_BEFORE_WRITE = 1;
75    final ArraySet<JobStatus> mJobSet;
76    final Context mContext;
77
78    private int mDirtyOperations;
79
80    private static final Object sSingletonLock = new Object();
81    private final AtomicFile mJobsFile;
82    /** Handler backed by IoThread for writing to disk. */
83    private final Handler mIoHandler = IoThread.getHandler();
84    private static JobStore sSingleton;
85
86    /** Used by the {@link JobSchedulerService} to instantiate the JobStore. */
87    static JobStore initAndGet(JobSchedulerService jobManagerService) {
88        synchronized (sSingletonLock) {
89            if (sSingleton == null) {
90                sSingleton = new JobStore(jobManagerService.getContext(),
91                        Environment.getDataDirectory(), jobManagerService);
92            }
93            return sSingleton;
94        }
95    }
96
97    @VisibleForTesting
98    public static JobStore initAndGetForTesting(Context context, File dataDir,
99                                                 JobMapReadFinishedListener callback) {
100        return new JobStore(context, dataDir, callback);
101    }
102
103    private JobStore(Context context, File dataDir, JobMapReadFinishedListener callback) {
104        mContext = context;
105        mDirtyOperations = 0;
106
107        File systemDir = new File(dataDir, "system");
108        File jobDir = new File(systemDir, "job");
109        jobDir.mkdirs();
110        mJobsFile = new AtomicFile(new File(jobDir, "jobs.xml"));
111
112        mJobSet = new ArraySet<JobStatus>();
113
114        readJobMapFromDiskAsync(callback);
115    }
116
117    /**
118     * Add a job to the master list, persisting it if necessary. If the JobStatus already exists,
119     * it will be replaced.
120     * @param jobStatus Job to add.
121     * @return Whether or not an equivalent JobStatus was replaced by this operation.
122     */
123    public boolean add(JobStatus jobStatus) {
124        boolean replaced = mJobSet.remove(jobStatus);
125        mJobSet.add(jobStatus);
126        if (jobStatus.isPersisted()) {
127            maybeWriteStatusToDiskAsync();
128        }
129        if (DEBUG) {
130            Slog.d(TAG, "Added job status to store: " + jobStatus);
131        }
132        return replaced;
133    }
134
135    /**
136     * Whether this jobStatus object already exists in the JobStore.
137     */
138    public boolean containsJobIdForUid(int jobId, int uId) {
139        for (JobStatus ts : mJobSet) {
140            if (ts.getUid() == uId && ts.getJobId() == jobId) {
141                return true;
142            }
143        }
144        return false;
145    }
146
147    public int size() {
148        return mJobSet.size();
149    }
150
151    /**
152     * Remove the provided job. Will also delete the job if it was persisted.
153     * @return Whether or not the job existed to be removed.
154     */
155    public boolean remove(JobStatus jobStatus) {
156        boolean removed = mJobSet.remove(jobStatus);
157        if (!removed) {
158            if (DEBUG) {
159                Slog.d(TAG, "Couldn't remove job: didn't exist: " + jobStatus);
160            }
161            return false;
162        }
163        maybeWriteStatusToDiskAsync();
164        return removed;
165    }
166
167    @VisibleForTesting
168    public void clear() {
169        mJobSet.clear();
170        maybeWriteStatusToDiskAsync();
171    }
172
173    public List<JobStatus> getJobsByUser(int userHandle) {
174        List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
175        Iterator<JobStatus> it = mJobSet.iterator();
176        while (it.hasNext()) {
177            JobStatus ts = it.next();
178            if (UserHandle.getUserId(ts.getUid()) == userHandle) {
179                matchingJobs.add(ts);
180            }
181        }
182        return matchingJobs;
183    }
184
185    /**
186     * @param uid Uid of the requesting app.
187     * @return All JobStatus objects for a given uid from the master list.
188     */
189    public List<JobStatus> getJobsByUid(int uid) {
190        List<JobStatus> matchingJobs = new ArrayList<JobStatus>();
191        Iterator<JobStatus> it = mJobSet.iterator();
192        while (it.hasNext()) {
193            JobStatus ts = it.next();
194            if (ts.getUid() == uid) {
195                matchingJobs.add(ts);
196            }
197        }
198        return matchingJobs;
199    }
200
201    /**
202     * @param uid Uid of the requesting app.
203     * @param jobId Job id, specified at schedule-time.
204     * @return the JobStatus that matches the provided uId and jobId, or null if none found.
205     */
206    public JobStatus getJobByUidAndJobId(int uid, int jobId) {
207        Iterator<JobStatus> it = mJobSet.iterator();
208        while (it.hasNext()) {
209            JobStatus ts = it.next();
210            if (ts.getUid() == uid && ts.getJobId() == jobid) {
211                return ts;
212            }
213        }
214        return null;
215    }
216
217    /**
218     * @return The live array of JobStatus objects.
219     */
220    public ArraySet<JobStatus> getJobs() {
221        return mJobSet;
222    }
223
224    /** Version of the db schema. */
225    private static final int JOBS_FILE_VERSION = 0;
226    /** Tag corresponds to constraints this job needs. */
227    private static final String XML_TAG_PARAMS_CONSTRAINTS = "constraints";
228    /** Tag corresponds to execution parameters. */
229    private static final String XML_TAG_PERIODIC = "periodic";
230    private static final String XML_TAG_ONEOFF = "one-off";
231    private static final String XML_TAG_EXTRAS = "extras";
232
233    /**
234     * Every time the state changes we write all the jobs in one swath, instead of trying to
235     * track incremental changes.
236     * @return Whether the operation was successful. This will only fail for e.g. if the system is
237     * low on storage. If this happens, we continue as normal
238     */
239    private void maybeWriteStatusToDiskAsync() {
240        mDirtyOperations++;
241        if (mDirtyOperations >= MAX_OPS_BEFORE_WRITE) {
242            if (DEBUG) {
243                Slog.v(TAG, "Writing jobs to disk.");
244            }
245            mIoHandler.post(new WriteJobsMapToDiskRunnable());
246        }
247    }
248
249    private void readJobMapFromDiskAsync(JobMapReadFinishedListener callback) {
250        mIoHandler.post(new ReadJobMapFromDiskRunnable(callback));
251    }
252
253    public void readJobMapFromDisk(JobMapReadFinishedListener callback) {
254        new ReadJobMapFromDiskRunnable(callback).run();
255    }
256
257    /**
258     * Runnable that writes {@link #mJobSet} out to xml.
259     * NOTE: This Runnable locks on JobStore.this
260     */
261    private class WriteJobsMapToDiskRunnable implements Runnable {
262        @Override
263        public void run() {
264            final long startElapsed = SystemClock.elapsedRealtime();
265            synchronized (JobStore.this) {
266                writeJobsMapImpl();
267            }
268            if (JobSchedulerService.DEBUG) {
269                Slog.v(TAG, "Finished writing, took " + (SystemClock.elapsedRealtime()
270                        - startElapsed) + "ms");
271            }
272        }
273
274        private void writeJobsMapImpl() {
275            try {
276                ByteArrayOutputStream baos = new ByteArrayOutputStream();
277                XmlSerializer out = new FastXmlSerializer();
278                out.setOutput(baos, "utf-8");
279                out.startDocument(null, true);
280                out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
281
282                out.startTag(null, "job-info");
283                out.attribute(null, "version", Integer.toString(JOBS_FILE_VERSION));
284                for (int i = 0; i < mJobSet.size(); i++) {
285                    final JobStatus jobStatus = mJobSet.valueAt(i);
286                    if (DEBUG) {
287                        Slog.d(TAG, "Saving job " + jobStatus.getJobId());
288                    }
289                    out.startTag(null, "job");
290                    addIdentifierAttributesToJobTag(out, jobStatus);
291                    writeConstraintsToXml(out, jobStatus);
292                    writeExecutionCriteriaToXml(out, jobStatus);
293                    writeBundleToXml(jobStatus.getExtras(), out);
294                    out.endTag(null, "job");
295                }
296                out.endTag(null, "job-info");
297                out.endDocument();
298
299                // Write out to disk in one fell sweep.
300                FileOutputStream fos = mJobsFile.startWrite();
301                fos.write(baos.toByteArray());
302                mJobsFile.finishWrite(fos);
303                mDirtyOperations = 0;
304            } catch (IOException e) {
305                if (DEBUG) {
306                    Slog.v(TAG, "Error writing out job data.", e);
307                }
308            } catch (XmlPullParserException e) {
309                if (DEBUG) {
310                    Slog.d(TAG, "Error persisting bundle.", e);
311                }
312            }
313        }
314
315        /** Write out a tag with data comprising the required fields of this job and its client. */
316        private void addIdentifierAttributesToJobTag(XmlSerializer out, JobStatus jobStatus)
317                throws IOException {
318            out.attribute(null, "jobid", Integer.toString(jobStatus.getJobId()));
319            out.attribute(null, "package", jobStatus.getServiceComponent().getPackageName());
320            out.attribute(null, "class", jobStatus.getServiceComponent().getClassName());
321            out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
322        }
323
324        private void writeBundleToXml(PersistableBundle extras, XmlSerializer out)
325                throws IOException, XmlPullParserException {
326            out.startTag(null, XML_TAG_EXTRAS);
327            extras.saveToXml(out);
328            out.endTag(null, XML_TAG_EXTRAS);
329        }
330        /**
331         * Write out a tag with data identifying this job's constraints. If the constraint isn't here
332         * it doesn't apply.
333         */
334        private void writeConstraintsToXml(XmlSerializer out, JobStatus jobStatus) throws IOException {
335            out.startTag(null, XML_TAG_PARAMS_CONSTRAINTS);
336            if (jobStatus.hasUnmeteredConstraint()) {
337                out.attribute(null, "unmetered", Boolean.toString(true));
338            }
339            if (jobStatus.hasConnectivityConstraint()) {
340                out.attribute(null, "connectivity", Boolean.toString(true));
341            }
342            if (jobStatus.hasIdleConstraint()) {
343                out.attribute(null, "idle", Boolean.toString(true));
344            }
345            if (jobStatus.hasChargingConstraint()) {
346                out.attribute(null, "charging", Boolean.toString(true));
347            }
348            out.endTag(null, XML_TAG_PARAMS_CONSTRAINTS);
349        }
350
351        private void writeExecutionCriteriaToXml(XmlSerializer out, JobStatus jobStatus)
352                throws IOException {
353            final JobInfo job = jobStatus.getJob();
354            if (jobStatus.getJob().isPeriodic()) {
355                out.startTag(null, XML_TAG_PERIODIC);
356                out.attribute(null, "period", Long.toString(job.getIntervalMillis()));
357            } else {
358                out.startTag(null, XML_TAG_ONEOFF);
359            }
360
361            if (jobStatus.hasDeadlineConstraint()) {
362                // Wall clock deadline.
363                final long deadlineWallclock =  System.currentTimeMillis() +
364                        (jobStatus.getLatestRunTimeElapsed() - SystemClock.elapsedRealtime());
365                out.attribute(null, "deadline", Long.toString(deadlineWallclock));
366            }
367            if (jobStatus.hasTimingDelayConstraint()) {
368                final long delayWallclock = System.currentTimeMillis() +
369                        (jobStatus.getEarliestRunTime() - SystemClock.elapsedRealtime());
370                out.attribute(null, "delay", Long.toString(delayWallclock));
371            }
372
373            // Only write out back-off policy if it differs from the default.
374            // This also helps the case where the job is idle -> these aren't allowed to specify
375            // back-off.
376            if (jobStatus.getJob().getInitialBackoffMillis() != JobInfo.DEFAULT_INITIAL_BACKOFF_MILLIS
377                    || jobStatus.getJob().getBackoffPolicy() != JobInfo.DEFAULT_BACKOFF_POLICY) {
378                out.attribute(null, "backoff-policy", Integer.toString(job.getBackoffPolicy()));
379                out.attribute(null, "initial-backoff", Long.toString(job.getInitialBackoffMillis()));
380            }
381            if (job.isPeriodic()) {
382                out.endTag(null, XML_TAG_PERIODIC);
383            } else {
384                out.endTag(null, XML_TAG_ONEOFF);
385            }
386        }
387    }
388
389    /**
390     * Runnable that reads list of persisted job from xml.
391     * NOTE: This Runnable locks on JobStore.this
392     */
393    private class ReadJobMapFromDiskRunnable implements Runnable {
394        private JobMapReadFinishedListener mCallback;
395        public ReadJobMapFromDiskRunnable(JobMapReadFinishedListener callback) {
396            mCallback = callback;
397        }
398
399        @Override
400        public void run() {
401            try {
402                List<JobStatus> jobs;
403                FileInputStream fis = mJobsFile.openRead();
404                synchronized (JobStore.this) {
405                    jobs = readJobMapImpl(fis);
406                }
407                fis.close();
408                if (jobs != null) {
409                    mCallback.onJobMapReadFinished(jobs);
410                }
411            } catch (FileNotFoundException e) {
412                if (JobSchedulerService.DEBUG) {
413                    Slog.d(TAG, "Could not find jobs file, probably there was nothing to load.");
414                }
415            } catch (XmlPullParserException e) {
416                if (JobSchedulerService.DEBUG) {
417                    Slog.d(TAG, "Error parsing xml.", e);
418                }
419            } catch (IOException e) {
420                if (JobSchedulerService.DEBUG) {
421                    Slog.d(TAG, "Error parsing xml.", e);
422                }
423            }
424        }
425
426        private List<JobStatus> readJobMapImpl(FileInputStream fis) throws XmlPullParserException, IOException {
427            XmlPullParser parser = Xml.newPullParser();
428            parser.setInput(fis, null);
429
430            int eventType = parser.getEventType();
431            while (eventType != XmlPullParser.START_TAG &&
432                    eventType != XmlPullParser.END_DOCUMENT) {
433                eventType = parser.next();
434                Slog.d(TAG, parser.getName());
435            }
436            if (eventType == XmlPullParser.END_DOCUMENT) {
437                if (DEBUG) {
438                    Slog.d(TAG, "No persisted jobs.");
439                }
440                return null;
441            }
442
443            String tagName = parser.getName();
444            if ("job-info".equals(tagName)) {
445                final List<JobStatus> jobs = new ArrayList<JobStatus>();
446                // Read in version info.
447                try {
448                    int version = Integer.valueOf(parser.getAttributeValue(null, "version"));
449                    if (version != JOBS_FILE_VERSION) {
450                        Slog.d(TAG, "Invalid version number, aborting jobs file read.");
451                        return null;
452                    }
453                } catch (NumberFormatException e) {
454                    Slog.e(TAG, "Invalid version number, aborting jobs file read.");
455                    return null;
456                }
457                eventType = parser.next();
458                do {
459                    // Read each <job/>
460                    if (eventType == XmlPullParser.START_TAG) {
461                        tagName = parser.getName();
462                        // Start reading job.
463                        if ("job".equals(tagName)) {
464                            JobStatus persistedJob = restoreJobFromXml(parser);
465                            if (persistedJob != null) {
466                                if (DEBUG) {
467                                    Slog.d(TAG, "Read out " + persistedJob);
468                                }
469                                jobs.add(persistedJob);
470                            } else {
471                                Slog.d(TAG, "Error reading job from file.");
472                            }
473                        }
474                    }
475                    eventType = parser.next();
476                } while (eventType != XmlPullParser.END_DOCUMENT);
477                return jobs;
478            }
479            return null;
480        }
481
482        /**
483         * @param parser Xml parser at the beginning of a "<job/>" tag. The next "parser.next()" call
484         *               will take the parser into the body of the job tag.
485         * @return Newly instantiated job holding all the information we just read out of the xml tag.
486         */
487        private JobStatus restoreJobFromXml(XmlPullParser parser) throws XmlPullParserException,
488                IOException {
489            JobInfo.Builder jobBuilder;
490            int uid;
491
492            // Read out job identifier attributes.
493            try {
494                jobBuilder = buildBuilderFromXml(parser);
495                uid = Integer.valueOf(parser.getAttributeValue(null, "uid"));
496            } catch (NumberFormatException e) {
497                Slog.e(TAG, "Error parsing job's required fields, skipping");
498                return null;
499            }
500
501            int eventType;
502            // Read out constraints tag.
503            do {
504                eventType = parser.next();
505            } while (eventType == XmlPullParser.TEXT);  // Push through to next START_TAG.
506
507            if (!(eventType == XmlPullParser.START_TAG &&
508                    XML_TAG_PARAMS_CONSTRAINTS.equals(parser.getName()))) {
509                // Expecting a <constraints> start tag.
510                return null;
511            }
512            try {
513                buildConstraintsFromXml(jobBuilder, parser);
514            } catch (NumberFormatException e) {
515                Slog.d(TAG, "Error reading constraints, skipping.");
516                return null;
517            }
518            parser.next(); // Consume </constraints>
519
520            // Read out execution parameters tag.
521            do {
522                eventType = parser.next();
523            } while (eventType == XmlPullParser.TEXT);
524            if (eventType != XmlPullParser.START_TAG) {
525                return null;
526            }
527
528            Pair<Long, Long> runtimes;
529            try {
530                runtimes = buildExecutionTimesFromXml(parser);
531            } catch (NumberFormatException e) {
532                if (DEBUG) {
533                    Slog.d(TAG, "Error parsing execution time parameters, skipping.");
534                }
535                return null;
536            }
537
538            if (XML_TAG_PERIODIC.equals(parser.getName())) {
539                try {
540                    String val = parser.getAttributeValue(null, "period");
541                    jobBuilder.setPeriodic(Long.valueOf(val));
542                } catch (NumberFormatException e) {
543                    Slog.d(TAG, "Error reading periodic execution criteria, skipping.");
544                    return null;
545                }
546            } else if (XML_TAG_ONEOFF.equals(parser.getName())) {
547                try {
548                    if (runtimes.first != JobStatus.NO_EARLIEST_RUNTIME) {
549                        jobBuilder.setMinimumLatency(runtimes.first - SystemClock.elapsedRealtime());
550                    }
551                    if (runtimes.second != JobStatus.NO_LATEST_RUNTIME) {
552                        jobBuilder.setOverrideDeadline(
553                                runtimes.second - SystemClock.elapsedRealtime());
554                    }
555                } catch (NumberFormatException e) {
556                    Slog.d(TAG, "Error reading job execution criteria, skipping.");
557                    return null;
558                }
559            } else {
560                if (DEBUG) {
561                    Slog.d(TAG, "Invalid parameter tag, skipping - " + parser.getName());
562                }
563                // Expecting a parameters start tag.
564                return null;
565            }
566            maybeBuildBackoffPolicyFromXml(jobBuilder, parser);
567
568            parser.nextTag(); // Consume parameters end tag.
569
570            // Read out extras Bundle.
571            do {
572                eventType = parser.next();
573            } while (eventType == XmlPullParser.TEXT);
574            if (!(eventType == XmlPullParser.START_TAG && XML_TAG_EXTRAS.equals(parser.getName()))) {
575                if (DEBUG) {
576                    Slog.d(TAG, "Error reading extras, skipping.");
577                }
578                return null;
579            }
580
581            PersistableBundle extras = PersistableBundle.restoreFromXml(parser);
582            jobBuilder.setExtras(extras);
583            parser.nextTag(); // Consume </extras>
584
585            return new JobStatus(jobBuilder.build(), uid, runtimes.first, runtimes.second);
586        }
587
588        private JobInfo.Builder buildBuilderFromXml(XmlPullParser parser) throws NumberFormatException {
589            // Pull out required fields from <job> attributes.
590            int jobId = Integer.valueOf(parser.getAttributeValue(null, "jobid"));
591            String packageName = parser.getAttributeValue(null, "package");
592            String className = parser.getAttributeValue(null, "class");
593            ComponentName cname = new ComponentName(packageName, className);
594
595            return new JobInfo.Builder(jobId, cname);
596        }
597
598        private void buildConstraintsFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
599            String val = parser.getAttributeValue(null, "unmetered");
600            if (val != null) {
601                jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.UNMETERED);
602            }
603            val = parser.getAttributeValue(null, "connectivity");
604            if (val != null) {
605                jobBuilder.setRequiredNetworkCapabilities(JobInfo.NetworkType.ANY);
606            }
607            val = parser.getAttributeValue(null, "idle");
608            if (val != null) {
609                jobBuilder.setRequiresDeviceIdle(true);
610            }
611            val = parser.getAttributeValue(null, "charging");
612            if (val != null) {
613                jobBuilder.setRequiresCharging(true);
614            }
615        }
616
617        /**
618         * Builds the back-off policy out of the params tag. These attributes may not exist, depending
619         * on whether the back-off was set when the job was first scheduled.
620         */
621        private void maybeBuildBackoffPolicyFromXml(JobInfo.Builder jobBuilder, XmlPullParser parser) {
622            String val = parser.getAttributeValue(null, "initial-backoff");
623            if (val != null) {
624                long initialBackoff = Long.valueOf(val);
625                val = parser.getAttributeValue(null, "backoff-policy");
626                int backoffPolicy = Integer.valueOf(val);  // Will throw NFE which we catch higher up.
627                jobBuilder.setBackoffCriteria(initialBackoff, backoffPolicy);
628            }
629        }
630
631        /**
632         * Convenience function to read out and convert deadline and delay from xml into elapsed real
633         * time.
634         * @return A {@link android.util.Pair}, where the first value is the earliest elapsed runtime
635         * and the second is the latest elapsed runtime.
636         */
637        private Pair<Long, Long> buildExecutionTimesFromXml(XmlPullParser parser)
638                throws NumberFormatException {
639            // Pull out execution time data.
640            final long nowWallclock = System.currentTimeMillis();
641            final long nowElapsed = SystemClock.elapsedRealtime();
642
643            long earliestRunTimeElapsed = JobStatus.NO_EARLIEST_RUNTIME;
644            long latestRunTimeElapsed = JobStatus.NO_LATEST_RUNTIME;
645            String val = parser.getAttributeValue(null, "deadline");
646            if (val != null) {
647                long latestRuntimeWallclock = Long.valueOf(val);
648                long maxDelayElapsed =
649                        Math.max(latestRuntimeWallclock - nowWallclock, 0);
650                latestRunTimeElapsed = nowElapsed + maxDelayElapsed;
651            }
652            val = parser.getAttributeValue(null, "delay");
653            if (val != null) {
654                long earliestRuntimeWallclock = Long.valueOf(val);
655                long minDelayElapsed =
656                        Math.max(earliestRuntimeWallclock - nowWallclock, 0);
657                earliestRunTimeElapsed = nowElapsed + minDelayElapsed;
658
659            }
660            return Pair.create(earliestRunTimeElapsed, latestRunTimeElapsed);
661        }
662    }
663}