All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.id.ajlane.concurrent.DelayBasedCalendarExecutorService Maven / Gradle / Ivy

Go to download

A clock-based ExecutorService using Java 8 Time. For when Quartz is just too heavy.

There is a newer version: 0.0.3
Show newest version
/*
 * Copyright 2016 Aaron Lane
 *
 * 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.
 */

package au.id.ajlane.concurrent;

import java.text.MessageFormat;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Delayed;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

/**
 * A straight-forward {@link CalendarExecutorService} that uses an internal {@link ScheduledExecutorService} to delay
 * task execution according to difference between the current
 * time and the scheduled time.
 * 

* This is the default {@code CalendarExecutorService} provided by utility methods on the {@code * CalendarExecutorService} interface. *

* Pending tasks are re-scheduled according to the {@link #getAdjustmentPeriod() adjustment period} (every 2 hours by * default). This allows the service to self-correct if the {@link Clock} falls out-of-sync with the internal timer. A * shorter adjustment period will allow the service to self-correct faster, but will incur greater runtime costs. */ public final class DelayBasedCalendarExecutorService implements CalendarExecutorService { @SuppressWarnings("ComparableImplementedButEqualsNotOverridden") private static final class WrappedCompletableCalendarFuture implements CalendarFuture { public static WrappedCompletableCalendarFuture wrap( final Instant instant, final CompletableFuture future ) { return wrap(Clock.systemDefaultZone(), instant, future); } public static WrappedCompletableCalendarFuture wrap( final Clock clock, final Instant instant, final CompletableFuture future ) { return new WrappedCompletableCalendarFuture<>(clock, instant, future); } private final Clock clock; private final CompletableFuture future; private final Instant instant; private WrappedCompletableCalendarFuture( final Clock clock, final Instant instant, final CompletableFuture future ) { this.clock = clock; this.instant = instant; this.future = future; } @Override public CalendarFuture acceptEither( final CompletionStage other, final Consumer action ) { return wrap(future.acceptEither(other, action)); } @Override public CalendarFuture acceptEitherAsync( final CompletionStage other, final Consumer action ) { return wrap(future.acceptEitherAsync(other, action)); } @Override public CalendarFuture acceptEitherAsync( final CompletionStage other, final Consumer action, final Executor executor ) { return wrap(future.acceptEitherAsync(other, action, executor)); } @Override public CalendarFuture applyToEither( final CompletionStage other, final Function fn ) { return wrap(future.applyToEither(other, fn)); } @Override public CalendarFuture applyToEitherAsync( final CompletionStage other, final Function fn ) { return wrap(future.applyToEitherAsync(other, fn)); } @Override public CalendarFuture applyToEitherAsync( final CompletionStage other, final Function fn, final Executor executor ) { return wrap(future.applyToEitherAsync(other, fn, executor)); } @Override public boolean cancel(final boolean mayInterruptIfRunning) { return future.cancel(mayInterruptIfRunning); } @Override public int compareTo(final Delayed o) { return Long.compare(getDelay(TimeUnit.NANOSECONDS), o.getDelay(TimeUnit.NANOSECONDS)); } @Override public CalendarFuture exceptionally(final Function fn) { return wrap(future.exceptionally(fn)); } @Override public V get() throws InterruptedException, ExecutionException { return future.get(); } @Override public V get(final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return future.get(timeout, unit); } @Override public long getDelay(final TimeUnit unit) { final Instant now = clock.instant(); return Duration.between(instant, now) .get(TimeUnits.toTemporalUnit(unit)); } @Override public Instant getInstant() { return instant; } @Override public CalendarFuture handle(final BiFunction fn) { return wrap(future.handle(fn)); } @Override public CalendarFuture handleAsync(final BiFunction fn) { return wrap(future.handleAsync(fn)); } @Override public CalendarFuture handleAsync( final BiFunction fn, final Executor executor ) { return wrap(future.handleAsync(fn, executor)); } @Override public boolean isCancelled() { return future.isCancelled(); } @Override public boolean isDone() { return future.isDone(); } @Override public CalendarFuture runAfterBoth( final CompletionStage other, final Runnable action ) { return wrap(future.runAfterBoth(other, action)); } @Override public CalendarFuture runAfterBothAsync( final CompletionStage other, final Runnable action ) { return wrap(future.runAfterBothAsync(other, action)); } @Override public CalendarFuture runAfterBothAsync( final CompletionStage other, final Runnable action, final Executor executor ) { return wrap(future.runAfterBothAsync(other, action, executor)); } @Override public CalendarFuture runAfterEither( final CompletionStage other, final Runnable action ) { return wrap(future.runAfterEither(other, action)); } @Override public CalendarFuture runAfterEitherAsync( final CompletionStage other, final Runnable action ) { return wrap(future.runAfterEitherAsync(other, action)); } @Override public CalendarFuture runAfterEitherAsync( final CompletionStage other, final Runnable action, final Executor executor ) { return wrap(future.runAfterEitherAsync(other, action, executor)); } @Override public CalendarFuture thenAccept(final Consumer action) { return wrap(future.thenAccept(action)); } @Override public CalendarFuture thenAcceptAsync(final Consumer action) { return wrap(future.thenAcceptAsync(action)); } @Override public CalendarFuture thenAcceptAsync( final Consumer action, final Executor executor ) { return wrap(future.thenAcceptAsync(action, executor)); } @Override public CalendarFuture thenAcceptBoth( final CompletionStage other, final BiConsumer action ) { return wrap(future.thenAcceptBoth(other, action)); } @Override public CalendarFuture thenAcceptBothAsync( final CompletionStage other, final BiConsumer action ) { return wrap(future.thenAcceptBothAsync(other, action)); } @Override public CalendarFuture thenAcceptBothAsync( final CompletionStage other, final BiConsumer action, final Executor executor ) { return wrap(future.thenAcceptBothAsync(other, action, executor)); } @Override public CalendarFuture thenApply(final Function fn) { return wrap(future.thenApply(fn)); } @Override public CalendarFuture thenApplyAsync(final Function fn) { return wrap(future.thenApplyAsync(fn)); } @Override public CalendarFuture thenApplyAsync( final Function fn, final Executor executor ) { return wrap(future.thenApplyAsync(fn, executor)); } @Override public CalendarFuture thenCombine( final CompletionStage other, final BiFunction fn ) { return wrap(future.thenCombine(other, fn)); } @Override public CalendarFuture thenCombineAsync( final CompletionStage other, final BiFunction fn ) { return wrap(future.thenCombineAsync(other, fn)); } @Override public CalendarFuture thenCombineAsync( final CompletionStage other, final BiFunction fn, final Executor executor ) { return wrap(future.thenCombineAsync(other, fn, executor)); } @Override public CalendarFuture thenCompose(final Function> fn) { return wrap(future.thenCompose(fn)); } @Override public CalendarFuture thenComposeAsync(final Function> fn) { return wrap(future.thenComposeAsync(fn)); } @Override public CalendarFuture thenComposeAsync( final Function> fn, final Executor executor ) { return wrap(future.thenComposeAsync(fn, executor)); } @Override public CalendarFuture thenRun(final Runnable action) { return wrap(future.thenRun(action)); } @Override public CalendarFuture thenRunAsync(final Runnable action) { return wrap(future.thenRunAsync(action)); } @Override public CalendarFuture thenRunAsync( final Runnable action, final Executor executor ) { return wrap(future.thenRunAsync(action, executor)); } @Override public CompletableFuture toCompletableFuture() { return future; } @Override public String toString() { return MessageFormat.format( "WrappedCompletableCalendarFuture'{'clock={0}, future={1}, instant={2}'}'", clock, future, instant ); } @Override public CalendarFuture whenComplete(final BiConsumer action) { return wrap(future.whenComplete(action)); } @Override public CalendarFuture whenCompleteAsync(final BiConsumer action) { return wrap(future.whenCompleteAsync(action)); } @Override public CalendarFuture whenCompleteAsync( final BiConsumer action, final Executor executor ) { return wrap(future.whenCompleteAsync(action, executor)); } private WrappedCompletableCalendarFuture wrap(final CompletableFuture future) { return wrap(clock, instant, future); } } /** * Wraps an existing {@link ScheduledExecutorService} to create a new {@code * DelayBasedCalendarExecutorService}. * * @param executor * The underlying executor. Must not be {@code null}. * * @return A new {@code DelayBasedCalendarExecutorService}. */ public static DelayBasedCalendarExecutorService wrap(final ScheduledExecutorService executor) { return wrap(Clock.systemDefaultZone(), executor); } /** * Wraps an existing {@link ScheduledExecutorService} to create a new {@code * DelayBasedCalendarExecutorService}. * * @param clock * The clock to use to determine the current time. Must not be {@code null}. * @param executor * The underlying executor. Must not be {@code null}. * * @return A new {@code DelayBasedCalendarExecutorService}. */ public static DelayBasedCalendarExecutorService wrap(final Clock clock, final ScheduledExecutorService executor) { if (clock == null) { throw new NullPointerException("The clock must not be null."); } if (executor == null) { throw new NullPointerException("The executor must not be null."); } return new DelayBasedCalendarExecutorService(clock, executor); } private final Clock clock; private final ScheduledExecutorService executor; private Duration adjustmentPeriod = Duration.ofHours(2); /** * Constructs a new {@code DelayBasedCalendarExecutorService}. * * @param executor * The underlying executor. */ private DelayBasedCalendarExecutorService( final Clock clock, final ScheduledExecutorService executor ) { this.clock = clock; this.executor = executor; } @Override public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { return executor.awaitTermination(timeout, unit); } /** * Gets the adjustment period for this service. * * @return The current adjustment period. The period is never {@code null} or negative. */ public Duration getAdjustmentPeriod() { return adjustmentPeriod; } /** * Sets a new adjustment period for this service. *

* All tasks are re-scheduled every adjustment period, in case the internal timer falls out-of-sync with the clock. *

* The default adjustment period is 2 hours. * * @param adjustmentPeriod * The new adjustment period. Must not be {@code null} or negative. */ public void setAdjustmentPeriod(final Duration adjustmentPeriod) { if (adjustmentPeriod == null) { throw new NullPointerException("The adjustment period cannot be null."); } if (adjustmentPeriod.isNegative()) { throw new IllegalArgumentException("The adjustment period cannot be negative."); } this.adjustmentPeriod = adjustmentPeriod; } @Override public Clock getClock() { return clock; } /** * The underlying executor. * * @return The executor. */ public ScheduledExecutorService getExecutor() { return executor; } @Override public boolean isShutdown() { return executor.isShutdown(); } @Override public boolean isTerminated() { return executor.isTerminated(); } @Override public CalendarFuture scheduleDynamically( final Callable action, final Instant initial, final ScheduleCallback callback ) { final AtomicReference> future = new AtomicReference<>(); final ReadWriteLock lock = new ReentrantReadWriteLock(); final CompletableFuture completable = new CompletableFuture() { @Override public boolean cancel(final boolean mayInterruptIfRunning) { lock.readLock() .lock(); try { future.get() .cancel(mayInterruptIfRunning); return super.cancel(mayInterruptIfRunning); } finally { lock.readLock() .unlock(); } } }; lock.writeLock() .lock(); try { final long idealInitialDelay = Duration.between(clock.instant(), initial) .toNanos(); final long maxInitialDelay = adjustmentPeriod.toNanos(); final long initialDelay = Math.min(idealInitialDelay, maxInitialDelay); future.set( executor.schedule( new Callable() { @Override public Void call() { try { final Duration countdown = Duration.between(initial, clock.instant()); if (countdown.isNegative()) { lock.writeLock() .lock(); try { final long idealDelay = countdown.negated() .toNanos(); final long maxDelay = adjustmentPeriod.toNanos(); final long delay = Math.min(idealDelay, maxDelay); future.set(executor.schedule(this, delay, TimeUnit.NANOSECONDS)); } finally { lock.writeLock() .unlock(); } } else { final V result = action.call(); if (callback != null) { final Instant next = callback.getNext(initial, result); if (next != null) { lock.writeLock() .lock(); try { final long idealDelay = Duration.between(clock.instant(), next) .toNanos(); final long maxDelay = adjustmentPeriod.toNanos(); final long delay = Math.min(idealDelay, maxDelay); future.set(executor.schedule(this, delay, TimeUnit.NANOSECONDS)); } finally { lock.writeLock() .unlock(); } } } completable.complete(result); } } catch (final Exception ex) { completable.completeExceptionally(ex); } return null; } }, initialDelay, TimeUnit.NANOSECONDS ) ); } finally { lock.writeLock() .unlock(); } return WrappedCompletableCalendarFuture.wrap(clock, initial, completable); } @Override public void shutdown() { executor.shutdown(); } @Override public List shutdownNow() { return executor.shutdownNow(); } @Override public String toString() { return MessageFormat.format( "DelayBasedCalendarExecutorService'{'clock={0}, executor={1}, adjustmentPeriod={2}'}'", clock, executor, adjustmentPeriod ); } }