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

io.cdap.cdap.internal.app.runtime.AbstractProgramController Maven / Gradle / Ivy

The newest version!
/*
 * 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 io.cdap.cdap.internal.app.runtime;

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 com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.cdap.cdap.app.runtime.ProgramController;
import io.cdap.cdap.common.app.RunIds;
import io.cdap.cdap.proto.id.ProgramRunId;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.apache.twill.api.RunId;
import org.apache.twill.common.Cancellable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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(AbstractProgramController.class);

  private final AtomicReference state;
  private final ProgramRunId programRunId;
  private final Map listeners;
  private final Listener caller;
  private final ExecutorService executor;

  private Throwable failureCause;
  private volatile long gracefulTimeoutMillis;

  protected AbstractProgramController(ProgramRunId programRunId) {
    this.state = new AtomicReference<>(State.STARTING);
    this.programRunId = programRunId;
    this.listeners = new HashMap<>();
    this.caller = new MultiListenerCaller();
    this.gracefulTimeoutMillis = -1L;

    // 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.
    String name = programRunId.getParent() + "-" + programRunId.getRun();
    this.executor = new ThreadPoolExecutor(0, 1, 0, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(),
        new ThreadFactoryBuilder()
            .setNameFormat("pcontroller-" + name + "-%d").build());
  }

  @Override
  public ProgramRunId getProgramRunId() {
    return programRunId;
  }

  @Override
  public RunId getRunId() {
    return RunIds.fromString(programRunId.getRun());
  }

  @Override
  public final ListenableFuture suspend() {
    LOG.trace("Suspend program {}", programRunId);
    if (!state.compareAndSet(State.ALIVE, State.SUSPENDING)) {
      return Futures.immediateFailedFuture(
          new IllegalStateException(
              "Suspension not allowed for " + programRunId + " in " + state.get()));
    }
    final SettableFuture result = SettableFuture.create();
    executor.execute(() -> {
      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 resume() {
    LOG.trace("Resume program {}", programRunId);
    if (!state.compareAndSet(State.SUSPENDED, State.RESUMING)) {
      return Futures.immediateFailedFuture(
          new IllegalStateException(
              "Resumption not allowed for " + programRunId + " in " + state.get()));
    }
    final SettableFuture result = SettableFuture.create();
    executor.execute(() -> {
      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 stop() {
    LOG.trace("Stop program {}", programRunId);
    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 " + programRunId + " in " + state.get()));
    }
    final SettableFuture result = SettableFuture.create();
    executor.execute(() -> {
      try {
        caller.stopping();
        doStop();
        state.set(State.KILLED);
        result.set(AbstractProgramController.this);
        caller.killed();
      } catch (Throwable t) {
        error(t, result);
      }
    });
    return result;
  }

  @Override
  public final ListenableFuture stop(long timeout, TimeUnit timeoutUnit) {
    if (timeout < 0) {
      throw new IllegalStateException("Stop timeout must be >= 0");
    }

    LOG.trace("Stop program {} with timeout {}{}", programRunId, timeout, timeoutUnit);
    this.gracefulTimeoutMillis = timeoutUnit.toMillis(timeout);
    return stop();
  }

  @Override
  public void kill() {
    stop();
  }

  /**
   * Returns the graceful stop timeout in milliseconds. If it returns a value that is smaller than
   * {@code 0}, if means graceful stop timeout is not defined. It is up to the implementation of the
   * controller to determine stop timeout.
   */
  protected final long getGracefulTimeoutMillis() {
    return gracefulTimeoutMillis;
  }

  /**
   * Children call this method to signal the program is completed.
   */
  protected void complete() {
    complete(State.COMPLETED);
  }

  protected void complete(final State completionState) {
    LOG.trace("Program {} completed with state {}", programRunId, completionState);
    if (!state.compareAndSet(State.STARTING, completionState)
        && !state.compareAndSet(State.ALIVE, completionState)
        && !state.compareAndSet(State.SUSPENDED, completionState)) {
      LOG.warn("Cannot transit to COMPLETED state from {} state: {}", state.get(), programRunId);
      return;
    }
    executor.execute(() -> {
      state.set(completionState);
      if (State.KILLED.equals(completionState)) {
        caller.killed();
      } else if (State.ERROR.equals(completionState)) {
        // mark program as error when its in error state.
        caller.error(
            new Exception(String.format("Program %s completed with exception.", programRunId)));
      } else {
        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 = () -> {
      // Simply remove the listener from the map through the executor and block on the completion
      Futures.getUnchecked(executor.submit(() -> {
        listeners.remove(caller);
      }));
    };

    try {
      // Use a blocking queue to communicate the Cancellable to return
      BlockingQueue result = new ArrayBlockingQueue<>(1);

      // Use the single thread executor to add the listener and call init
      executor.submit(() -> {
        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 command(final String name, final Object value) {
    final SettableFuture result = SettableFuture.create();
    executor.execute(() -> {
      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(() -> error(t, null));
  }

  /**
   * Children call this method to signal the program is started.
   */
  protected final void started() {
    LOG.trace("Program {} started", programRunId);
    if (!state.compareAndSet(State.STARTING, State.ALIVE)) {
      LOG.debug("Cannot transit to ALIVE state from {} state: {}", state.get(), programRunId);
      return;
    }
    executor.execute(() -> {
      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  void error(Throwable t, SettableFuture future) {
    LOG.trace("Program {} forced to error", programRunId, t);
    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;
    }

    private void execute(Runnable runnable, String methodName) {
      try {
        executor.execute(runnable);
      } catch (Throwable t) {
        String msg = String.format(
            "Exception while executing method '%s' on listener %s with executor %s.",
            methodName, listener, executor);
        LOG.error(msg, t);
      }
    }

    @Override
    public void init(final State currentState, @Nullable final Throwable cause) {
      execute(() -> listener.init(currentState, cause), "init");
    }

    @Override
    public void suspending() {
      execute(listener::suspending, "suspending");
    }

    @Override
    public void suspended() {
      execute(listener::suspended, "suspended");
    }

    @Override
    public void resuming() {
      execute(listener::resuming, "resuming");
    }

    @Override
    public void alive() {
      execute(listener::alive, "alive");
    }

    @Override
    public void stopping() {
      execute(listener::stopping, "stopping");
    }

    @Override
    public void completed() {
      execute(listener::completed, "completed");
    }

    @Override
    public void killed() {
      execute(listener::killed, "killed");
    }

    @Override
    public void error(final Throwable cause) {
      execute(() -> listener.error(cause), "error");
    }

    @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);
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy