/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.flink.streaming.runtime.tasks;
import static org.apache.flink.util.Preconditions.checkNotNull;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Delayed;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.util.Preconditions;
/**
* A {@link ProcessingTimeService} which assigns as current processing time the result of calling
* {@link System#currentTimeMillis()} and registers timers using a {@link ScheduledThreadPoolExecutor}.
*/
public class SystemProcessingTimeService extends ProcessingTimeService {
private static final int STATUS_ALIVE = 0;
private static final int STATUS_QUIESCED = 1;
private static final int STATUS_SHUTDOWN = 2;
// ------------------------------------------------------------------------
/** The containing task that owns this time service provider. */
private final AsyncExceptionHandler task;
/** The lock that timers acquire upon triggering. */
private final Object checkpointLock;
/** The executor service that schedules and calls the triggers of this task. */
private final ScheduledThreadPoolExecutor timerService;
private final AtomicInteger status;
public SystemProcessingTimeService(AsyncExceptionHandler failureHandler, Object checkpointLock) {
this(failureHandler, checkpointLock, null);
}
public SystemProcessingTimeService(
AsyncExceptionHandler task,
Object checkpointLock,
ThreadFactory threadFactory) {
this.task = checkNotNull(task);
this.checkpointLock = checkNotNull(checkpointLock);
this.status = new AtomicInteger(STATUS_ALIVE);
if (threadFactory == null) {
this.timerService = new ScheduledThreadPoolExecutor(1);
} else {
this.timerService = new ScheduledThreadPoolExecutor(1, threadFactory);
}
// tasks should be removed if the future is canceled
this.timerService.setRemoveOnCancelPolicy(true);
// make sure shutdown removes all pending tasks
this.timerService.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
this.timerService.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
}
@Override
public long getCurrentProcessingTime() {
return System.currentTimeMillis();
}
/**
* Registers a task to be executed no sooner than time {@code timestamp}, but without strong
* guarantees of order.
*
* @param timestamp Time when the task is to be enabled (in processing time)
* @param target The task to be executed
* @return The future that represents the scheduled task. This always returns some future,
* even if the timer was shut down
*/
@Override
public ScheduledFuture<?> registerTimer(long timestamp, ProcessingTimeCallback target) {
long delay = Math.max(timestamp - getCurrentProcessingTime(), 0);
// we directly try to register the timer and only react to the status on exception
// that way we save unnecessary volatile accesses for each timer
try {
return timerService.schedule(
new TriggerTask(task, checkpointLock, target, timestamp), delay, TimeUnit.MILLISECONDS);
}
catch (RejectedExecutionException e) {
final int status = this.status.get();
if (status == STATUS_QUIESCED) {
return new NeverCompleteFuture(delay);
}
else if (status == STATUS_SHUTDOWN) {
throw new IllegalStateException("Timer service is shut down");
}
else {
// something else happened, so propagate the exception
throw e;
}
}
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(ProcessingTimeCallback callback, long initialDelay, long period) {
long nextTimestamp = getCurrentProcessingTime() + initialDelay;
// we directly try to register the timer and only react to the status on exception
// that way we save unnecessary volatile accesses for each timer
try {
return timerService.scheduleAtFixedRate(
new RepeatedTriggerTask(task, checkpointLock, callback, nextTimestamp, period),
initialDelay,
period,
TimeUnit.MILLISECONDS);
} catch (RejectedExecutionException e) {
final int status = this.status.get();
if (status == STATUS_QUIESCED) {
return new NeverCompleteFuture(initialDelay);
}
else if (status == STATUS_SHUTDOWN) {
throw new IllegalStateException("Timer service is shut down");
}
else {
// something else happened, so propagate the exception
throw e;
}
}
}
@Override
public boolean isTerminated() {
return status.get() == STATUS_SHUTDOWN;
}
@Override
public void quiesceAndAwaitPending() throws InterruptedException {
if (status.compareAndSet(STATUS_ALIVE, STATUS_QUIESCED)) {
timerService.shutdown();
// await forever (almost)
timerService.awaitTermination(365, TimeUnit.DAYS);
}
}
@Override
public void shutdownService() {
if (status.compareAndSet(STATUS_ALIVE, STATUS_SHUTDOWN) ||
status.compareAndSet(STATUS_QUIESCED, STATUS_SHUTDOWN)) {
timerService.shutdownNow();
}
}
// safety net to destroy the thread pool
@Override
protected void finalize() throws Throwable {
super.finalize();
timerService.shutdownNow();
}
@VisibleForTesting
int getNumTasksScheduled() {
BlockingQueue<?> queue = timerService.getQueue();
if (queue == null) {
return 0;
} else {
return queue.size();
}
}
// ------------------------------------------------------------------------
/**
* Internal task that is invoked by the timer service and triggers the target.
*/
private static final class TriggerTask implements Runnable {
private final Object lock;
private final ProcessingTimeCallback target;
private final long timestamp;
private final AsyncExceptionHandler exceptionHandler;
TriggerTask(AsyncExceptionHandler exceptionHandler, final Object lock, ProcessingTimeCallback target, long timestamp) {
this.exceptionHandler = exceptionHandler;
this.lock = lock;
this.target = target;
this.timestamp = timestamp;
}
@Override
public void run() {
synchronized (lock) {
try {
target.onProcessingTime(timestamp);
} catch (Throwable t) {
TimerException asyncException = new TimerException(t);
exceptionHandler.handleAsyncException("Caught exception while processing timer.", asyncException);
}
}
}
}
/**
* Internal task which is repeatedly called by the processing time service.
*/
private static final class RepeatedTriggerTask implements Runnable {
private final Object lock;
private final ProcessingTimeCallback target;
private final long period;
private final AsyncExceptionHandler exceptionHandler;
private long nextTimestamp;
private RepeatedTriggerTask(
AsyncExceptionHandler exceptionHandler,
Object lock,
ProcessingTimeCallback target,
long nextTimestamp,
long period) {
this.lock = Preconditions.checkNotNull(lock);
this.target = Preconditions.checkNotNull(target);
this.period = period;
this.exceptionHandler = Preconditions.checkNotNull(exceptionHandler);
this.nextTimestamp = nextTimestamp;
}
@Override
public void run() {
try {
synchronized (lock) {
target.onProcessingTime(nextTimestamp);
}
nextTimestamp += period;
} catch (Throwable t) {
TimerException asyncException = new TimerException(t);
exceptionHandler.handleAsyncException("Caught exception while processing repeated timer task.", asyncException);
}
}
}
// ------------------------------------------------------------------------
private static final class NeverCompleteFuture implements ScheduledFuture<Object> {
private final Object lock = new Object();
private final long delayMillis;
private volatile boolean canceled;
private NeverCompleteFuture(long delayMillis) {
this.delayMillis = delayMillis;
}
@Override
public long getDelay(@Nonnull TimeUnit unit) {
return unit.convert(delayMillis, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(@Nonnull Delayed o) {
long otherMillis = o.getDelay(TimeUnit.MILLISECONDS);
return Long.compare(this.delayMillis, otherMillis);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
synchronized (lock) {
canceled = true;
lock.notifyAll();
}
return true;
}
@Override
public boolean isCancelled() {
return canceled;
}
@Override
public boolean isDone() {
return false;
}
@Override
public Object get() throws InterruptedException {
synchronized (lock) {
while (!canceled) {
lock.wait();
}
}
throw new CancellationException();
}
@Override
public Object get(long timeout, @Nonnull TimeUnit unit) throws InterruptedException, TimeoutException {
synchronized (lock) {
while (!canceled) {
unit.timedWait(lock, timeout);
}
if (canceled) {
throw new CancellationException();
} else {
throw new TimeoutException();
}
}
}
}
}