org.codice.ddf.catalog.sourcepoller.PollerRunner.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.catalog.sourcepoller.PollerRunner.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or any later version.
 *
 * <p>This program 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 Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.catalog.sourcepoller;

import static org.apache.commons.lang3.Validate.notNull;

import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link #setPollIntervalMinutes} starts the process to periodically call {@link
 * Poller#pollItems(long, TimeUnit, ImmutableMap)}.
 *
 * <p>This class must be extended to implement the {@link #getValueLoaders()} method. Extensions of
 * this class can also specify {@link K}/{@link V}. This class contains the common logic to
 * periodically poll the injected {@link Poller} (and handle failures) regardless of these
 * variables.
 *
 * @param <K> type of the unique key used to identify the items to poll, e.g. {@link SourceKey}
 * @param <V> type of the value returned when an item is polled
 */
abstract class PollerRunner<K, V> {

    private static final Logger LOGGER = LoggerFactory.getLogger(PollerRunner.class);

    private static final long MINIMUM_POLL_INTERVAL_MINUTES = 1;

    private final Poller<K, V> poller;

    private final ScheduledExecutorService scheduledExecutorService;

    @Nullable
    private volatile Future scheduledPollingFuture;

    private volatile long pollIntervalMinutes;

    private final Lock startPollingLock = new ReentrantLock();

    /**
     * {@link #startPolling(long)} must be called to schedule the polling.
     *
     * @param poller which to poll periodically at the configured poll interval
     * @param pollIntervalMinutes the poll interval. The poll interval can be updated via {@link
     *     #setPollIntervalMinutes(long)}.
     * @param scheduledExecutorService the {@link ScheduledExecutorService} used to schedule the poll
     *     task to execute at the configured poll interval
     * @throws IllegalArgumentException if {@code pollIntervalMinutes} is less than {@value
     *     #MINIMUM_POLL_INTERVAL_MINUTES} or if the {@code scheduledExecutorService} {@link
     *     ScheduledExecutorService#isShutdown()}
     * @throws NullPointerException if any of the parameters are {@code null}
     */
    protected PollerRunner(final Poller<K, V> poller, final long pollIntervalMinutes,
            final ScheduledExecutorService scheduledExecutorService) {
        this.poller = notNull(poller);
        if (scheduledExecutorService.isShutdown()) {
            throw new IllegalArgumentException("scheduledExecutorService argument may not be shutdown");
        }
        this.scheduledExecutorService = scheduledExecutorService;
        this.pollIntervalMinutes = validPollIntervalMinutes(pollIntervalMinutes);
    }

    /** @throws IllegalStateException if unable to schedule polling */
    public void init() {
        startPolling(pollIntervalMinutes);
    }

    /** Stops the {@link PollerRunner} and all {@link java.util.concurrent.ExecutorService}s */
    public void destroy() {
        MoreExecutors.shutdownAndAwaitTermination(scheduledExecutorService, 5, TimeUnit.SECONDS);
        LOGGER.debug("Destroyed PollerRunner");
    }

    /**
     * Updates the poll interval and then calls {@link #startPolling(long)}
     *
     * <p>WARNING:
     *
     * <p>The delay until the {@link Poller#getCachedValue(Object)} will begin reporting a new or
     * updated value is determined by the how often {@link Poller#pollItems(long, TimeUnit,
     * ImmutableMap)} is called.
     *
     * <p>This delay will be a maximum of 2*{@code pollIntervalMinutes} (or longer if threads are
     * backed-up in the {@link Poller}). Therefore, <b>the {@code pollIntervalMinutes} should not be
     * set to a value which results in an unacceptable maximum delay</b>.
     *
     * @param pollIntervalMinutes the interval (in minutes) of the new schedule
     * @throws IllegalArgumentException if {@code pollIntervalMinutes} is less than {@value
     *     #MINIMUM_POLL_INTERVAL_MINUTES}
     * @throws IllegalStateException if {@link #scheduledExecutorService} {@link
     *     ScheduledExecutorService#isShutdown()} or if unable to schedule polling
     */
    public synchronized void setPollIntervalMinutes(final long pollIntervalMinutes) {
        if (scheduledExecutorService.isShutdown()) {
            final String message = "Unable to start polling because scheduledExecutorService is shutdown";
            LOGGER.warn("{}. Try restarting the system.", message);
            throw new IllegalStateException(message);
        }

        startPolling(validPollIntervalMinutes(pollIntervalMinutes));
        this.pollIntervalMinutes = pollIntervalMinutes;
    }

    /**
     * Anything thrown by this method will cause the poll to fail, but future polls will still be
     * executed.
     *
     * @return a non-null {@link ImmutableMap} of current keys and a loader {@link Callable} to get
     *     the current value for each key, which may be empty
     */
    protected abstract ImmutableMap<K, Callable<V>> getValueLoaders();

    /** @throws IllegalStateException if unable to schedule polling */
    private void startPolling(final long pollIntervalMinutes) {
        if (!startPollingLock.tryLock()) {
            final String message = "Unable to start the schedule. Multiple threads may not start the schedule at the same time.";
            LOGGER.warn("{} The Poller will not be periodically updated. Try restarting the system.", message);
            throw new IllegalStateException(message);
        }

        try {
            doStartPolling(pollIntervalMinutes);
        } finally {
            startPollingLock.unlock();
        }
    }

    /** @throws IllegalStateException if unable to schedule polling */
    private void doStartPolling(long pollIntervalMinutes) {
        if (scheduledPollingFuture != null) {
            scheduledPollingFuture.cancel(true);
            LOGGER.debug("Stopped the scheduled process to poll all of the current items at a fixed rate");
        }

        // From the ScheduledExecutorService#scheduleAtFixedRate javadoc: "If any execution of this task
        // takes longer than its period, then subsequent executions may start late, but will not
        // concurrently execute."
        try {
            scheduledPollingFuture = scheduledExecutorService.scheduleAtFixedRate(() -> poll(pollIntervalMinutes),
                    0, pollIntervalMinutes, TimeUnit.MINUTES);
        } catch (final RejectedExecutionException e) {
            final String message = "Unable to schedule polling at at a fixed rate of " + pollIntervalMinutes
                    + " minute(s)";
            LOGGER.warn("{}. Try restarting the system.", message, e);
            throw new IllegalStateException(message, e);
        }
        LOGGER.debug(
                "Successfully scheduled a process to poll all of the current items at a fixed rate of {} minute(s), where polls will time out at {} minute(s)",
                pollIntervalMinutes, pollIntervalMinutes);
    }

    @SuppressWarnings("squid:S1181" /*Catching throwable intentionally*/)
    private void poll(final long pollIntervalMinutes) {
        try {
            final ImmutableMap<K, Callable<V>> itemsToPoll = getValueLoaders();
            final int currentItemsCount = itemsToPoll.size();

            LOGGER.trace(
                    "Starting a process to poll the {} current item(s), where polls will time out at {} minute(s)",
                    currentItemsCount, pollIntervalMinutes);
            // If there are any unhandled exceptions in this method, all future calls will be suppressed.
            poller.pollItems(pollIntervalMinutes, TimeUnit.MINUTES, itemsToPoll);
            LOGGER.trace("Successfully finished the process of polling the {} source(s)", currentItemsCount);
        } catch (InterruptedException e) {
            LOGGER.debug("A scheduled poll was interrupted.", pollIntervalMinutes, e);
            Thread.currentThread().interrupt();
        } catch (final VirtualMachineError e) {
            final String message = "A scheduled poll failed";
            LOGGER.debug(message, e);
            LOGGER.warn(
                    "{}. See debug log for more details. The current items will NOT continue to be polled at a fixed rate of {} minutes. Try restarting the system.",
                    message, pollIntervalMinutes);
            throw e;
        } catch (final PollerException e) {
            e.getCauses().forEach((k, t) -> LOGGER.debug("Unable to load a new value for {}", k, t));
            LOGGER.warn(
                    "A scheduled poll failed. {}. See debug log for more details. The current items will continue to be polled at a fixed rate of {} minutes. Try restarting the system if future polls also fail.",
                    e.getMessage(), pollIntervalMinutes);
        } catch (final Throwable e) {
            final String message = "A scheduled poll failed";
            LOGGER.debug(message, e);
            LOGGER.warn(
                    "A scheduled poll failed. See debug log for more details. The current items will continue to be polled at a fixed rate of {} minutes. Try restarting the system if future polls also fail.",
                    pollIntervalMinutes);
        }
    }

    /**
     * @throws IllegalArgumentException if {@code pollIntervalMinutes} is less than {@value
     *     #MINIMUM_POLL_INTERVAL_MINUTES}
     */
    private static long validPollIntervalMinutes(long pollIntervalMinutes) {
        Validate.isTrue(pollIntervalMinutes >= MINIMUM_POLL_INTERVAL_MINUTES,
                "pollIntervalMinutes argument may not be less than %d minutes. Not updating the poll interval.",
                MINIMUM_POLL_INTERVAL_MINUTES);
        return pollIntervalMinutes;
    }
}