package io.eguan.dtx.journal; /* * #%L * Project eguan * %% * Copyright (C) 2012 - 2017 Oodrive * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class to handle the rotation schedule of {@link WritableTxJournal}s. * * A dedicated thread pool is created with scheduled tasks that launch requested rotations by calling the * {@link WritableTxJournal#executeRotation()} method. * * To keep this the least intrusive possible, only {@link WeakReference}s to the target {@link WritableTxJournal}s are * maintained and rotations are scheduled on a best-effort basis. * * @author oodrive * @author pwehrle * */ public final class JournalRotationManager { private static final Logger LOGGER = LoggerFactory.getLogger(JournalRotationManager.class); private static final int MIN_NB_ROTATOR_THREADS = 1; private static final int ROTATOR_TERMINATION_TIMEOUT = 5; private static final int SUBMIT_TIMEOUT_MS = 200; /** * Listener interface for receivers of {@link RotationEvent}s. * * */ public interface RotationListener { /** * Method called by RotationEvent sources after a rotation is complete. * * @param rotevt * the {@link RotationEvent} describing the rotation * @throws InterruptedException * if the thread is interrupted during event processing */ public void rotationEventOccured(RotationEvent rotevt) throws InterruptedException; } /** * Event triggered by a journal file rotation. * * */ @Immutable public static final class RotationEvent { /** * The stages of journal file rotation. * * */ public enum RotationStage { /** * Pre-rotation stage triggered if journal needs rotation, but before any modifications to files are * attempted. */ PRE_ROTATE, /** * Post-rotation stage triggered once the journal was successfully rotated. */ ROTATE_SUCCESS, /** * Post-rotation stage triggered if the journal rotation was attempted, but failed. */ ROTATE_FAILURE; } private final String filename; private final RotationStage stage; /** * Constructs an event for a given filename. * * @param filename * the filename having been rotated * @param stage * the {@link RotationStage} represented by the event */ RotationEvent(final String filename, final RotationStage stage) { this.filename = filename; this.stage = stage; } /** * Gets the filename for which this event was created. * * @return the filename the name of the file having been rotated */ public final String getFilename() { return filename; } /** * @return the stage this event represents */ public final RotationStage getStage() { return stage; } } /** * Rotation task implemented as a {@link Callable} that launches the rotation of the provided * {@link WritableTxJournal} . * * * This class is {@link Thread#interrupt() interrupt}-aware, i.e. it will stop short of starting a rotation if * {@link Thread#isInterrupted()} becomes <code>true</code>. * * */ private final class RotationTask implements Callable<Void> { private final WeakReference<WritableTxJournal> targetJRef; RotationTask(@Nonnull final WeakReference<WritableTxJournal> targetJRef) { this.targetJRef = Objects.requireNonNull(targetJRef); } @Override public final Void call() { final Thread thread = Thread.currentThread(); final WritableTxJournal targetJournal = targetJRef.get(); if (targetJournal == null) { return null; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Preparing rotation on reference; target=" + targetJRef.get() + ", thread=" + thread.getName()); } // does not start a rotation if the thread is interrupted if (thread.isInterrupted()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Rotation thread is interrupted; thread=" + thread.getName()); } removeJournalFromRunning(targetJournal); return null; } try { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Executing rotation on journal; " + targetJournal); } rotListenerLock.lockInterruptibly(); try { final Set<RotationListener> listenerList = rotationListeners .get(targetJournal.getJournalFilename()); if (listenerList == null) { targetJournal.executeRotation(); } else { final RotationListener[] listeners = listenerList.toArray(new RotationListener[listenerList .size()]); targetJournal.executeRotation(listeners); } } finally { rotListenerLock.unlock(); } } catch (final InterruptedException e) { // gracefully exit the method on being interrupted if (LOGGER.isDebugEnabled()) { LOGGER.debug("Interrupted while executing rotation; thread=" + thread.getName()); } } finally { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Finished rotation, removing from active list; journal=" + targetJournal + ", thread=" + thread.getName()); } removeJournalFromRunning(targetJournal); } return null; } } /** * Lock to guard access to the {@link #runningRotations} map. */ private final Object runningLock = new Object(); /** * Map holding {@link WeakReference}s to {@link WritableTxJournal}s being rotated with their {@link Future} handles * on currently running rotation tasks. * * Currently {@link Future}s are added to the map on {@link #submitRotation(WritableTxJournal) submission} and * removed either upon completion of {@link RotationTask#call() rotation} or {@link #cleanRotationWorkers() cleanup} * . The map is read and cleared when calling {@link #stop()} to explicitly {@link Future#cancel(boolean) cancel} * all running workers. */ @GuardedBy("runningLock") private final ConcurrentHashMap<WeakReference<WritableTxJournal>, Future<?>> runningRotations; /** * Lock to guard access to this instances runtime state. */ private final Object statusLock = new Object(); @GuardedBy("statusLock") private volatile boolean started = false; /** * Fixed-sized {@link ExecutorService} for {@link RotationTask}s. */ @GuardedBy("statusLock") private ExecutorService executor; private final int nbRotatorThreads; private final ReentrantLock rotListenerLock = new ReentrantLock(); private final HashMap<String, Set<RotationListener>> rotationListeners; /** * Constructs an autonomous instance that must be {@link #start() started} to begin operations. * * @param nbRotatorThreads * the number of worker threads to provision for rotation, defaults to {@value #MIN_NB_ROTATOR_THREADS} * if given an inferior value */ public JournalRotationManager(final int nbRotatorThreads) { runningRotations = new ConcurrentHashMap<WeakReference<WritableTxJournal>, Future<?>>(); rotationListeners = new HashMap<String, Set<RotationListener>>(); this.nbRotatorThreads = Math.max(MIN_NB_ROTATOR_THREADS, nbRotatorThreads); } /** * Starts the instance. After successful completion, rotation requests can be * {@link #submitRotation(WritableTxJournal) submitted} and will be serviced. */ public final void start() { synchronized (statusLock) { if (started) { return; } executor = Executors.newFixedThreadPool(nbRotatorThreads, new ThreadFactory() { private int serial; @Override public final Thread newThread(final Runnable r) { serial++; final Thread result = new Thread(r, "JournalRotation-" + serial); result.setPriority(Thread.NORM_PRIORITY + 1); result.setDaemon(true); return result; } }); started = true; } } /** * Gets the started status. * * @return <code>true</code> if this instance has been successfully {@link #start() started}, <code>false</code> * otherwise */ public final boolean isStarted() { return started; } /** * Stops the instance. After successful completion, no more rotation requests can be * {@link #submitRotation(WritableTxJournal) submitted}. This is however reversible by calling {@link #start()}. */ public final void stop() { synchronized (statusLock) { if (!started) { return; } synchronized (runningLock) { // at this point no more new rotations should be launched for (final Future<?> currWorker : runningRotations.values()) { currWorker.cancel(true); } // clear the list of running workers (to avoid memory leaks) runningRotations.clear(); } executor.shutdown(); try { if (!executor.awaitTermination(ROTATOR_TERMINATION_TIMEOUT, TimeUnit.SECONDS)) { LOGGER.warn("Rotation manager terminated running tasks"); } } catch (final InterruptedException e) { LOGGER.error("Interrupted while shutting down rotators."); } finally { final List<Runnable> rotationBacklog = executor.shutdownNow(); started = false; if (LOGGER.isDebugEnabled()) { LOGGER.debug("Shutdown rotation executor; pending tasks remaining=" + rotationBacklog.size()); } } } } /** * Adds a {@link RotationListener} that will be notified on rotations of any of the given files. * * @param listener * the non-<code>null</code> {@link RotationListener} * @param filenames * the filenames for which to register the event listener */ public final void addRotationEventListener(@Nonnull final RotationListener listener, final String... filenames) { Objects.requireNonNull(listener); if (filenames.length == 0) { return; } try { rotListenerLock.lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted", e); } try { for (final String currFilename : filenames) { Set<RotationListener> listeners = rotationListeners.get(currFilename); if (listeners == null) { listeners = new HashSet<RotationListener>(); rotationListeners.put(currFilename, listeners); } listeners.add(listener); } } finally { rotListenerLock.unlock(); } } /** * Removes a registered listener. * * @param listener * the {@link RotationListener} to remove */ public final void removeRotationEventListener(final RotationListener listener) { try { rotListenerLock.lockInterruptibly(); } catch (final InterruptedException e) { throw new IllegalStateException("Interrupted", e); } try { for (final String currFilename : rotationListeners.keySet()) { rotationListeners.get(currFilename).remove(listener); } } finally { rotListenerLock.unlock(); } } /** * Submits a journal for rotation. * * This class only retains {@link WeakReference}s to the given {@link WritableTxJournal} * * @param journal * a non-<code>null</code> {@link WritableTxJournal} */ final void submitRotation(@Nonnull final WritableTxJournal journal) { if (!started) { throw new IllegalStateException("Not started"); } Objects.requireNonNull(journal); synchronized (runningLock) { cleanRotationWorkers(); for (final WeakReference<WritableTxJournal> currJournalRef : runningRotations.keySet()) { if (journal.equals(currJournalRef.get())) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Rotation of journal is running; journal=" + journal); } return; } } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Enqueueing journal for rotation; journal=" + journal); } runningRotations.put(new WeakReference<WritableTxJournal>(journal), executor.submit(new RotationTask(new WeakReference<WritableTxJournal>(journal)))); } } /** * Checks if there are finished tasks in {@link #runningRotations} and removes them if necessary. * * This methods need external synchronization on {@link #runningRotations}. */ private final void cleanRotationWorkers() { final ArrayList<WeakReference<WritableTxJournal>> removeJournalRefs = new ArrayList<>(); for (final WeakReference<WritableTxJournal> currJournalRef : runningRotations.keySet()) { final Future<?> currFuture = runningRotations.get(currJournalRef); if (currFuture.isDone()) { try { currFuture.get(SUBMIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { LOGGER.warn("Worker ended with error", e); } removeJournalRefs.add(currJournalRef); } } for (final WeakReference<WritableTxJournal> removeJournal : removeJournalRefs) { runningRotations.remove(removeJournal); } } /** * Removes a journal instance from the list of {@link #runningRotations running rotations}. * * @param targetJournal * the {@link WritableTxJournal} to remove * @throws NullPointerException * if the argument is <code>null</code> */ private final void removeJournalFromRunning(@Nonnull final WritableTxJournal targetJournal) throws NullPointerException { Objects.requireNonNull(targetJournal); synchronized (runningLock) { for (final Iterator<WeakReference<WritableTxJournal>> iter = this.runningRotations.keySet().iterator(); iter .hasNext();) { final WeakReference<WritableTxJournal> currRef = iter.next(); final WritableTxJournal currJournal = currRef.get(); if ((currJournal != null) && (currJournal == targetJournal)) { this.runningRotations.remove(currRef); return; } } } } }