package ru.hflabs.rcd.task; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.support.CronTrigger; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import ru.hflabs.rcd.event.ContextEvent; import ru.hflabs.rcd.event.modify.ChangeEvent; import ru.hflabs.rcd.event.task.TaskExecutionEvent; import ru.hflabs.rcd.event.task.TaskScheduleEvent; import ru.hflabs.rcd.exception.constraint.IllegalPrimaryKeyException; import ru.hflabs.rcd.exception.constraint.task.IllegalTaskParametersException; import ru.hflabs.rcd.model.ModelUtils; import ru.hflabs.rcd.model.change.ChangeMode; import ru.hflabs.rcd.model.change.ChangeSet; import ru.hflabs.rcd.model.change.ChangeType; import ru.hflabs.rcd.model.task.TaskDescriptor; import ru.hflabs.rcd.model.task.TaskExecution; import ru.hflabs.rcd.model.task.TaskExecutionStatus; import ru.hflabs.rcd.model.task.TaskResult; import ru.hflabs.rcd.service.IFindService; import ru.hflabs.rcd.service.ISequenceService; import ru.hflabs.rcd.service.ServiceUtils; import ru.hflabs.rcd.service.task.ITaskLauncher; import ru.hflabs.rcd.service.task.ITaskPerformer; import ru.hflabs.util.security.SecurityUtil; import ru.hflabs.util.security.SystemAuthenticationProvider; import ru.hflabs.util.security.SystemAuthenticationProviderAware; import ru.hflabs.util.spring.Assert; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledFuture; /** * Класс <class>TaskLauncher</class> реализует сервис запуска задач * * @author Nazin Alexander */ public class TaskLauncher implements ITaskLauncher, BeanFactoryAware, ApplicationEventPublisherAware, SystemAuthenticationProviderAware, ApplicationListener<ApplicationEvent>, DisposableBean { private final Logger LOG = LoggerFactory.getLogger(getClass()); /** Фабрика создания классов */ private BeanFactory beanFactory; /** Сервис публикации событий */ private ApplicationEventPublisher eventPublisher; /** Провайдер аутентификации системной роли */ private SystemAuthenticationProvider systemAuthenticationProvider; /** Идентификатор сервиса */ private String launcherId; /** Сервис создания уникальных идентификаторов */ private ISequenceService sequenceService; /** Сервис работы с репозиторием задач */ private IFindService<TaskDescriptor> taskRepository; /** Планировщик задач */ private TaskScheduler schedulerService; /** Пул исполнения задач */ private ExecutorService executorService; /** Коллекция запланированных задач, где ключ - идентификатор дескриптора задачи, значение - поток постановки задачи в очередь на выполнение */ private Map<String, ScheduledFuture> scheduledTasks; /** Коллекция исполняемыз задач, где ключ - идентификатор исполняемой задачи, значение - результат выполнения */ private Map<String, TaskWorker> executedTasks; public TaskLauncher() { this.executedTasks = new ConcurrentHashMap<>(); this.scheduledTasks = new ConcurrentHashMap<>(); } @Override public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @Override public void setSystemAuthenticationProvider(SystemAuthenticationProvider systemAuthenticationProvider) { this.systemAuthenticationProvider = systemAuthenticationProvider; } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } public void setLauncherId(String launcherId) { this.launcherId = launcherId; } public void setSequenceService(ISequenceService sequenceService) { this.sequenceService = sequenceService; } public void setTaskRepository(IFindService<TaskDescriptor> taskRepository) { this.taskRepository = taskRepository; } public void setSchedulerService(TaskScheduler schedulerService) { this.schedulerService = schedulerService; } public void setExecutorService(ExecutorService executorService) { this.executorService = executorService; } @Override public Class<TaskExecution> retrieveTargetClass() { return TaskExecution.class; } @Override public Collection<TaskExecution> findExecuted() { ImmutableList.Builder<TaskExecution> result = ImmutableList.builder(); for (TaskWorker worker : executedTasks.values()) { TaskExecution execution = worker.getTaskExecution(); if (!TaskExecutionStatus.READY.equals(execution.getStatus())) { result.add(execution); } } return result.build(); } @Override public TaskExecution findByID(String id, boolean fillTransitive, boolean quietly) { Assert.isTrue(StringUtils.hasText(id), "ID must not be NULL or EMPTY"); return ServiceUtils.extractSingleDocument(findByIDs(ImmutableSet.of(id), fillTransitive, quietly), null); } @Override public Collection<TaskExecution> findByIDs(Set<String> ids, boolean fillTransitive, boolean quietly) throws IllegalPrimaryKeyException { Assert.isTrue(!CollectionUtils.isEmpty(ids), "IDs must not be NULL or EMPTY"); ImmutableList.Builder<TaskExecution> result = ImmutableList.builder(); for (String id : ids) { TaskWorker worker = executedTasks.get(id); if (worker != null) { TaskExecution execution = worker.getTaskExecution(); if (!TaskExecutionStatus.READY.equals(execution.getStatus())) { result.add(worker.getTaskExecution()); } } } return ServiceUtils.checkFoundDocuments(retrieveTargetClass(), ids, result.build(), quietly); } /** * Создает и возвращает экземпляр исполнителя задачи * * @param performerName идентификатор исполнителя задачи * @return Возвращает созданный экземпляр исполнителя задачи */ private ITaskPerformer createTaskPerformer(String performerName) { return beanFactory.getBean(performerName, ITaskPerformer.class); } /** * Обновляет результаты выполнения задачи * * @param event событие изменения состояния задачи */ private void refreshTaskResult(TaskExecutionEvent event) { if (TaskExecutionStatus.READY.equals(event.getStatus())) { TaskWorker worker = executedTasks.get(event.getId()); Assert.notNull(worker, String.format("Task %s is not registered in launcher '%s'", event.identity(), launcherId)); try { TaskResult taskResult = sequenceService.fillIdentifier(worker.getQuietly(), true); eventPublisher.publishEvent( new ChangeEvent(this, new ChangeSet<>(TaskResult.class, ChangeType.CREATE, ChangeMode.DEFAULT, Arrays.asList(taskResult))) ); } catch (Throwable ex) { LOG.error(String.format("Can't create task result %s. Cause by: %s", event.identity(), ex.getMessage()), ex); } finally { executedTasks.remove(event.getId()); } } } /** * Возвращает поток исполнения задачи * * @param descriptor дескриптор задачи * @return Возвращает исполнителя задачи */ private synchronized TaskWorker submitTask(TaskDescriptor descriptor) { Assert.notNull(descriptor, "Task descriptor must not be NULL"); Assert.isTrue(StringUtils.hasText(descriptor.getId()), "ID must not be NULL"); TaskWorker worker = executedTasks.get(descriptor.getId()); // Проверяем, что задача еще не выполняется if (worker == null) { Assert.notNull(descriptor.getParameters(), "Task parameters not properly configured", IllegalTaskParametersException.class); worker = new TaskWorker( launcherId, SecurityUtil.getCurrentUserName(), descriptor, createTaskPerformer(descriptor.getName()), eventPublisher ); executedTasks.put(descriptor.getId(), worker); executorService.submit(SecurityUtil.wrapWithCurrentAuthentication(worker)); } // Возвращаем дескриптор запущенной задачи return worker; } @Override public TaskExecution submitAsyncTask(String descriptorId) { return submitAsyncTask(taskRepository.findByID(descriptorId, false, false)); } @Override public TaskExecution submitAsyncTask(TaskDescriptor descriptor) { return submitTask(descriptor).getTaskExecution(); } @Override public TaskResult submitSyncTask(TaskDescriptor descriptor) { return submitTask(descriptor).getQuietly(); } /** * Регистрирует задачу в планировщике * * @param event событие постановки задачи в очередь на выполнение */ private void scheduleTask(TaskScheduleEvent event) { // Удаляем предыдущую задачу из списка запланированных ScheduledFuture existedFuture = scheduledTasks.remove(event.getId()); if (existedFuture != null) { existedFuture.cancel(true); LOG.info(String.format("Previous task %s schedule canceled", event.identity())); } // Добавляем дескриптор выполнения в планировщик if (StringUtils.hasText(event.getCron())) { Runnable scheduledTask = systemAuthenticationProvider.wrapWithSystemAuthentication(new TaskSchedulerThread(event.getId())); try { scheduledTasks.put(event.getId(), schedulerService.schedule(scheduledTask, new CronTrigger(event.getCron()))); LOG.info(String.format("Task %s scheduled", event.identity())); } catch (IllegalArgumentException ex) { LOG.info(String.format("Task %s schedule skipped. Cause by: %s", event.identity(), ex.getMessage())); } } } @Override public Collection<TaskExecution> cancelTask(Set<String> resultIDs) { Assert.isTrue(!CollectionUtils.isEmpty(resultIDs), "Task execution IDs must not be NULL"); Collection<TaskExecution> result = new ArrayList<>(resultIDs.size()); for (String resultId : resultIDs) { TaskWorker worker = executedTasks.get(resultId); if (worker != null) { worker.cancel(false); result.add(worker.getTaskExecution()); } } return result; } @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextEvent && ((ContextEvent) event).registryListener(launcherId)) { // Событие изменения статуса задачи if (event instanceof TaskExecutionEvent) { refreshTaskResult((TaskExecutionEvent) event); } // Событие постановки задачи в планировщик if (event instanceof TaskScheduleEvent) { scheduleTask((TaskScheduleEvent) event); } // Событие модификации дескриптора задачи if (event instanceof ChangeEvent && TaskDescriptor.class.isAssignableFrom(((ChangeEvent) event).getChangedClass())) { ChangeEvent changeEvent = (ChangeEvent) event; // Событие закрытия дескриптора задачи if (ChangeType.CLOSE.equals(changeEvent.getChangeType())) { cancelTask(Sets.newLinkedHashSet(Collections2.transform(changeEvent.getChanged(TaskDescriptor.class), ModelUtils.ID_FUNCTION))); } } } } @Override public void destroy() throws Exception { // Отменяем все ранее запларированные задачи for (ScheduledFuture future : scheduledTasks.values()) { future.cancel(true); } scheduledTasks.clear(); // Прерываем все выполняемые задачи executorService.shutdown(); for (TaskWorker worker : executedTasks.values()) { worker.cancel(true); } executedTasks.clear(); } /** * Класс <class>TaskSchedulerThread</class> реализует поток постановки запланированной задачи в очередь на исполнение * * @see org.springframework.scheduling.Trigger */ private class TaskSchedulerThread implements Runnable { /** Идентификатор дескриптора выполнения задачи */ private final String descriptorId; private TaskSchedulerThread(String descriptorId) { this.descriptorId = descriptorId; } @Override public void run() { try { submitAsyncTask(descriptorId); } catch (Exception ex) { LOG.error(String.format("Can't submit task ID '%s' to execution. Cause by: %s", descriptorId, ex.getMessage()), ex); } } } }