
com.path.android.jobqueue.executor.JobConsumerExecutor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of android-priority-jobqueue Show documentation
Show all versions of android-priority-jobqueue Show documentation
a Job Queue specifically written for Android to easily schedule jobs (tasks) that run in the background, improving UX and application stability.
package com.path.android.jobqueue.executor;
import com.path.android.jobqueue.JobHolder;
import com.path.android.jobqueue.JobManager;
import com.path.android.jobqueue.JobQueue;
import com.path.android.jobqueue.config.Configuration;
import com.path.android.jobqueue.log.JqLog;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* An executor class that takes care of spinning consumer threads and making sure enough is alive.
* works deeply coupled with {@link JobManager}
*/
public class JobConsumerExecutor {
private int maxConsumerSize;
private int minConsumerSize;
private int loadFactor;
private final ThreadGroup threadGroup;
private final Contract contract;
private final int keepAliveSeconds;
private final AtomicInteger activeConsumerCount = new AtomicInteger(0);
// key : id + (isPersistent)
private final ConcurrentHashMap runningJobHolders;
public JobConsumerExecutor(Configuration config, Contract contract) {
this.loadFactor = config.getLoadFactor();
this.maxConsumerSize = config.getMaxConsumerCount();
this.minConsumerSize = config.getMinConsumerCount();
this.keepAliveSeconds = config.getConsumerKeepAlive();
this.contract = contract;
threadGroup = new ThreadGroup("JobConsumers");
runningJobHolders = new ConcurrentHashMap();
}
/**
* creates a new consumer thread if needed.
*/
public void considerAddingConsumer() {
doINeedANewThread(false, true);
}
private boolean canIDie() {
if(doINeedANewThread(true, false) == false) {
return true;
}
return false;
}
private boolean doINeedANewThread(boolean inConsumerThread, boolean addIfNeeded) {
//if network provider cannot notify us, we have to busy wait
if(contract.isRunning() == false) {
if(inConsumerThread) {
activeConsumerCount.decrementAndGet();
}
return false;
}
synchronized (threadGroup) {
if(isAboveLoadFactor(inConsumerThread) && canAddMoreConsumers()) {
if(addIfNeeded) {
addConsumer();
}
return true;
}
}
if(inConsumerThread) {
activeConsumerCount.decrementAndGet();
}
return false;
}
private void addConsumer() {
JqLog.d("adding another consumer");
synchronized (threadGroup) {
Thread thread = new Thread(threadGroup, new JobConsumer(contract, this));
activeConsumerCount.incrementAndGet();
thread.start();
}
}
private boolean canAddMoreConsumers() {
synchronized (threadGroup) {
//there is a race condition for the time thread if about to finish
return activeConsumerCount.intValue() < maxConsumerSize;
}
}
private boolean isAboveLoadFactor(boolean inConsumerThread) {
synchronized (threadGroup) {
//if i am called from a consumer thread, don't count me
int consumerCnt = activeConsumerCount.intValue() - (inConsumerThread ? 1 : 0);
boolean res =
consumerCnt < minConsumerSize ||
consumerCnt * loadFactor < contract.countRemainingReadyJobs() + runningJobHolders.size();
if(JqLog.isDebugEnabled()) {
JqLog.d("%s: load factor check. %s = (%d < %d)|| (%d * %d < %d + %d). consumer thread: %s", Thread.currentThread().getName(), res,
consumerCnt, minConsumerSize,
consumerCnt, loadFactor, contract.countRemainingReadyJobs(), runningJobHolders.size(), inConsumerThread);
}
return res;
}
}
private void onBeforeRun(JobHolder jobHolder) {
runningJobHolders.put(createRunningJobHolderKey(jobHolder), jobHolder);
}
private void onAfterRun(JobHolder jobHolder) {
runningJobHolders.remove(createRunningJobHolderKey(jobHolder));
}
private String createRunningJobHolderKey(JobHolder jobHolder) {
return createRunningJobHolderKey(jobHolder.getId(), jobHolder.getBaseJob().isPersistent());
}
private String createRunningJobHolderKey(long id, boolean isPersistent) {
return id + "_" + (isPersistent ? "t" : "f");
}
/**
* returns true if job is currently handled by one of the executor threads
* @param id id of the job
* @param persistent boolean flag to distinguish id conflicts
* @return true if job is currently handled here
*/
public boolean isRunning(long id, boolean persistent) {
return runningJobHolders.containsKey(createRunningJobHolderKey(id, persistent));
}
/**
* contract between the {@link JobManager} and {@link JobConsumerExecutor}
*/
public static interface Contract {
/**
* @return if {@link JobManager} is currently running.
*/
public boolean isRunning();
/**
* should insert the given {@link JobHolder} to related {@link JobQueue}. if it already exists, should replace the
* existing one.
* @param jobHolder
*/
public void insertOrReplace(JobHolder jobHolder);
/**
* should remove the job from the related {@link JobQueue}
* @param jobHolder
*/
public void removeJob(JobHolder jobHolder);
/**
* should return the next job which is available to be run.
* @param wait
* @param waitUnit
* @return next job to execute or null if no jobs are available
*/
public JobHolder getNextJob(int wait, TimeUnit waitUnit);
/**
* @return the number of Jobs that are ready to be run
*/
public int countRemainingReadyJobs();
}
/**
* a simple {@link Runnable} that can take jobs from the {@link Contract} and execute them
*/
private static class JobConsumer implements Runnable {
private final Contract contract;
private final JobConsumerExecutor executor;
private boolean didRunOnce = false;
public JobConsumer(Contract contract, JobConsumerExecutor executor) {
this.executor = executor;
this.contract = contract;
}
@Override
public void run() {
boolean canDie;
do {
try {
if(JqLog.isDebugEnabled()) {
if(didRunOnce == false) {
JqLog.d("starting consumer %s", Thread.currentThread().getName());
didRunOnce = true;
} else {
JqLog.d("re-running consumer %s", Thread.currentThread().getName());
}
}
JobHolder nextJob;
do {
nextJob = contract.isRunning() ? contract.getNextJob(executor.keepAliveSeconds, TimeUnit.SECONDS) : null;
if (nextJob != null) {
executor.onBeforeRun(nextJob);
if (nextJob.safeRun(nextJob.getRunCount())) {
contract.removeJob(nextJob);
} else {
contract.insertOrReplace(nextJob);
}
executor.onAfterRun(nextJob);
}
} while (nextJob != null);
} finally {
//to avoid creating a new thread for no reason, consider not killing this one first
canDie = executor.canIDie();
if(JqLog.isDebugEnabled()) {
if(canDie) {
JqLog.d("finishing consumer %s", Thread.currentThread().getName());
} else {
JqLog.d("didn't allow me to die, re-running %s", Thread.currentThread().getName());
}
}
}
} while (!canDie);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy