com.omertron.slackbot.functions.scheduler.AbstractBotTask.java Source code

Java tutorial

Introduction

Here is the source code for com.omertron.slackbot.functions.scheduler.AbstractBotTask.java

Source

/*
 *      Copyright (c) 2017 Stuart Boston
 *
 *      This file is part of the BGG Slack Bot.
 *
 *      The BGG Slack Bot is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation, either version 3 of the License, or
 *      any later version.
 *
 *      The BGG Slack Bot is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *
 *      You should have received a copy of the GNU General Public License
 *      along with the BGG Slack Bot.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.omertron.slackbot.functions.scheduler;

import com.omertron.slackbot.Constants;
import com.ullink.slack.simpleslackapi.SlackAttachment;
import com.ullink.slack.simpleslackapi.SlackChannel;
import com.ullink.slack.simpleslackapi.SlackSession;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This is the task that does the work
 *
 * @author Omertron
 */
public abstract class AbstractBotTask implements BotTaskInterface {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractBotTask.class);
    private static final ZoneId TIMEZONE = ZoneId.of("Europe/London");

    private final ScheduledExecutorService executorService;
    private volatile ScheduledFuture<?> scheduledTask = null;

    private final String name;
    private final int targetHour;
    private final int targetMin;
    private static final int TARGET_SEC = 0;
    private final AtomicInteger completedTasks = new AtomicInteger(0);
    private volatile boolean isBusy = false;

    private final SlackSession session;
    private final SlackChannel channel;

    public AbstractBotTask(ScheduledExecutorService executorService, String name, int targetHour, int targetMin,
            SlackSession session, SlackChannel channel) {
        this.executorService = executorService;
        this.name = "Executor [" + name + "]";
        this.targetHour = targetHour;
        this.targetMin = targetMin;
        this.session = session;
        this.channel = channel;

        LOG.info("{} scheduled for {}:{} on channel '{}'", name, targetHour, targetMin, channel.getName());
    }

    @Override
    public final String getName() {
        return name;
    }

    public final int getTargetHour() {
        return targetHour;
    }

    public final int getTargetMin() {
        return targetMin;
    }

    public final SlackSession getSession() {
        return session;
    }

    public final SlackChannel getChannel() {
        return channel;
    }

    public final ScheduledFuture getScheduledTask() {
        return scheduledTask;
    }

    public final AtomicInteger getCompletedTasks() {
        return completedTasks;
    }

    /**
     * Return an attachment with the status of the BotTask
     *
     * @return
     */
    @Override
    public final SlackAttachment getStatus() {
        SlackAttachment sa = new SlackAttachment();

        sa.setTitle(name);

        StringBuilder time = new StringBuilder();
        time.append(StringUtils.leftPad(Integer.toString(targetHour), 2, '0')).append(':')
                .append(StringUtils.leftPad(Integer.toString(targetMin), 2, '0'));

        sa.addField("Target Time", time.toString(), true);
        sa.addField("Remaining Time", formatSeconds(scheduledTask.getDelay(TimeUnit.SECONDS)), true);
        sa.addField("Channel", channel.getName(), true);
        sa.addField("Executions", Integer.toString(completedTasks.get()), true);
        sa.setColor(Constants.ATTACH_COLOUR_GOOD);

        return sa;
    }

    @Override
    public final void start() {
        scheduleNextTask(doTaskWork());
    }

    @Override
    public final void stop() {
        LOG.info("{} is stopping.", name);
        if (scheduledTask != null) {
            scheduledTask.cancel(false);
        }
        executorService.shutdown();
        LOG.info("{} stopped.", name);
        try {
            LOG.info("{} awaitTermination, start: isBusy [{}]", name, isBusy);
            // wait one minute to termination if busy
            if (isBusy) {
                executorService.awaitTermination(1, TimeUnit.MINUTES);
            }
        } catch (InterruptedException ex) {
            LOG.error("{} awaitTermination exception", name, ex);
            // Restore interrupted state...
            Thread.currentThread().interrupt();
        } finally {
            LOG.info("{} awaitTermination, finished", name);
        }
    }

    private Runnable doTaskWork() {
        return () -> {
            LOG.info("{} [{}] started at {}", name, completedTasks.get(), formattedDateTime());
            try {
                isBusy = true;
                doWork();
                LOG.info("{} finished work at {}", name, formattedDateTime());
            } catch (Exception ex) {
                LOG.error("{} threw exception at {}", name, formattedDateTime(), ex);
            } finally {
                isBusy = false;
            }
            scheduleNextTask(doTaskWork());
            LOG.info("{} [{}] finished at {}", name, completedTasks.getAndIncrement(), formattedDateTime());
        };
    }

    /**
     * Schedule the task for the next occurrence of the hour/minute combination
     *
     * @param task
     */
    private void scheduleNextTask(Runnable task) {
        LOG.info("{} creating next schedule at {}", name, formattedDateTime());
        long delay = computeNextDelay(targetHour, targetMin, TARGET_SEC);
        LOG.info("{} is next scheduled in {}", name, formatSeconds(delay));
        scheduledTask = executorService.schedule(task, delay, TimeUnit.SECONDS);
    }

    /**
     * Convert seconds to Hours, Minutes and Seconds
     *
     * @param seconds
     * @return
     */
    protected final String formatSeconds(long seconds) {
        int day = (int) TimeUnit.SECONDS.toDays(seconds);
        long hours = TimeUnit.SECONDS.toHours(seconds) - TimeUnit.DAYS.toHours(day);
        long minute = TimeUnit.SECONDS.toMinutes(seconds) - TimeUnit.DAYS.toMinutes(day)
                - TimeUnit.HOURS.toMinutes(hours);
        long second = TimeUnit.SECONDS.toSeconds(seconds) - TimeUnit.DAYS.toSeconds(day)
                - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minute);

        return String.format("%1$dh %2$dm %3$ds", hours, minute, second);
    }

    /**
     * Calculate the time between "now" and the execution time.
     *
     * @param targetHour
     * @param targetMin
     * @param targetSec
     * @return
     */
    private long computeNextDelay(int targetHour, int targetMin, int targetSec) {
        ZonedDateTime zonedNow = localeDateTime();
        ZonedDateTime zonedNextTarget = zonedNow.withHour(targetHour).withMinute(targetMin).withSecond(targetSec)
                .withNano(0);

        if (zonedNow.compareTo(zonedNextTarget) >= 0) {
            zonedNextTarget = zonedNextTarget.plusDays(1);
        }

        Duration duration = Duration.between(zonedNow, zonedNextTarget);

        // If we are scheduled within the next minute, then skip a day as we probably just ran fast
        if (duration.getSeconds() <= 60l) {
            zonedNextTarget = zonedNextTarget.plusDays(1);
            duration = Duration.between(zonedNow, zonedNextTarget);
        }

        return duration.getSeconds();
    }

    /**
     * Get the current date/time in the local time zone
     *
     * @return
     */
    private static ZonedDateTime localeDateTime() {
        return ZonedDateTime.now(TIMEZONE);
    }

    /**
     * Return the current date/time formatted for printing
     *
     * @return
     */
    protected static final String formattedDateTime() {
        return localeDateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}