package fr.openwide.core.jpa.more.business.task.service; import static fr.openwide.core.jpa.more.property.JpaMoreTaskPropertyIds.START_MODE; import static fr.openwide.core.jpa.more.property.JpaMoreTaskPropertyIds.STOP_TIMEOUT; import static fr.openwide.core.jpa.more.property.JpaMoreTaskPropertyIds.*; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.PreDestroy; import javax.annotation.Resource; 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.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.util.Assert; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Supplier; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import fr.openwide.core.jpa.exception.SecurityServiceException; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.jpa.more.business.task.model.AbstractTask; import fr.openwide.core.jpa.more.business.task.model.IQueueId; import fr.openwide.core.jpa.more.business.task.model.QueuedTaskHolder; import fr.openwide.core.jpa.more.business.task.service.impl.TaskConsumer; import fr.openwide.core.jpa.more.business.task.service.impl.TaskQueue; import fr.openwide.core.jpa.more.business.task.util.TaskStatus; import fr.openwide.core.jpa.more.config.spring.AbstractTaskManagementConfig; import fr.openwide.core.jpa.more.util.transaction.model.ITransactionSynchronizationAfterCommitTask; import fr.openwide.core.jpa.more.util.transaction.service.ITransactionSynchronizationTaskManagerService; import fr.openwide.core.spring.config.util.TaskQueueStartMode; import fr.openwide.core.spring.property.service.IPropertyService; import fr.openwide.core.spring.util.SpringBeanUtils; public class QueuedTaskHolderManagerImpl implements IQueuedTaskHolderManager, ApplicationListener<ContextRefreshedEvent> { private static final Logger LOGGER = LoggerFactory.getLogger(QueuedTaskHolderManagerImpl.class); @Autowired private ApplicationContext applicationContext; @Autowired private IQueuedTaskHolderService queuedTaskHolderService; @Autowired @Qualifier(AbstractTaskManagementConfig.OBJECT_MAPPER_BEAN_NAME) private ObjectMapper queuedTaskHolderObjectMapper; @Autowired private IPropertyService propertyService; @Autowired private ITransactionSynchronizationTaskManagerService synchronizationManager; @Resource private Collection<? extends IQueueId> queueIds; private int stopTimeout; private final Multimap<TaskQueue, TaskConsumer> consumersByQueue = Multimaps.newListMultimap(new HashMap<TaskQueue, Collection<TaskConsumer>>(), new Supplier<List<TaskConsumer>>() { @Override public List<TaskConsumer> get() { return Lists.newArrayList(); } }); private final Map<String, TaskQueue> queuesById = Maps.newHashMap(); private TaskQueue defaultQueue; private AtomicBoolean active = new AtomicBoolean(false); private AtomicBoolean availableForAction = new AtomicBoolean(true); @Override public void onApplicationEvent(ContextRefreshedEvent event) { /* * onApplicationEvent is called for every context initialization, including potential child contexts. * We avoid starting queues multiple times, by calling init() only on the root application context initialization. */ if (event != null && event.getSource() != null && AbstractApplicationContext.class.isAssignableFrom(event.getSource().getClass()) && ((AbstractApplicationContext) event.getSource()).getParent() == null) { init(); } } private void init() { stopTimeout = propertyService.get(STOP_TIMEOUT); initQueues(); if (TaskQueueStartMode.auto.equals(propertyService.get(START_MODE))) { start(); } else { LOGGER.warn("Task queue start configured in \"manual\" mode."); } } private final void initQueues() { Collection<String> queueIdsAsStrings = Lists.newArrayList(); // Sanity checks for (IQueueId queueId : queueIds) { String queueIdAsString = queueId.getUniqueStringId(); Assert.state(!IQueueId.RESERVED_DEFAULT_QUEUE_ID_STRING.equals(queueIdAsString), "Queue ID '" + IQueueId.RESERVED_DEFAULT_QUEUE_ID_STRING + "' is reserved for implementation purposes. Please choose another ID."); Assert.state(!queueIdsAsStrings.contains(queueIdAsString), "Queue ID '" + queueIdAsString + "' was defined more than once. Queue IDs must be unique."); queueIdsAsStrings.add(queueIdAsString); } defaultQueue = initQueue(IQueueId.RESERVED_DEFAULT_QUEUE_ID_STRING, 1); for (String queueIdAsString : queueIdsAsStrings) { int numberOfThreads = propertyService.get(queueNumberOfThreads(queueIdAsString)); if (numberOfThreads < 1) { LOGGER.warn("Number of threads for queue '{}' is set to an invalid value ({}); defaulting to 1", queueIdAsString, numberOfThreads); numberOfThreads = 1; } initQueue(queueIdAsString, numberOfThreads); } } private TaskQueue initQueue(String queueId, int numberOfThreads) { TaskQueue queue = new TaskQueue(queueId); for (int i = 0 ; i < numberOfThreads ; ++i) { TaskConsumer consumer = new TaskConsumer(queue, i); SpringBeanUtils.autowireBean(applicationContext, consumer); consumersByQueue.put(queue, consumer); } queuesById.put(queueId, queue); return queue; } private String selectQueue(AbstractTask task) { SpringBeanUtils.autowireBean(applicationContext, task); IQueueId queueId = task.selectQueue(); String queueIdString = queueId == null ? null : queueId.getUniqueStringId(); return queueIdString; } private TaskQueue getQueue(String queueId) { if (queueId == null) { return defaultQueue; } TaskQueue queue = queuesById.get(queueId); if (queue == null) { LOGGER.warn("Queue ID '{}' is unknown ; falling back to default queue", queueId); queue = defaultQueue; } return queue; } @Override public boolean isAvailableForAction() { return availableForAction.get(); } @Override public boolean isActive() { return active.get(); } @Override public int getNumberOfWaitingTasks() { int total = 0; for (TaskQueue queue : queuesById.values()) { total += queue.size(); } return total; } @Override public int getNumberOfRunningTasks() { int total = 0; for (TaskQueue queue : queuesById.values()) { Collection<TaskConsumer> consumers = consumersByQueue.get(queue); for (TaskConsumer consumer : consumers) { if (consumer.isWorking()) { total++; } } } return total; } @Override public Collection<IQueueId> getQueueIds() { return Collections.unmodifiableCollection(queueIds); } @Override public void start() { startConsumers(); } protected synchronized void startConsumers() { if (!active.get()) { availableForAction.set(false); try { initQueuesFromDatabase(); for (TaskConsumer consumer : consumersByQueue.values()) { Long configurationStartDelay = getStartDelay(consumer.getQueue().getId()); consumer.start(configurationStartDelay); } } finally { active.set(true); availableForAction.set(true); } } } /** * The length of time the consumer threads will wait before their first access to their task queue. */ protected Long getStartDelay(String queueId) { return propertyService.get(queueStartDelay(queueId)); } private void initQueuesFromDatabase() { for (TaskQueue queue : queuesById.values()) { try { List<Long> taskIds = queuedTaskHolderService.initializeTasksAndListConsumable(queue.getId()); if (queue == defaultQueue) { taskIds.addAll(queuedTaskHolderService.initializeTasksAndListConsumable(null)); } for (Long taskId : taskIds) { boolean status = queue.offer(taskId); if (!status) { LOGGER.error("Unable to offer the task " + taskId + " to the queue"); } } } catch (RuntimeException | ServiceException | SecurityServiceException e) { LOGGER.error("Error while trying to init queue " + queue + " from database", e); } } } @Override public final QueuedTaskHolder submit(AbstractTask task) throws ServiceException { QueuedTaskHolder newQueuedTaskHolder = null; final String selectedQueueId; try { String serializedTask; selectedQueueId = selectQueue(task); serializedTask = queuedTaskHolderObjectMapper.writeValueAsString(task); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Serialized task: " + serializedTask); } newQueuedTaskHolder = new QueuedTaskHolder( task.getTaskName(), selectedQueueId /* May differ from selectedQueue.getId() */, task.getTaskType(), serializedTask ); queuedTaskHolderService.create(newQueuedTaskHolder); } catch (IOException e) { throw new ServiceException("Error while trying to serialize task " + task, e); } catch (RuntimeException | ServiceException | SecurityServiceException e) { throw new ServiceException("Error while creating and saving task " + task, e); } final Long newQueuedTaskHolderId = newQueuedTaskHolder.getId(); synchronizationManager.push(new ITransactionSynchronizationAfterCommitTask() { @Override public void run() throws Exception { doSubmit(selectedQueueId, newQueuedTaskHolderId); } }); return newQueuedTaskHolder; } protected void doSubmit(String queueId, Long newQueuedTaskHolderId) throws ServiceException { if (active.get()) { TaskQueue selectedQueue = getQueue(queueId); boolean status = selectedQueue.offer(newQueuedTaskHolderId); if (!status) { LOGGER.error("Unable to offer the task " + newQueuedTaskHolderId + " to the queue"); } } } @Override public void reload(Long queuedTaskHolderId) throws ServiceException, SecurityServiceException { QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId); if (queuedTaskHolder != null) { queuedTaskHolder.setStatus(TaskStatus.TO_RUN); queuedTaskHolder.resetExecutionInformation(); queuedTaskHolderService.update(queuedTaskHolder); TaskQueue queue = getQueue(queuedTaskHolder.getQueueId()); if (active.get()) { boolean status = queue.offer(queuedTaskHolder.getId()); if (!status) { LOGGER.error("Unable to offer the task " + queuedTaskHolder.getId() + " to the queue"); } } } } @Override public void cancel(Long queuedTaskHolderId) throws ServiceException, SecurityServiceException { QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId); if (queuedTaskHolder != null) { queuedTaskHolder.setStatus(TaskStatus.CANCELLED); queuedTaskHolderService.update(queuedTaskHolder); } } /** * Warning: this destroy method is called twice, so all the related code has * to be written with this constraint * * @throws Exception */ @PreDestroy private void destroy() { stop(); } @Override public synchronized void stop() { if (active.get()) { availableForAction.set(false); try { // TODO YRO gérer le timeout plus intelligemment for (TaskQueue queue : queuesById.values()) { for (TaskConsumer consumer : consumersByQueue.get(queue)) { try { consumer.stop(stopTimeout); } catch (RuntimeException e) { LOGGER.error("Error while trying to stop consumer " + consumer, e); } } interruptQueueProcesses(queue); } } finally { active.set(false); availableForAction.set(true); } } } private void interruptQueueProcesses(TaskQueue queue) { List<Long> queuedTaskHolderIds = new LinkedList<Long>(); queue.drainTo(queuedTaskHolderIds); for (Long queuedTaskHolderId : queuedTaskHolderIds) { QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId); if (queuedTaskHolder != null) { try { queuedTaskHolder.setStatus(TaskStatus.INTERRUPTED); queuedTaskHolder.setEndDate(new Date()); queuedTaskHolder.resetExecutionInformation(); queuedTaskHolderService.update(queuedTaskHolder); } catch (RuntimeException | ServiceException | SecurityServiceException e) { throw new RuntimeException(e); } } } } }