package fr.openwide.core.jpa.more.business.task.service.impl;
import static fr.openwide.core.jpa.more.property.JpaMoreTaskPropertyIds.*;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.RateLimiter;
import fr.openwide.core.jpa.more.business.task.model.AbstractTask;
import fr.openwide.core.jpa.more.business.task.model.QueuedTaskHolder;
import fr.openwide.core.jpa.more.business.task.service.IQueuedTaskHolderService;
import fr.openwide.core.jpa.more.config.spring.AbstractTaskManagementConfig;
import fr.openwide.core.jpa.more.rendering.service.IRendererService;
import fr.openwide.core.jpa.util.EntityManagerUtils;
import fr.openwide.core.spring.property.service.IPropertyService;
import fr.openwide.core.spring.util.SpringBeanUtils;
public final class TaskConsumer {
private static final Logger LOGGER = LoggerFactory.getLogger(TaskConsumer.class);
private static final String THREAD_NAME_FORMAT = "TaskConsumer-%1$s-%2$s";
private static final long MAX_STOP_TIMEOUT_WAIT_INCREMENT_MS = 100L;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private IQueuedTaskHolderService queuedTaskHolderService;
@Autowired
@Qualifier(AbstractTaskManagementConfig.OBJECT_MAPPER_BEAN_NAME)
private ObjectMapper queuedTaskHolderObjectMapper;
@Autowired
private EntityManagerUtils entityManagerUtils;
@Autowired
private IRendererService rendererService;
@Autowired
private IPropertyService propertyService;
/**
* Used so that consume / switch to working is an atomic state
*
* Without this lock, working may be false between the moment element is taken from the queue and
* working is changed whereas it is known that there will be work to be done even if queue is empty.
*/
private final Object workingLock = new Object();
private final TaskQueue queue;
private final int threadIdForThisQueue;
private ConsumerThread thread;
public TaskConsumer(TaskQueue queue, int threadIdForThisQueue) {
super();
Assert.notNull(queue);
this.queue = queue;
this.threadIdForThisQueue = threadIdForThisQueue;
}
public void start() {
start(0L);
}
/**
* @param startDelay A length of time the consumer thread will wait before its first access to the task queue.
*/
public synchronized void start(long startDelay) {
if (thread == null || !thread.isAlive()) { // synchronized access
thread = new ConsumerThread(
String.format(THREAD_NAME_FORMAT, queue.getId(), threadIdForThisQueue),
startDelay
);
thread.start();
}
}
public synchronized void stop(long stopTimeout) {
if (thread != null) { // synchronized access
thread.stop(stopTimeout);
thread = null;
}
}
public boolean isWorking() {
return thread != null && thread.isWorking();
}
public TaskQueue getQueue() {
return queue;
}
/**
* A consumer thread with the ability to try stopping gracefully.
* @see #stop(int)
*/
private class ConsumerThread extends Thread {
/**
* A flag indicating whether new tasks should be consumed.
*/
private volatile boolean active = false;
/**
* A flag indicating whether the thread is currently executing a task.
* <p>Differs from <code>isAlive()</code> in that <code>isAlive()</code> returns true even if the thread is
* only waiting for a task to be offered in the queue.
*/
private volatile boolean working = false;
private final long startDelay;
/**
* task consumption take some time, so rateLimiter only limit rate when there is no task to consume.
*/
private final RateLimiter rateLimiter = RateLimiter.create(0.5);
public ConsumerThread(String name, long startDelay) {
super(name);
this.startDelay = startDelay;
}
@Override
public synchronized void start() {
active = true;
try {
super.start();
} catch (RuntimeException e) {
active = false;
throw e;
}
}
public void stop(long stopTimeout) {
/*
* Signal the run() method that it should not consume any more tasks.
*/
active = false;
try {
/*
* If a task is currently being handled, we wait for it to complete within the given time limit.
*/
long timeRemaining = stopTimeout;
while (timeRemaining > 0 && isWorking()) {
/*
* Wait for small durations of time, so that we'll stop waiting as
* soon as the consumer thread stops even if the timeout is huge.
*/
long step = Longs.min(MAX_STOP_TIMEOUT_WAIT_INCREMENT_MS, timeRemaining);
Thread.sleep(step); // NOSONAR findbugs:SWL_SLEEP_WITH_LOCK_HELD
/*
* Sleep in synchronized method does not harm because there is no
* high concurrency on this method (we simply don't want concurrent
* execution)
*/
timeRemaining -= step;
}
} catch (InterruptedException e) {
/*
* The current thread (the one waiting for the consumer thread) was interrupted
* Just put back the interrupt marker on the current thread before we interrupt the consumer thread
*/
Thread.currentThread().interrupt();
} finally {
/*
* If the current thread (the one waiting for the consumer thread) was interrupted, or if the timeout
* was reached when waiting for the task to complete, or if the task completed in time, we order the
* consumer thread to stop ASAP.
*/
this.interrupt();
}
}
public boolean isWorking() {
synchronized (workingLock) {
return working;
}
}
@Override
public void run() {
try {
if (startDelay > 0) {
Thread.sleep(startDelay);
}
/*
* Before starting tasks consumption we check that required execution context can be opened if needed
*/
if (propertyService.get(queueStartExecutionContextWaitReady(queue.getId()))) {
while (!rendererService.context().isReady()) {
Thread.sleep(5000l);
}
}
/*
* condition: permits thread to finish gracefully (stop was
* signaled, last taken element had been consumed, we can
* stop without any other action)
*/
while (active && !Thread.currentThread().isInterrupted()) {
Long queuedTaskHolderId;
// if there are tasks to consume, rateLimiter is not limiting due to task consumption's duration
// this allow to have a chance to
rateLimiter.acquire();
try {
synchronized (workingLock) {
queuedTaskHolderId = queue.poll(100, TimeUnit.MILLISECONDS);
if (queuedTaskHolderId != null) {
this.working = true;
}
}
if (queuedTaskHolderId != null) {
entityManagerUtils.openEntityManager();
try {
tryConsumeTask(queuedTaskHolderId);
} finally {
entityManagerUtils.closeEntityManager();
}
}
} finally {
this.working = false;
}
}
} catch (InterruptedException ignored) {
// Do nothing, just stop taking tasks
}
}
/**
* TOTALLY safe, NEVER throws any exception.
*/
private void tryConsumeTask(Long queuedTaskHolderId) {
QueuedTaskHolder queuedTaskHolder;
try {
queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId);
} catch (RuntimeException e) {
LOGGER.error("Error while trying to fetch a task from database before run (holder: " + queuedTaskHolderId + ").", e);
return;
}
AbstractTask runnableTask;
try {
runnableTask = queuedTaskHolderObjectMapper.readValue(queuedTaskHolder.getSerializedTask(), AbstractTask.class);
} catch (RuntimeException | IOException e) {
LOGGER.error("Error while trying to deserialize a task before run (holder: " + queuedTaskHolder + ").", e);
return;
}
try {
runnableTask.setQueuedTaskHolderId(queuedTaskHolder.getId());
SpringBeanUtils.autowireBean(applicationContext, runnableTask);
} catch (RuntimeException e) {
LOGGER.error("Error while trying to initialize a task before run (holder: " + queuedTaskHolder + ").", e);
return;
}
try {
runnableTask.run();
} catch (RuntimeException e) {
LOGGER.error("Error while trying to consume a task (holder: " + queuedTaskHolder + "); the task holder was probably left in a stale state.", e);
return;
}
}
}
}