
com.path.android.jobqueue.JobManager Maven / Gradle / Ivy
package com.path.android.jobqueue;
import android.content.Context;
import com.path.android.jobqueue.cachedQueue.CachedJobQueue;
import com.path.android.jobqueue.config.Configuration;
import com.path.android.jobqueue.di.DependencyInjector;
import com.path.android.jobqueue.executor.JobConsumerExecutor;
import com.path.android.jobqueue.log.JqLog;
import com.path.android.jobqueue.network.NetworkEventProvider;
import com.path.android.jobqueue.network.NetworkUtil;
import com.path.android.jobqueue.nonPersistentQueue.NonPersistentPriorityQueue;
import com.path.android.jobqueue.persistentQueue.sqlite.SqliteJobQueue;
import java.util.concurrent.*;
/**
* a JobManager that supports;
* -> Persistent / Non Persistent Jobs
* -> Job Priority
* -> Running Jobs in Parallel
* -> Grouping jobs so that they won't run at the same time
* -> Stats like waiting Job Count
*/
public class JobManager implements NetworkEventProvider.Listener {
public static final long NS_PER_MS = 1000000;
public static final long NOT_RUNNING_SESSION_ID = Long.MIN_VALUE;
public static final long NOT_DELAYED_JOB_DELAY = Long.MIN_VALUE;
@SuppressWarnings("FieldCanBeLocal")//used for testing
private final long sessionId;
private boolean running;
private final Context appContext;
private final NetworkUtil networkUtil;
private final DependencyInjector dependencyInjector;
private final JobQueue persistentJobQueue;
private final JobQueue nonPersistentJobQueue;
private final CopyOnWriteGroupSet runningJobGroups;
private final JobConsumerExecutor jobConsumerExecutor;
private final Object newJobListeners = new Object();
private final ConcurrentHashMap persistentOnAddedLocks;
private final ConcurrentHashMap nonPersistentOnAddedLocks;
private final ScheduledExecutorService timedExecutor;
/**
* Default constructor that will create a JobManager with 1 {@link SqliteJobQueue} and 1 {@link NonPersistentPriorityQueue}
* @param context job manager will use applicationContext.
*/
public JobManager(Context context) {
this(context, "default");
}
/**
* Default constructor that will create a JobManager with a default {@link Configuration}
* @param context application context
* @param id an id that is unique to this JobManager
*/
public JobManager(Context context, String id) {
this(context, new Configuration.Builder(context).id(id).build());
}
/**
*
* @param context used to acquire ApplicationContext
* @param config
*/
public JobManager(Context context, Configuration config) {
if(config.getCustomLogger() != null) {
JqLog.setCustomLogger(config.getCustomLogger());
}
appContext = context.getApplicationContext();
running = true;
runningJobGroups = new CopyOnWriteGroupSet();
sessionId = System.nanoTime();
this.persistentJobQueue = config.getQueueFactory().createPersistentQueue(context, sessionId, config.getId());
this.nonPersistentJobQueue = config.getQueueFactory().createNonPersistent(context, sessionId, config.getId());
persistentOnAddedLocks = new ConcurrentHashMap();
nonPersistentOnAddedLocks = new ConcurrentHashMap();
networkUtil = config.getNetworkUtil();
dependencyInjector = config.getDependencyInjector();
if(networkUtil instanceof NetworkEventProvider) {
((NetworkEventProvider) networkUtil).setListener(this);
}
//is important to initialize consumers last so that they can start running
jobConsumerExecutor = new JobConsumerExecutor(config,consumerContract);
timedExecutor = Executors.newSingleThreadScheduledExecutor();
start();
}
/**
* Stops consuming jobs. Currently running jobs will be finished but no new jobs will be run.
*/
public void stop() {
running = false;
}
/**
* restarts the JobManager. Will create a new consumer if necessary.
*/
public void start() {
if(running) {
return;
}
running = true;
notifyJobConsumer();
}
/**
* returns the # of jobs that are waiting to be executed.
* This might be a good place to decide whether you should wake your app up on boot etc. to complete pending jobs.
* @return # of total jobs.
*/
public int count() {
int cnt = 0;
synchronized (nonPersistentJobQueue) {
cnt += nonPersistentJobQueue.count();
}
synchronized (persistentJobQueue) {
cnt += persistentJobQueue.count();
}
return cnt;
}
private int countReadyJobs(boolean hasNetwork) {
//TODO we can cache this
int total = 0;
synchronized (nonPersistentJobQueue) {
total += nonPersistentJobQueue.countReadyJobs(hasNetwork, runningJobGroups.getSafe());
}
synchronized (persistentJobQueue) {
total += persistentJobQueue.countReadyJobs(hasNetwork, runningJobGroups.getSafe());
}
return total;
}
/**
* Adds a new Job to the list and returns an ID for it.
* @param job to add
* @return id for the job.
*/
public long addJob(Job job) {
//noinspection deprecation
return addJob(job.getPriority(), job.getDelayInMs(), job);
}
/**
* Non-blocking convenience method to add a job in background thread.
* @see #addJob(Job)
* @param job job to add
*
*/
public void addJobInBackground(Job job) {
//noinspection deprecation
addJobInBackground(job.getPriority(), job.getDelayInMs(), job);
}
public void addJobInBackground(Job job, /*nullable*/ AsyncAddCallback callback) {
addJobInBackground(job.getPriority(), job.getDelayInMs(), job, callback);
}
//need to sync on related job queue before calling this
private void addOnAddedLock(ConcurrentHashMap lockMap, long id) {
lockMap.put(id, new CountDownLatch(1));
}
//need to sync on related job queue before calling this
private void waitForOnAddedLock(ConcurrentHashMap lockMap, long id) {
CountDownLatch latch = lockMap.get(id);
if(latch == null) {
return;
}
try {
latch.await();
} catch (InterruptedException e) {
JqLog.e(e, "could not wait for onAdded lock");
}
}
//need to sync on related job queue before calling this
private void clearOnAddedLock(ConcurrentHashMap lockMap, long id) {
CountDownLatch latch = lockMap.get(id);
if(latch != null) {
latch.countDown();
}
lockMap.remove(id);
}
/**
* checks next available job and returns when it will be available (if it will, otherwise returns {@link Long#MAX_VALUE})
* also creates a timer to notify listeners at that time
* @param hasNetwork .
* @return time wait until next job (in milliseconds)
*/
private long ensureConsumerWhenNeeded(Boolean hasNetwork) {
if(hasNetwork == null) {
//if network util can inform us when network is recovered, we we'll check only next job that does not
//require network. if it does not know how to inform us, we have to keep a busy loop.
//noinspection SimplifiableConditionalExpression
hasNetwork = networkUtil instanceof NetworkEventProvider ? hasNetwork() : true;
}
//this method is called when there are jobs but job consumer was not given any
//this may happen in a race condition or when the latest job is a delayed job
Long nextRunNs;
synchronized (nonPersistentJobQueue) {
nextRunNs = nonPersistentJobQueue.getNextJobDelayUntilNs(hasNetwork);
}
if(nextRunNs != null && nextRunNs <= System.nanoTime()) {
notifyJobConsumer();
return 0L;
}
Long persistedJobRunNs;
synchronized (persistentJobQueue) {
persistedJobRunNs = persistentJobQueue.getNextJobDelayUntilNs(hasNetwork);
}
if(persistedJobRunNs != null) {
if(nextRunNs == null) {
nextRunNs = persistedJobRunNs;
} else if(persistedJobRunNs < nextRunNs) {
nextRunNs = persistedJobRunNs;
}
}
if(nextRunNs != null) {
//to avoid overflow, we need to check equality first
if(nextRunNs < System.nanoTime()) {
notifyJobConsumer();
return 0L;
}
long diff = (long)Math.ceil((double)(nextRunNs - System.nanoTime()) / NS_PER_MS);
ensureConsumerOnTime(diff);
return diff;
}
return Long.MAX_VALUE;
}
private void notifyJobConsumer() {
synchronized (newJobListeners) {
newJobListeners.notifyAll();
}
jobConsumerExecutor.considerAddingConsumer();
}
private final Runnable notifyRunnable = new Runnable() {
@Override
public void run() {
notifyJobConsumer();
}
};
private void ensureConsumerOnTime(long waitMs) {
timedExecutor.schedule(notifyRunnable, waitMs, TimeUnit.MILLISECONDS);
}
private boolean hasNetwork() {
return networkUtil == null || networkUtil.isConnected(appContext);
}
private JobHolder getNextJob() {
boolean haveNetwork = hasNetwork();
JobHolder jobHolder;
boolean persistent = false;
synchronized (nonPersistentJobQueue) {
jobHolder = nonPersistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobGroups.getSafe());
}
if (jobHolder == null) {
//go to disk, there aren't any non-persistent jobs
synchronized (persistentJobQueue) {
jobHolder = persistentJobQueue.nextJobAndIncRunCount(haveNetwork, runningJobGroups.getSafe());
persistent = true;
}
}
if(jobHolder != null) {
//wait for onAdded locks
if(persistent) {
waitForOnAddedLock(persistentOnAddedLocks, jobHolder.getId());
} else {
waitForOnAddedLock(nonPersistentOnAddedLocks, jobHolder.getId());
}
}
if(persistent && jobHolder != null && dependencyInjector != null) {
dependencyInjector.inject(jobHolder.getBaseJob());
}
if(jobHolder != null && jobHolder.getGroupId() != null) {
runningJobGroups.add(jobHolder.getGroupId());
}
return jobHolder;
}
private void reAddJob(JobHolder jobHolder) {
JqLog.d("re-adding job %s", jobHolder.getId());
if (jobHolder.getBaseJob().isPersistent()) {
synchronized (persistentJobQueue) {
persistentJobQueue.insertOrReplace(jobHolder);
}
} else {
synchronized (nonPersistentJobQueue) {
nonPersistentJobQueue.insertOrReplace(jobHolder);
}
}
if(jobHolder.getGroupId() != null) {
runningJobGroups.remove(jobHolder.getGroupId());
}
}
/**
* Returns the current status of a {@link Job}.
*
* You should not call this method on the UI thread because it may make a db request.
*
*
* This is not a very fast call so try not to make it unless necessary. Consider using events if you need to be
* informed about a job's lifecycle.
*
* @param id the ID, returned by the addJob method
* @param isPersistent Jobs are added to different queues depending on if they are persistent or not. This is necessary
* because each queue has independent id sets.
* @return
*/
public JobStatus getJobStatus(long id, boolean isPersistent) {
if(jobConsumerExecutor.isRunning(id, isPersistent)) {
return JobStatus.RUNNING;
}
JobHolder holder;
if(isPersistent) {
synchronized (persistentJobQueue) {
holder = persistentJobQueue.findJobById(id);
}
} else {
synchronized (nonPersistentJobQueue) {
holder = nonPersistentJobQueue.findJobById(id);
}
}
if(holder == null) {
return JobStatus.UNKNOWN;
}
boolean network = hasNetwork();
if(holder.requiresNetwork() && !network) {
return JobStatus.WAITING_NOT_READY;
}
if(holder.getDelayUntilNs() > System.nanoTime()) {
return JobStatus.WAITING_NOT_READY;
}
return JobStatus.WAITING_READY;
}
private void removeJob(JobHolder jobHolder) {
if (jobHolder.getBaseJob().isPersistent()) {
synchronized (persistentJobQueue) {
persistentJobQueue.remove(jobHolder);
}
} else {
synchronized (nonPersistentJobQueue) {
nonPersistentJobQueue.remove(jobHolder);
}
}
if(jobHolder.getGroupId() != null) {
runningJobGroups.remove(jobHolder.getGroupId());
}
}
public synchronized void clear() {
synchronized (nonPersistentJobQueue) {
nonPersistentJobQueue.clear();
nonPersistentOnAddedLocks.clear();
}
synchronized (persistentJobQueue) {
persistentJobQueue.clear();
persistentOnAddedLocks.clear();
}
runningJobGroups.clear();
}
/**
* if {@link NetworkUtil} implements {@link NetworkEventProvider}, this method is called when network is recovered
* @param isConnected network connection state.
*/
@Override
public void onNetworkChange(boolean isConnected) {
ensureConsumerWhenNeeded(isConnected);
}
@SuppressWarnings("FieldCanBeLocal")
private final JobConsumerExecutor.Contract consumerContract = new JobConsumerExecutor.Contract() {
@Override
public boolean isRunning() {
return running;
}
@Override
public void insertOrReplace(JobHolder jobHolder) {
reAddJob(jobHolder);
}
@Override
public void removeJob(JobHolder jobHolder) {
JobManager.this.removeJob(jobHolder);
}
@Override
public JobHolder getNextJob(int wait, TimeUnit waitDuration) {
//be optimistic
JobHolder nextJob = JobManager.this.getNextJob();
if(nextJob != null) {
return nextJob;
}
long start = System.nanoTime();
long remainingWait = waitDuration.toNanos(wait);
long waitUntil = remainingWait + start;
//for delayed jobs,
long nextJobDelay = ensureConsumerWhenNeeded(null);
while (nextJob == null && waitUntil > System.nanoTime()) {
//keep running inside here to avoid busy loop
nextJob = running ? JobManager.this.getNextJob() : null;
if(nextJob == null) {
long remaining = waitUntil - System.nanoTime();
if(remaining > 0) {
//if we can't detect network changes, we won't be notified.
//to avoid waiting up to give time, wait in chunks of 500 ms max
long maxWait = Math.min(nextJobDelay, TimeUnit.NANOSECONDS.toMillis(remaining));
if(maxWait < 1) {
continue;//wait(0) will cause infinite wait.
}
if(networkUtil instanceof NetworkEventProvider) {
//to handle delayed jobs, make sure we trigger this first
//looks like there is no job available right now, wait for an event.
//there is a chance that if it triggers a timer and it gets called before I enter
//sync block, i am going to lose it
//TODO fix above case where we may wait unnecessarily long if a job is about to become available
synchronized (newJobListeners) {
try {
newJobListeners.wait(maxWait);
} catch (InterruptedException e) {
JqLog.e(e, "exception while waiting for a new job.");
}
}
} else {
//we cannot detect network changes. our best option is to wait for some time and try again
//then trigger {@link ensureConsumerWhenNeeded)
synchronized (newJobListeners) {
try {
newJobListeners.wait(Math.min(500, maxWait));
} catch (InterruptedException e) {
JqLog.e(e, "exception while waiting for a new job.");
}
}
}
}
}
}
return nextJob;
}
@Override
public int countRemainingReadyJobs() {
//if we can't detect network changes, assume we have network otherwise nothing will trigger a consumer
//noinspection SimplifiableConditionalExpression
return countReadyJobs(networkUtil instanceof NetworkEventProvider ? hasNetwork() : true);
}
};
/**
* Deprecated, please use {@link #addJob(Job)}.
*
* Adds a job with given priority and returns the JobId.
* @param priority Higher runs first
* @param baseJob The actual job to run
* @return job id
*/
@Deprecated
public long addJob(int priority, BaseJob baseJob) {
return addJob(priority, 0, baseJob);
}
/**
* Deprecated, please use {@link #addJob(Job)}.
*
* Adds a job with given priority and returns the JobId.
* @param priority Higher runs first
* @param delay number of milliseconds that this job should be delayed
* @param baseJob The actual job to run
* @return a job id. is useless for now but we'll use this to cancel jobs in the future.
*/
@Deprecated
public long addJob(int priority, long delay, BaseJob baseJob) {
JobHolder jobHolder = new JobHolder(priority, baseJob, delay > 0 ? System.nanoTime() + delay * NS_PER_MS : NOT_DELAYED_JOB_DELAY, NOT_RUNNING_SESSION_ID);
long id;
if (baseJob.isPersistent()) {
synchronized (persistentJobQueue) {
id = persistentJobQueue.insert(jobHolder);
addOnAddedLock(persistentOnAddedLocks, id);
}
} else {
synchronized (nonPersistentJobQueue) {
id = nonPersistentJobQueue.insert(jobHolder);
addOnAddedLock(nonPersistentOnAddedLocks, id);
}
}
if(JqLog.isDebugEnabled()) {
JqLog.d("added job id: %d class: %s priority: %d delay: %d group : %s persistent: %s requires network: %s"
, id, baseJob.getClass().getSimpleName(), priority, delay, baseJob.getRunGroupId()
, baseJob.isPersistent(), baseJob.requiresNetwork());
}
if(dependencyInjector != null) {
//inject members b4 calling onAdded
dependencyInjector.inject(baseJob);
}
jobHolder.getBaseJob().onAdded();
if(baseJob.isPersistent()) {
synchronized (persistentJobQueue) {
clearOnAddedLock(persistentOnAddedLocks, id);
}
} else {
synchronized (nonPersistentJobQueue) {
clearOnAddedLock(nonPersistentOnAddedLocks, id);
}
}
notifyJobConsumer();
return id;
}
/**
* Please use {@link #addJobInBackground(Job)}.
* Non-blocking convenience method to add a job in background thread.
*
* @see #addJob(int, BaseJob) addJob(priority, job).
*/
@Deprecated
public void addJobInBackground(final int priority, final BaseJob baseJob) {
timedExecutor.execute(new Runnable() {
@Override
public void run() {
addJob(priority, baseJob);
}
});
}
/**
* Deprecated, please use {@link #addJobInBackground(Job)}.
* Non-blocking convenience method to add a job in background thread.
* @see #addJob(int, long, BaseJob) addJob(priority, delay, job).
*/
@Deprecated
public void addJobInBackground(final int priority, final long delay, final BaseJob baseJob) {
addJobInBackground(priority, delay, baseJob, null);
}
protected void addJobInBackground(final int priority, final long delay, final BaseJob baseJob,
/*nullable*/final AsyncAddCallback callback) {
final long callTime = System.nanoTime();
timedExecutor.execute(new Runnable() {
@Override
public void run() {
final long runDelay = (System.nanoTime() - callTime) / NS_PER_MS;
long id = addJob(priority, Math.max(0, delay - runDelay), baseJob);
if(callback != null) {
callback.onAdded(id);
}
}
});
}
/**
* Default implementation of QueueFactory that creates one {@link SqliteJobQueue} and one {@link NonPersistentPriorityQueue}
* both are wrapped inside a {@link CachedJobQueue} to improve performance
*/
public static class DefaultQueueFactory implements QueueFactory {
SqliteJobQueue.JobSerializer jobSerializer;
public DefaultQueueFactory() {
jobSerializer = new SqliteJobQueue.JavaSerializer();
}
public DefaultQueueFactory(SqliteJobQueue.JobSerializer jobSerializer) {
this.jobSerializer = jobSerializer;
}
@Override
public JobQueue createPersistentQueue(Context context, Long sessionId, String id) {
return new CachedJobQueue(new SqliteJobQueue(context, sessionId, id, jobSerializer));
}
@Override
public JobQueue createNonPersistent(Context context, Long sessionId, String id) {
return new CachedJobQueue(new NonPersistentPriorityQueue(sessionId, id));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy