package com.asteria.service;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy;
import java.util.concurrent.TimeUnit;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
/**
* The class that manages an internal queue of {@link Service}s within a
* {@link ScheduledExecutorService}. The primary use for this class is to make
* executing scheduled asynchronous tasks more manageable.
* <p>
* <p>
* Please keep in mind that even though the management of services is
* synchronized, that the code within the services must be thread safe or race
* conditions and other concurrency issues can occur.
*
* @author lare96 <http://github.org/lare96>
*/
public final class ServiceQueue {
/**
* The executor service that will allow us to execute various
* {@link Service}s asynchronously.
*/
private final ScheduledExecutorService executor;
/**
* Creates a new {@link ServiceQueue}.
*
* @param executor
* the executor to run this service queue with.
*/
public ServiceQueue(ScheduledExecutorService executor) {
this.executor = executor;
}
/**
* Creates a new {@link ServiceQueue} with a timeout value of
* {@code seconds}.
*/
public ServiceQueue(long seconds) {
this(createServiceExecutor(seconds));
}
/**
* Creates a new {@link ServiceQueue} with a timeout value of {@code 0},
* meaning this service queue will never go idle.
*/
public ServiceQueue() {
this(createServiceExecutor(0));
}
/**
* Submits {@code service} to this service queue to be executed by the
* internal executor either as a scheduled or direct service. A direct
* service has a delay of {@code 0} and a scheduled has a delay of above
* that.
*
* @param service
* the service to submit to this service queue.
*/
public void submit(Service service) {
Preconditions.checkState(!executor.isShutdown(), "Cannot submit services to a ServiceQueue that has been shutdown.");
Preconditions.checkArgument(!service.isDone(), "Cannot submit cancelled services to this ServiceQueue.");
if (service.getDelay() == 0) {
submitDirect(service);
} else {
submitScheduled(service);
}
}
/**
* Submits a scheduled {@code service} to this service queue to be executed
* by the internal executor.
*
* @param service
* the service to submit to this service queue.
*/
private void submitScheduled(Service service) {
service.setFuture(executor.scheduleAtFixedRate(() -> {
try {
service.execute(this);
} catch (Throwable t) {
t.printStackTrace();
}
}, service.getDelay(), service.getDelay(), TimeUnit.MILLISECONDS));
}
/**
* Submits a direct {@code service} to this service queue to be executed by
* the internal executor.
*
* @param service
* the service to submit to this service queue.
*/
private void submitDirect(Service service) {
service.setFuture(executor.submit(() -> {
try {
service.execute(this);
service.cancel();
} catch (Throwable t) {
t.printStackTrace();
}
}));
}
/**
* Attempts to gracefully terminate this service queue. After this function
* returns, this service queue will not be able to be used.
* <p>
* <p>
* Please note that this function does indeed block the underlying thread
* until {@code 5} seconds pass or all services complete.
*/
public void terminate() {
try {
executor.shutdownNow();
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
}
/**
* Creates and configures a new {@link ScheduledExecutorService} with a
* timeout value of {@code seconds}. If the timeout value is below or equal
* to zero then the returned executor will never timeout.
*
* @return the newly created and configured executor service.
*/
private static ScheduledExecutorService createServiceExecutor(long seconds) {
Preconditions.checkArgument(seconds >= 0, "The timeout value must be equal to or greater than 0!");
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
executor.setThreadFactory(new ThreadFactoryBuilder().setNameFormat("ServiceQueueThread").build());
if (seconds > 0) {
executor.setKeepAliveTime(seconds, TimeUnit.SECONDS);
executor.allowCoreThreadTimeOut(true);
}
return executor;
}
}