/* * Copyright © 2014 Cask Data, Inc. * * 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 co.cask.cdap.internal.app.runtime; import co.cask.cdap.app.runtime.ProgramController; import co.cask.cdap.proto.Id; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Throwables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import org.apache.twill.api.RunId; import org.apache.twill.common.Cancellable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; /** * Abstract base implementation of {@link ProgramController} that governs state transitions as well as * {@link Listener} invocation mechanism. */ public abstract class AbstractProgramController implements ProgramController { private static final Logger LOG = LoggerFactory.getLogger(ProgramController.class); private final AtomicReference<State> state; private final Id.Program programId; private final RunId runId; private final String componentName; private final Map<ListenerCaller, Cancellable> listeners; private final Listener caller; private final ExecutorService executor; private final String name; private Throwable failureCause; protected AbstractProgramController(final Id.Program programId, RunId runId) { this(programId, runId, null); } protected AbstractProgramController(Id.Program programId, RunId runId, @Nullable String componentName) { this.state = new AtomicReference<>(State.STARTING); this.programId = programId; this.runId = runId; this.componentName = componentName; this.listeners = new HashMap<>(); this.caller = new MultiListenerCaller(); this.name = programId + (componentName == null ? "" : "-" + componentName) + "-" + runId.getId(); // Create a single thread executor that doesn't keep core thread and the thread will shutdown when there // is no pending task. In this way, we don't need to shutdown the executor since there will be no thread // hanging around when it is idle. this.executor = new ThreadPoolExecutor(0, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(null, r, "pcontroller-" + name); } }); } @Override public Id.Program getProgramId() { return programId; } @Override public RunId getRunId() { return runId; } @Nullable @Override public String getComponentName() { return componentName; } @Override public final ListenableFuture<ProgramController> suspend() { if (!state.compareAndSet(State.ALIVE, State.SUSPENDING)) { return Futures.immediateFailedFuture( new IllegalStateException("Suspension not allowed for " + programId + " in " + state.get())); } final SettableFuture<ProgramController> result = SettableFuture.create(); executor.execute(new Runnable() { @Override public void run() { try { caller.suspending(); doSuspend(); state.set(State.SUSPENDED); result.set(AbstractProgramController.this); caller.suspended(); } catch (Throwable t) { error(t, result); } } }); return result; } @Override public final ListenableFuture<ProgramController> resume() { if (!state.compareAndSet(State.SUSPENDED, State.RESUMING)) { return Futures.immediateFailedFuture( new IllegalStateException("Resumption not allowed for " + name + " in " + state.get())); } final SettableFuture<ProgramController> result = SettableFuture.create(); executor.execute(new Runnable() { @Override public void run() { try { caller.resuming(); doResume(); state.set(State.ALIVE); result.set(AbstractProgramController.this); caller.alive(); } catch (Throwable t) { error(t, result); } } }); return result; } @Override public final ListenableFuture<ProgramController> stop() { if (!state.compareAndSet(State.STARTING, State.STOPPING) && !state.compareAndSet(State.ALIVE, State.STOPPING) && !state.compareAndSet(State.SUSPENDED, State.STOPPING)) { return Futures.immediateFailedFuture( new IllegalStateException("Stopping not allowed for " + name + " in " + state.get())); } final SettableFuture<ProgramController> result = SettableFuture.create(); executor.execute(new Runnable() { @Override public void run() { try { caller.stopping(); doStop(); state.set(State.KILLED); result.set(AbstractProgramController.this); caller.killed(); } catch (Throwable t) { error(t, result); } } }); return result; } /** * Children call this method to signal the program is completed. */ protected void complete() { if (!state.compareAndSet(State.ALIVE, State.COMPLETED)) { LOG.debug("Cannot transit to COMPLETED state from {} state: {} {}", state.get(), name); return; } executor.execute(new Runnable() { @Override public void run() { state.set(State.COMPLETED); caller.completed(); } }); } @Override public final Cancellable addListener(Listener listener, final Executor listenerExecutor) { Preconditions.checkNotNull(listener, "Listener shouldn't be null."); Preconditions.checkNotNull(listenerExecutor, "Executor shouldn't be null."); final ListenerCaller caller = new ListenerCaller(listener, listenerExecutor); final Cancellable cancellable = new Cancellable() { @Override public void cancel() { // Simply remove the listener from the map through the executor and block on the completion Futures.getUnchecked(executor.submit(new Runnable() { @Override public void run() { listeners.remove(caller); } })); } }; try { // Use a synchronous queue to communicate the Cancellable to return final SynchronousQueue<Cancellable> result = new SynchronousQueue<>(); // Use the single thread executor to add the listener and call init executor.submit(new Callable<Void>() { @Override public Void call() throws Exception { Cancellable existing = listeners.get(caller); if (existing == null) { listeners.put(caller, cancellable); result.put(cancellable); caller.init(getState(), getFailureCause()); } else { result.put(existing); } return null; } }); return result.take(); } catch (Exception e) { // Not expecting exception since the Callable only do action on Map and calling caller.init, which // already have exceptions handled inside the method. Also, we never shutdown the executor explicitly, // there shouldn't be interrupted exception as well. throw Throwables.propagate(Throwables.getRootCause(e)); } } @Override public final ListenableFuture<ProgramController> command(final String name, final Object value) { final SettableFuture<ProgramController> result = SettableFuture.create(); executor.execute(new Runnable() { @Override public void run() { try { doCommand(name, value); result.set(AbstractProgramController.this); } catch (Throwable t) { error(t, result); } } }); return result; } @Override public final State getState() { return state.get(); } @Override public final Throwable getFailureCause() { return failureCause; } /** * Force this controller into error state. * @param t The failure cause */ protected final void error(final Throwable t) { executor.execute(new Runnable() { @Override public void run() { error(t, null); } }); } /** * Children call this method to signal the program is started. */ protected final void started() { if (!state.compareAndSet(State.STARTING, State.ALIVE)) { LOG.debug("Cannot transit to ALIVE state from {} state: {}", state.get(), name); return; } executor.execute(new Runnable() { @Override public void run() { state.set(State.ALIVE); caller.alive(); } }); } protected abstract void doSuspend() throws Exception; protected abstract void doResume() throws Exception; protected abstract void doStop() throws Exception; protected abstract void doCommand(String name, Object value) throws Exception; /** * Force this controller into error state and set the failure into the given future. * This method should only be called from the single thread executor of this class. * @param t The failure cause */ private <V> void error(Throwable t, SettableFuture<V> future) { failureCause = t; state.set(State.ERROR); if (future != null) { future.setException(t); } caller.error(t); } /** * Class for making calls to multiple {@link Listener}s on state change. */ private final class MultiListenerCaller implements Listener { @Override public void init(State currentState, @Nullable Throwable cause) { for (ListenerCaller caller : listeners.keySet()) { caller.init(currentState, cause); } } @Override public void suspending() { for (ListenerCaller caller : listeners.keySet()) { caller.suspending(); } } @Override public void suspended() { for (ListenerCaller caller : listeners.keySet()) { caller.suspended(); } } @Override public void resuming() { for (ListenerCaller caller : listeners.keySet()) { caller.resuming(); } } @Override public void alive() { for (ListenerCaller caller : listeners.keySet()) { caller.alive(); } } @Override public void stopping() { for (ListenerCaller caller : listeners.keySet()) { caller.stopping(); } } @Override public void completed() { for (ListenerCaller caller : listeners.keySet()) { caller.completed(); } } @Override public void killed() { for (ListenerCaller caller : listeners.keySet()) { caller.killed(); } } @Override public void error(Throwable cause) { for (ListenerCaller caller : listeners.keySet()) { caller.error(cause); } } } /** * Wrapper for making calls to {@link Listener} through an {@link Executor}. */ private static final class ListenerCaller implements Listener { private final Listener listener; private final Executor executor; private ListenerCaller(Listener listener, Executor executor) { this.listener = listener; this.executor = executor; } @Override public void init(final State currentState, @Nullable final Throwable cause) { executor.execute(new Runnable() { @Override public void run() { listener.init(currentState, cause); } }); } @Override public void suspending() { executor.execute(new Runnable() { @Override public void run() { listener.suspending(); } }); } @Override public void suspended() { executor.execute(new Runnable() { @Override public void run() { listener.suspended(); } }); } @Override public void resuming() { executor.execute(new Runnable() { @Override public void run() { listener.resuming(); } }); } @Override public void alive() { executor.execute(new Runnable() { @Override public void run() { listener.alive(); } }); } @Override public void stopping() { executor.execute(new Runnable() { @Override public void run() { listener.stopping(); } }); } @Override public void completed() { executor.execute(new Runnable() { @Override public void run() { listener.completed(); } }); } @Override public void killed() { executor.execute(new Runnable() { @Override public void run() { listener.killed(); } }); } @Override public void error(final Throwable cause) { executor.execute(new Runnable() { @Override public void run() { listener.error(cause); } }); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } // Only compare with the listener ListenerCaller other = (ListenerCaller) o; return Objects.equal(listener, other.listener); } @Override public int hashCode() { return Objects.hashCode(listener); } } }