package com.mongodb.hvdf.async;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoClientURI;
import com.mongodb.MongoException;
import com.mongodb.hvdf.MongoBackedService;
import com.mongodb.hvdf.api.ServiceException;
import com.mongodb.hvdf.configuration.AsyncServiceConfiguration;
import com.mongodb.hvdf.services.AsyncService;
import com.mongodb.hvdf.services.ServiceImplementation;
import com.yammer.dropwizard.config.Configuration;
@ServiceImplementation(
name = "DefaultAsyncService",
dependencies = { },
configClass = AsyncServiceConfiguration.class)
public class DefaultAsyncService
extends MongoBackedService implements AsyncService{
private static Logger logger = LoggerFactory.getLogger(DefaultAsyncService.class);
private AsyncTaskExecutor executor;
private final DBCollection recoveryColl;
private final AsyncServiceConfiguration config;
private final String signature;
private Timer recoveryTimer;
// Worker registration
private short workerTaskTypeId;
private AsyncWorker worker;
private volatile boolean shutdown = false;
public DefaultAsyncService(final MongoClientURI dbUri, final AsyncServiceConfiguration config){
super(dbUri, config);
this.config = config;
this.recoveryColl = this.database.getCollection(config.recovery_collection_name);
this.recoveryColl.createIndex(
new BasicDBObject(RecoveryRecord.STATE_KEY, 1));
// Get the identifier for this processor instance
if(config.service_signature.isEmpty() == true){
signature = ManagementFactory.getRuntimeMXBean().getName();
} else {
signature = config.service_signature;
}
// If the processing thread pool is configured, create an executor
final int threadCount = config.processing_thread_pool_size;
if(threadCount > 0){
createExecutor(threadCount);
} else {
this.executor = null;
}
// If recovery is enabled, set the recovery timer
if(this.executor != null && config.recovery_poll_time >= 0){
this.recoveryTimer = new Timer();
this.recoveryTimer.schedule(
new RecoveryTimerTask(this),
config.recovery_poll_time);
} else {
this.recoveryTimer = null;
}
}
// from TimerTask
public void recoverTasks() {
synchronized(recoveryTimer){
if(this.worker != null && !this.shutdown){
// While there are tasks to process, then keep doing them
RecoveryRecord recoveredTask = getTaskToRecover();
while(recoveredTask != null){
this.executor.execute(new AsyncReplayTask(this.worker, recoveredTask));
if(this.shutdown)
recoveredTask = null;
else
recoveredTask = getTaskToRecover();
}
}
try{
this.recoveryTimer.schedule(
new RecoveryTimerTask(this),
config.recovery_poll_time);
} catch(Exception e) {/* ignore due to cancelled timer */}
}
}
private RecoveryRecord getTaskToRecover() {
DBObject document = null;
// If configured to recover failed tasks, prefer these
if(this.config.failure_recovery_timeout > 0){
// Grab a qualifying task and update to processing
document = this.recoveryColl.findAndModify(
RecoveryRecord.findTimedOut(
this.workerTaskTypeId,
this.config.failure_recovery_timeout),
RecoveryRecord.oldestFirst(),
RecoveryRecord.updateAsProcessing(this.signature));
}
// If no timed out task is found, look for an available
if(document == null){
document = this.recoveryColl.findAndModify(
RecoveryRecord.findEligible(
this.workerTaskTypeId,
this.config.max_task_failures),
RecoveryRecord.oldestFirst(),
RecoveryRecord.updateAsProcessing(this.signature));
}
else{
logger.warn("Recovering timed out task : {}", document);
}
if(document != null){
return new RecoveryRecord(document);
}
return null;
}
@Override
public void submitTask(RecoverableAsyncTask task) {
// Get the recovery
RecoveryRecord record = task.getRecoveryRecord();
// If we intend to process, write the recovery record
if(isProcessingLocally()){
record.markAsProcessing(this.signature);
}
try{
// needs to be journaled
this.recoveryColl.insert(record.toDBObject());
}
catch(MongoException ex){
// If we failed to write at all, this is a complete
// failure, we need to flow this out to the caller
// or try to do the post synchronously
throw ServiceException.wrap(ex, AsyncError.CANNOT_WRITE_RECOVERY_RECORD);
}
// If this is a processor instance, post it to the executor
if(isProcessingLocally()){
this.executor.execute(task);
}
}
@Override
public void taskRejected(RecoverableAsyncTask task) {
// These are the only types of tasks
RecoverableAsyncTask asyncTask = (RecoverableAsyncTask)task;
if(this.config.persist_rejected_tasks == true
&& asyncTask.synchronousOnReject() == false){
// Need to flag it as available for processing
RecoveryRecord record = asyncTask.getRecoveryRecord();
this.recoveryColl.update(
RecoveryRecord.findById(record),
RecoveryRecord.updateAsAvailable());
} else {
// if not queuing, then run this task synchronously
// which will block the original submitter
Throwable taskException = null;
try{
asyncTask.run();
} catch(Exception e) {
taskException = e;
}
executor.afterExecute(asyncTask, taskException);
}
}
@Override
public void taskComplete(RecoverableAsyncTask task) {
RecoveryRecord record = task.getRecoveryRecord();
// The processor has completed a task, so it needs to be
// cleared in the task collection
this.recoveryColl.remove(RecoveryRecord.findById(record));
}
@Override
public void taskFailed(RecoverableAsyncTask task, Throwable cause) {
RecoveryRecord record = task.getRecoveryRecord();
// The worker failed a task, attempt to write/record the failure
// Mark it as failed and increment a counter.
this.recoveryColl.update(
RecoveryRecord.findById(record),
RecoveryRecord.updateAsFailed());
}
@Override
public void registerRecoveryService(
AsyncTaskType taskTypeId, AsyncWorker worker) {
// At this time we only have a single task type
// TODO : will need to be a map when multiple types are supported
this.workerTaskTypeId = taskTypeId.id();
this.worker = worker;
}
@Override
public Configuration getConfiguration() {
return this.config;
}
@Override
public void shutdown(long timeout, TimeUnit unit) {
logger.debug("Shutting down async service [{}]....", timeout);
// Cancel any impending recovery task
if(this.recoveryTimer != null){
this.shutdown = true;
this.recoveryTimer.cancel();
synchronized(recoveryTimer){}
}
// Shut down the task executor if one exists
if(this.executor != null){
this.executor.shutdown();
try {
this.executor.awaitTermination(timeout, unit);
} catch (InterruptedException e) {
logger.warn("Wait for shutdown interrupted, shutting down now !");
List<Runnable> incompleteList = this.executor.shutdownNow();
for(Runnable incompleteItem : incompleteList){
taskRejected((RecoverableAsyncTask) incompleteItem);
}
}
}
// Call through for cleanup on MongoDB connection
super.shutdown(timeout, unit);
}
private void createExecutor(int threadCount) {
// Create the right queue type for the configured size
final int maxQueueSize = config.async_tasks_max_queue_size;
BlockingQueue<Runnable> taskQueue = null;
if(maxQueueSize <= 0){
taskQueue = new SynchronousQueue<Runnable>();
} else {
taskQueue = new ArrayBlockingQueue<Runnable>(maxQueueSize);
}
// Setup the thread pool executor
this.executor = new AsyncTaskExecutor(this,
threadCount, threadCount,
(BlockingQueue<Runnable>) taskQueue);
}
private boolean isProcessingLocally() {
return this.executor != null;
}
}