package org.fenixedu.bennu.scheduler.domain; import it.sauronsoftware.cron4j.Scheduler; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import org.fenixedu.bennu.core.domain.Bennu; import org.fenixedu.bennu.io.domain.FileStorage; import org.fenixedu.bennu.scheduler.SchedulerConfiguration; import org.fenixedu.bennu.scheduler.TaskRunner; import org.fenixedu.bennu.scheduler.annotation.Task; import org.fenixedu.bennu.scheduler.log.ExecutionLogRepository; import org.fenixedu.bennu.scheduler.log.FileSystemLogRepository; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ist.fenixframework.Atomic; import pt.ist.fenixframework.Atomic.TxMode; import pt.ist.fenixframework.FenixFramework; import com.google.common.io.Files; public class SchedulerSystem extends SchedulerSystem_Base { private static final Logger LOG = LoggerFactory.getLogger(SchedulerSystem.class); private static final Map<String, Task> tasks = new HashMap<>(); private static Scheduler scheduler; public static LinkedBlockingQueue<TaskRunner> queue; public static Set<TaskRunner> runningTasks; public static Set<TaskSchedule> scheduledTasks; private static final List<Thread> activeConsumers = new ArrayList<>(); private static Set<Timer> timers; private static transient Integer leaseTime; private static transient Integer queueThreadsNumber; static { queue = new LinkedBlockingQueue<>(); timers = new HashSet<>(); runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<TaskRunner, Boolean>()); scheduledTasks = Collections.newSetFromMap(new ConcurrentHashMap<TaskSchedule, Boolean>()); getLeaseTimeMinutes(); getQueueThreadsNumber(); } /** * The scheduler will wait this value till next attempt to run scheduler. * This value must be greater than 1. * * @return reattempt scheduler initialization time (in minutes) */ private static Integer getLeaseTimeMinutes() { if (leaseTime == null) { final Integer leaseTimeProperty = SchedulerConfiguration.getConfiguration().leaseTimeMinutes(); if (leaseTimeProperty < 2) { throw new Error("property scheduler.lease.time.minutes must be a positive integer greater than 1."); } leaseTime = leaseTimeProperty; } return leaseTime; } /** * Number of threads that are processing the queue of threads * * @return number of threads */ private static Integer getQueueThreadsNumber() { if (queueThreadsNumber == null) { final Integer queueThreadsNumberProperty = SchedulerConfiguration.getConfiguration().queueThreadsNumber(); if (queueThreadsNumberProperty < 1) { throw new Error("property scheduler.queue.threads.number must be a positive integer greater than 0."); } queueThreadsNumber = queueThreadsNumberProperty; } return queueThreadsNumber; } private SchedulerSystem() { super(); setBennu(Bennu.getInstance()); File tmp = Files.createTempDir(); setLoggingStorage(FileStorage.createNewFileSystemStorage("schedulerSystemLoggingStorage", tmp.getAbsolutePath(), 0)); LOG.info("Create sensible default {} for logging storage", tmp.getAbsolutePath()); } public static SchedulerSystem getInstance() { if (Bennu.getInstance().getSchedulerSystem() == null) { return initialize(); } return Bennu.getInstance().getSchedulerSystem(); } @Atomic(mode = TxMode.WRITE) private static SchedulerSystem initialize() { if (Bennu.getInstance().getSchedulerSystem() == null) { return new SchedulerSystem(); } return Bennu.getInstance().getSchedulerSystem(); } @Override public Set<TaskSchedule> getTaskScheduleSet() { // FIXME: remove when the framework support read-only slots return super.getTaskScheduleSet(); } /** * * @return true if scheduler is running in this server instance. */ public static Boolean isRunning() { return isActive() && scheduler.isStarted(); } /** * * @return true if this server instance is responsible for running the scheduler. */ public static Boolean isActive() { return scheduler != null; } @Atomic(mode = TxMode.WRITE) private Boolean shouldRun() { if (isLeaseExpired()) { lease(); return true; } return false; } /** * Set's lease time * * @return */ private DateTime lease() { setLease(new DateTime()); return getLease(); } /** * True if lease is expired * * @return */ private boolean isLeaseExpired() { final DateTime lease = getLease(); if (lease == null) { return true; } return lease.plusMinutes(getLeaseTimeMinutes()).isBeforeNow(); } /*** * This method starts the scheduler if lease is expired. * */ public static void init() { Timer waitingForLeaseTimer = new Timer("waitingForLeaseTimer", true); waitingForLeaseTimer.scheduleAtFixedRate(new TimerTask() { Boolean shouldRun = false; @Override @Atomic(mode = TxMode.READ) public void run() { setShouldRun(SchedulerSystem.getInstance().shouldRun()); if (shouldRun) { bootstrap(); } else { LOG.debug("Lease is not gone. Wait for it ..."); } setShouldRun(false); } public void setShouldRun(Boolean shouldRun) { this.shouldRun = shouldRun; } }, 0, getLeaseTimeMinutes() * 60 * 1000); timers.add(waitingForLeaseTimer); } /** * Initializes the scheduler. */ private synchronized static void bootstrap() { LOG.info("Running Scheduler bootstrap"); if (scheduler == null) { scheduler = new Scheduler(); scheduler.setDaemon(true); } if (!scheduler.isStarted()) { cleanNonExistingSchedules(); initSchedules(); spawnConsumers(); spawnRefreshSchedulesTask(); spawnLeaseTimerTask(); scheduler.start(); } } /** * If the scheduler is initialized schedules the task that updates the lease time every getLeaseTimeMinutes() / 2 minutes */ private static void spawnLeaseTimerTask() { int period = getLeaseTimeMinutes() * 60 * 1000 / 2; Timer leaseTimer = new Timer("leaseTimer", true); leaseTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { if (isRunning()) { lease(); } } @Atomic(mode = TxMode.WRITE) private void lease() { final DateTime lease = SchedulerSystem.getInstance().lease(); LOG.debug("Leasing until {}", lease); } }, 0, period); timers.add(leaseTimer); } /** * Schedules or unschedules task schedules if created or deleted not in this server instance. * Runs every minute. */ private static void spawnRefreshSchedulesTask() { Timer refreshSchedulesTimer = new Timer("refreshSchedulesTimer", true); refreshSchedulesTimer.scheduleAtFixedRate(new TimerTask() { @Override @Atomic(mode = TxMode.READ) public void run() { LOG.debug("Running refresh schedules"); Set<TaskSchedule> domainSchedules = new HashSet<>(getInstance().getTaskScheduleSet()); for (TaskSchedule schedule : domainSchedules) { if (!schedule.isScheduled()) { LOG.debug("New schedule not scheduled before {} {}", schedule.getExternalId(), schedule.getTaskClassName()); schedule(schedule); } } for (TaskSchedule schedule : scheduledTasks) { if (!domainSchedules.contains(schedule)) { LOG.debug("schedule disappeared not unscheduled before {} {}", schedule.getExternalId()); unschedule(schedule); } } LOG.debug("Refresh schedules done"); } }, 0, 1 * 60 * 1000); timers.add(refreshSchedulesTimer); } private static void spawnConsumers() { for (int i = 1; i <= getQueueThreadsNumber(); i++) { LOG.debug("Launching queue consumer {}", i); Thread thread = new Thread(new ProcessQueue()); thread.setName("SchedulerConsumer-" + i); thread.start(); activeConsumers.add(thread); } } /** * If some task was deleted of the codebase, delete current schedules referencing it. */ @Atomic(mode = TxMode.WRITE) private static void cleanNonExistingSchedules() { Set<TaskSchedule> scheduleSet = new HashSet<>(SchedulerSystem.getInstance().getTaskScheduleSet()); for (TaskSchedule schedule : scheduleSet) { if (!tasks.containsKey(schedule.getTaskClassName())) { LOG.warn("Class {} is no longer available. schedule {} - {} - {} deleted. ", schedule.getTaskClassName(), schedule.getExternalId(), schedule.getTaskClassName(), schedule.getSchedule()); schedule.delete(false); } } } /** * Add to scheduler all existing tasks schedules. */ @Atomic(mode = TxMode.READ) private static void initSchedules() { for (TaskSchedule schedule : SchedulerSystem.getInstance().getTaskScheduleSet()) { schedule(schedule); } } /** * Schedules a task. * If the task is not already queued and is not running add it to the processing queue. * ProcessQueue threads will run pending tasks. * * @param schedule * The task to be added to the queue */ @Atomic(mode = TxMode.READ) public static void schedule(final TaskSchedule schedule) { if (isActive()) { if (schedule.isRunOnce()) { runNow(schedule); } else { LOG.debug("schedule [{}] {}", schedule.getSchedule(), schedule.getTaskClassName()); schedule.setTaskId(scheduler.schedule(schedule.getSchedule(), new Runnable() { @Override @Atomic(mode = TxMode.READ) public void run() { if (FenixFramework.isDomainObjectValid(schedule)) { final TaskRunner taskRunner = schedule.getTaskRunner(); queue(taskRunner); } } })); scheduledTasks.add(schedule); } } else { LOG.debug("don't schedule [{}] {}", schedule.getSchedule(), schedule.getTaskClassName()); } } /** * Remove schedule from the scheduler. This will not delete the TaskSchedule, only removes the scheduling. * * @param schedule * The task to be removed from the queue */ public static void unschedule(TaskSchedule schedule) { if (isActive()) { if (schedule.isRunOnce()) { LOG.debug("unschedule run once {}. delete it.", schedule.getTaskClassName()); schedule.delete(false); } else { LOG.debug("unschedule [{}] {}", schedule.getSchedule(), schedule.getTaskClassName()); scheduler.deschedule(schedule.getTaskId()); scheduledTasks.remove(schedule); } } else { LOG.debug("don't unschedule [{}] {}", schedule.getSchedule(), schedule.getTaskClassName()); } } @Atomic(mode = TxMode.WRITE) private static void runNow(TaskSchedule schedule) { LOG.debug("run once schedule {}", schedule.getTaskClassName()); queue(schedule.getTaskRunner()); unschedule(schedule); } public static final void addTask(String className, Task taskAnnotation) { LOG.debug("Register Task : {} with name {}", className, taskAnnotation.englishTitle()); tasks.put(className, taskAnnotation); } /** * When context is gracefully destroyed, set the lease time to null so that any other server instance * can start the scheduler. */ @Atomic(mode = TxMode.WRITE) private static void resetLease() { LOG.debug("Reset lease to null"); SchedulerSystem.getInstance().setLease(null); } public static void destroy() { for (final Timer timer : timers) { LOG.debug("interrupted timer thread {}", timer.toString()); timer.cancel(); } if (isActive()) { LOG.info("stopping scheduler"); scheduler.stop(); for (final Thread consumer : activeConsumers) { LOG.debug("interrupted consumer thread {}", consumer.getName()); consumer.interrupt(); } resetLease(); } } public static Map<String, Task> getTasks() { return tasks; } public static String getTaskName(String className) { final Task taskAnnotation = tasks.get(className); if (taskAnnotation != null) { return taskAnnotation.englishTitle(); } return null; } /** * Used by CronTask to store tasks' output files and custom logging. * * @return the physical absolute path of the logging storage. */ @Atomic(mode = TxMode.READ) public static String getLogsPath() { if (getInstance().getLoggingStorage() == null) { throw new Error("Please add logging storage"); } return getInstance().getLoggingStorage().getAbsolutePath(); } @Atomic(mode = TxMode.READ) public static void queue(final TaskRunner taskRunner) { synchronized (queue) { if (!queue.contains(taskRunner)) { if (!runningTasks.contains(taskRunner)) { LOG.debug("Add to queue {}", taskRunner.getTaskName()); try { queue.put(taskRunner); } catch (InterruptedException e) { LOG.warn("Thread was interrupted."); Thread.currentThread().interrupt(); } } else { LOG.debug("Don't add to queue. Task is running {}", taskRunner.getTaskName()); } } else { LOG.debug("Don't add to queue. Already exists {}", taskRunner.getTaskName()); } } } private static ExecutionLogRepository repository = new FileSystemLogRepository(3); /** * Configures a new log repository to be used by the scheduler system. * * @param repo * The new log repository * @throws NullPointerException * If the provided repository is {@code null} */ public static void setLogRepository(ExecutionLogRepository repo) { repository = Objects.requireNonNull(repo); } /** * Returns the currently configured {@link ExecutionLogRepository}. * * By default, a {@link FileSystemLogRepository} is used, with a dispersion factor of {@code 3}. * * @return * The current log repository */ public static ExecutionLogRepository getLogRepository() { return repository; } }