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

org.apache.hadoop.hbase.procedure2.Procedure Maven / Gradle / Ivy

There is a newer version: 4.15.0-HBase-1.5
Show newest version
/**
 * 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.hadoop.hbase.procedure2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.ProcedureInfo;
import org.apache.hadoop.hbase.classification.InterfaceAudience;
import org.apache.hadoop.hbase.classification.InterfaceStability;
import org.apache.hadoop.hbase.exceptions.TimeoutIOException;
import org.apache.hadoop.hbase.procedure2.util.StringUtils;
import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos;
import org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureState;
import org.apache.hadoop.hbase.util.ByteStringer;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.NonceKey;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;

/**
 * Base Procedure class responsible to handle the Procedure Metadata
 * e.g. state, startTime, lastUpdate, stack-indexes, ...
 *
 * execute() is called each time the procedure is executed.
 * it may be called multiple times in case of failure and restart, so the
 * code must be idempotent.
 * the return is a set of sub-procedures or null in case the procedure doesn't
 * have sub-procedures. Once the sub-procedures are successfully completed
 * the execute() method is called again, you should think at it as a stack:
 *  -> step 1
 *  ---> step 2
 *  -> step 1
 *
 * rollback() is called when the procedure or one of the sub-procedures is failed.
 * the rollback step is supposed to cleanup the resources created during the
 * execute() step. in case of failure and restart rollback() may be called
 * multiple times, so the code must be idempotent.
 */
@InterfaceAudience.Private
@InterfaceStability.Evolving
public abstract class Procedure implements Comparable {
  // unchanged after initialization
  private String owner = null;
  private Long parentProcId = null;
  private Long procId = null;
  private long startTime;

  // runtime state, updated every operation
  private ProcedureState state = ProcedureState.INITIALIZING;
  private Integer timeout = null;
  private int[] stackIndexes = null;
  private int childrenLatch = 0;
  private long lastUpdate;

  private RemoteProcedureException exception = null;
  private byte[] result = null;

  private NonceKey nonceKey = null;

  /**
   * The main code of the procedure. It must be idempotent since execute()
   * may be called multiple time in case of machine failure in the middle
   * of the execution.
   * @param env the environment passed to the ProcedureExecutor
   * @return a set of sub-procedures or null if there is nothing else to execute.
   * @throws ProcedureYieldException the procedure will be added back to the queue and retried later
   * @throws InterruptedException the procedure will be added back to the queue and retried later
   */
  protected abstract Procedure[] execute(TEnvironment env)
    throws ProcedureYieldException, InterruptedException;

  /**
   * The code to undo what done by the execute() code.
   * It is called when the procedure or one of the sub-procedure failed or an
   * abort was requested. It should cleanup all the resources created by
   * the execute() call. The implementation must be idempotent since rollback()
   * may be called multiple time in case of machine failure in the middle
   * of the execution.
   * @param env the environment passed to the ProcedureExecutor
   * @throws IOException temporary failure, the rollback will retry later
   * @throws InterruptedException the procedure will be added back to the queue and retried later
   */
  protected abstract void rollback(TEnvironment env)
    throws IOException, InterruptedException;

  /**
   * The abort() call is asynchronous and each procedure must decide how to deal
   * with that, if they want to be abortable. The simplest implementation
   * is to have an AtomicBoolean set in the abort() method and then the execute()
   * will check if the abort flag is set or not.
   * abort() may be called multiple times from the client, so the implementation
   * must be idempotent.
   *
   * NOTE: abort() is not like Thread.interrupt() it is just a notification
   * that allows the procedure implementor where to abort to avoid leak and
   * have a better control on what was executed and what not.
   */
  protected abstract boolean abort(TEnvironment env);

  /**
   * The user-level code of the procedure may have some state to
   * persist (e.g. input arguments) to be able to resume on failure.
   * @param stream the stream that will contain the user serialized data
   */
  protected abstract void serializeStateData(final OutputStream stream)
    throws IOException;

  /**
   * Called on store load to allow the user to decode the previously serialized
   * state.
   * @param stream the stream that contains the user serialized data
   */
  protected abstract void deserializeStateData(final InputStream stream)
    throws IOException;

  /**
   * The user should override this method, and try to take a lock if necessary.
   * A lock can be anything, and it is up to the implementor.
   * Example: in our Master we can execute request in parallel for different tables
   *          create t1 and create t2 can be executed at the same time.
   *          anything else on t1/t2 is queued waiting that specific table create to happen.
   *
   * @return true if the lock was acquired and false otherwise
   */
  protected boolean acquireLock(final TEnvironment env) {
    return true;
  }

  /**
   * The user should override this method, and release lock if necessary.
   */
  protected void releaseLock(final TEnvironment env) {
    // no-op
  }

  /**
   * Called when the procedure is loaded for replay.
   * The procedure implementor may use this method to perform some quick
   * operation before replay.
   * e.g. failing the procedure if the state on replay may be unknown.
   */
  protected void beforeReplay(final TEnvironment env) {
    // no-op
  }

  /**
   * Called when the procedure is marked as completed (success or rollback).
   * The procedure implementor may use this method to cleanup in-memory states.
   * This operation will not be retried on failure.
   */
  protected void completionCleanup(final TEnvironment env) {
    // no-op
  }

  /**
   * By default, the executor will try ro run procedures start to finish.
   * Return true to make the executor yield between each execution step to
   * give other procedures time to run their steps.
   * @param env the environment passed to the ProcedureExecutor
   * @return Return true if the executor should yield on completion of an execution step.
   *         Defaults to return false.
   */
  protected boolean isYieldAfterExecutionStep(final TEnvironment env) {
    return false;
  }

  /**
   * By default, the executor will keep the procedure result around util
   * the eviction TTL is expired. The client can cut down the waiting time
   * by requesting that the result is removed from the executor.
   * In case of system started procedure, we can force the executor to auto-ack.
   * @param env the environment passed to the ProcedureExecutor
   * @return true if the executor should wait the client ack for the result.
   *         Defaults to return true.
   */
  protected boolean shouldWaitClientAck(final TEnvironment env) {
    return true;
  }

  @Override
  public String toString() {
    // Return the simple String presentation of the procedure.
    return toStringSimpleSB().toString();
  }

  /**
   * Build the StringBuilder for the simple form of
   * procedure string.
   * @return the StringBuilder
   */
  protected StringBuilder toStringSimpleSB() {
    StringBuilder sb = new StringBuilder();
    toStringClassDetails(sb);

    if (procId != null) {
      sb.append(" id=");
      sb.append(getProcId());
    }

    if (hasParent()) {
      sb.append(" parent=");
      sb.append(getParentProcId());
    }

    if (hasOwner()) {
      sb.append(" owner=");
      sb.append(getOwner());
    }

    sb.append(" state=");
    toStringState(sb);

    return sb;
  }

  /**
   * Extend the toString() information with more procedure
   * details
   */
  public String toStringDetails() {
    StringBuilder sb = toStringSimpleSB();

    sb.append(" startTime=");
    sb.append(getStartTime());

    sb.append(" lastUpdate=");
    sb.append(getLastUpdate());

    int[] stackIndices = getStackIndexes();
    if (stackIndices != null) {
      sb.append("\n");
      sb.append("stackIndexes=");
      sb.append(Arrays.toString(stackIndices));
    }

    return sb.toString();
  }

  protected String toStringClass() {
    StringBuilder sb = new StringBuilder();
    toStringClassDetails(sb);

    return sb.toString();
  }

  /**
   * Called from {@link #toString()} when interpolating {@link Procedure} state
   * @param builder Append current {@link ProcedureState}
   */
  protected void toStringState(StringBuilder builder) {
    builder.append(getState());
  }

  /**
   * Extend the toString() information with the procedure details
   * e.g. className and parameters
   * @param builder the string builder to use to append the proc specific information
   */
  protected void toStringClassDetails(StringBuilder builder) {
    builder.append(getClass().getName());
  }

  /**
   * @return the serialized result if any, otherwise null
   */
  public byte[] getResult() {
    return result;
  }

  /**
   * The procedure may leave a "result" on completion.
   * @param result the serialized result that will be passed to the client
   */
  protected void setResult(final byte[] result) {
    this.result = result;
  }

  public long getProcId() {
    return procId;
  }

  public boolean hasParent() {
    return parentProcId != null;
  }

  public boolean hasException() {
    return exception != null;
  }

  public boolean hasTimeout() {
    return timeout != null;
  }

  public long getParentProcId() {
    return parentProcId;
  }

  public NonceKey getNonceKey() {
    return nonceKey;
  }

  /**
   * @return true if the procedure is in a RUNNABLE state.
   */
  protected synchronized boolean isRunnable() {
    return state == ProcedureState.RUNNABLE;
  }

  /**
   * @return true if the procedure has failed.
   *         true may mean failed but not yet rolledback or failed and rolledback.
   */
  public synchronized boolean isFailed() {
    return exception != null || state == ProcedureState.ROLLEDBACK;
  }

  /**
   * @return true if the procedure is finished successfully.
   */
  public synchronized boolean isSuccess() {
    return state == ProcedureState.FINISHED && exception == null;
  }

  /**
   * @return true if the procedure is finished. The Procedure may be completed
   *         successfuly or failed and rolledback.
   */
  public synchronized boolean isFinished() {
    switch (state) {
      case ROLLEDBACK:
        return true;
      case FINISHED:
        return exception == null;
      default:
        break;
    }
    return false;
  }

  /**
   * @return true if the procedure is waiting for a child to finish or for an external event.
   */
  public synchronized boolean isWaiting() {
    switch (state) {
      case WAITING:
      case WAITING_TIMEOUT:
        return true;
      default:
        break;
    }
    return false;
  }

  public synchronized RemoteProcedureException getException() {
    return exception;
  }

  public long getStartTime() {
    return startTime;
  }

  public synchronized long getLastUpdate() {
    return lastUpdate;
  }

  public synchronized long elapsedTime() {
    return lastUpdate - startTime;
  }

  /**
   * @param timeout timeout in msec
   */
  protected void setTimeout(final int timeout) {
    this.timeout = timeout;
  }

  /**
   * @return the timeout in msec
   */
  public int getTimeout() {
    return timeout;
  }

  /**
   * @return the remaining time before the timeout
   */
  public long getTimeRemaining() {
    return Math.max(0, timeout - (EnvironmentEdgeManager.currentTime() - startTime));
  }

  @VisibleForTesting
  @InterfaceAudience.Private
  public void setOwner(final String owner) {
    this.owner = StringUtils.isEmpty(owner) ? null : owner;
  }

  public String getOwner() {
    return owner;
  }

  public boolean hasOwner() {
    return owner != null;
  }

  @VisibleForTesting
  @InterfaceAudience.Private
  protected synchronized void setState(final ProcedureState state) {
    this.state = state;
    updateTimestamp();
  }

  @InterfaceAudience.Private
  protected synchronized ProcedureState getState() {
    return state;
  }

  protected void setFailure(final String source, final Throwable cause) {
    setFailure(new RemoteProcedureException(source, cause));
  }

  protected synchronized void setFailure(final RemoteProcedureException exception) {
    this.exception = exception;
    if (!isFinished()) {
      setState(ProcedureState.FINISHED);
    }
  }

  protected void setAbortFailure(final String source, final String msg) {
    setFailure(source, new ProcedureAbortedException(msg));
  }

  @InterfaceAudience.Private
  protected synchronized boolean setTimeoutFailure() {
    if (state == ProcedureState.WAITING_TIMEOUT) {
      long timeDiff = EnvironmentEdgeManager.currentTime() - lastUpdate;
      setFailure("ProcedureExecutor", new TimeoutIOException(
        "Operation timed out after " + StringUtils.humanTimeDiff(timeDiff)));
      return true;
    }
    return false;
  }

  /**
   * Called by the ProcedureExecutor to assign the ID to the newly created procedure.
   */
  @VisibleForTesting
  @InterfaceAudience.Private
  protected void setProcId(final long procId) {
    this.procId = procId;
    this.startTime = EnvironmentEdgeManager.currentTime();
    setState(ProcedureState.RUNNABLE);
  }

  /**
   * Called by the ProcedureExecutor to assign the parent to the newly created procedure.
   */
  @InterfaceAudience.Private
  protected void setParentProcId(final long parentProcId) {
    this.parentProcId = parentProcId;
  }

  /**
   * Called by the ProcedureExecutor to set the value to the newly created procedure.
   */
  @VisibleForTesting
  @InterfaceAudience.Private
  protected void setNonceKey(final NonceKey nonceKey) {
    this.nonceKey = nonceKey;
  }

  /**
   * Internal method called by the ProcedureExecutor that starts the
   * user-level code execute().
   */
  @InterfaceAudience.Private
  protected Procedure[] doExecute(final TEnvironment env)
      throws ProcedureYieldException, InterruptedException {
    try {
      updateTimestamp();
      return execute(env);
    } finally {
      updateTimestamp();
    }
  }

  /**
   * Internal method called by the ProcedureExecutor that starts the
   * user-level code rollback().
   */
  @InterfaceAudience.Private
  protected void doRollback(final TEnvironment env)
      throws IOException, InterruptedException {
    try {
      updateTimestamp();
      rollback(env);
    } finally {
      updateTimestamp();
    }
  }

  /**
   * Called on store load to initialize the Procedure internals after
   * the creation/deserialization.
   */
  @InterfaceAudience.Private
  protected void setStartTime(final long startTime) {
    this.startTime = startTime;
  }

  /**
   * Called on store load to initialize the Procedure internals after
   * the creation/deserialization.
   */
  private synchronized void setLastUpdate(final long lastUpdate) {
    this.lastUpdate = lastUpdate;
  }

  protected synchronized void updateTimestamp() {
    this.lastUpdate = EnvironmentEdgeManager.currentTime();
  }

  /**
   * Called by the ProcedureExecutor on procedure-load to restore the latch state
   */
  @InterfaceAudience.Private
  protected synchronized void setChildrenLatch(final int numChildren) {
    this.childrenLatch = numChildren;
  }

  /**
   * Called by the ProcedureExecutor on procedure-load to restore the latch state
   */
  @InterfaceAudience.Private
  protected synchronized void incChildrenLatch() {
    // TODO: can this be inferred from the stack? I think so...
    this.childrenLatch++;
  }

  /**
   * Called by the ProcedureExecutor to notify that one of the sub-procedures
   * has completed.
   */
  @InterfaceAudience.Private
  protected synchronized boolean childrenCountDown() {
    assert childrenLatch > 0;
    return --childrenLatch == 0;
  }

  @InterfaceAudience.Private
  protected synchronized boolean hasChildren() {
    return childrenLatch > 0;
  }

  /**
   * Called by the RootProcedureState on procedure execution.
   * Each procedure store its stack-index positions.
   */
  @InterfaceAudience.Private
  protected synchronized void addStackIndex(final int index) {
    if (stackIndexes == null) {
      stackIndexes = new int[] { index };
    } else {
      int count = stackIndexes.length;
      stackIndexes = Arrays.copyOf(stackIndexes, count + 1);
      stackIndexes[count] = index;
    }
  }

  @InterfaceAudience.Private
  protected synchronized boolean removeStackIndex() {
    if (stackIndexes != null && stackIndexes.length > 1) {
      stackIndexes = Arrays.copyOf(stackIndexes, stackIndexes.length - 1);
      return false;
    } else {
      stackIndexes = null;
      return true;
    }
  }

  /**
   * Called on store load to initialize the Procedure internals after
   * the creation/deserialization.
   */
  @InterfaceAudience.Private
  protected synchronized void setStackIndexes(final List stackIndexes) {
    this.stackIndexes = new int[stackIndexes.size()];
    for (int i = 0; i < this.stackIndexes.length; ++i) {
      this.stackIndexes[i] = stackIndexes.get(i);
    }
  }

  @InterfaceAudience.Private
  protected synchronized boolean wasExecuted() {
    return stackIndexes != null;
  }

  @InterfaceAudience.Private
  protected synchronized int[] getStackIndexes() {
    return stackIndexes;
  }

  @Override
  public int compareTo(final Procedure other) {
    long diff = getProcId() - other.getProcId();
    return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
  }

  /**
   * Get an hashcode for the specified Procedure ID
   * @return the hashcode for the specified procId
   */
  public static long getProcIdHashCode(final long procId) {
    long h = procId;
    h ^= h >> 16;
    h *= 0x85ebca6b;
    h ^= h >> 13;
    h *= 0xc2b2ae35;
    h ^= h >> 16;
    return h;
  }

  /*
   * Helper to lookup the root Procedure ID given a specified procedure.
   */
  @InterfaceAudience.Private
  protected static Long getRootProcedureId(final Map procedures, Procedure proc) {
    while (proc.hasParent()) {
      proc = procedures.get(proc.getParentProcId());
      if (proc == null) return null;
    }
    return proc.getProcId();
  }

  protected static Procedure newInstance(final String className) throws IOException {
    try {
      Class clazz = Class.forName(className);
      if (!Modifier.isPublic(clazz.getModifiers())) {
        throw new Exception("the " + clazz + " class is not public");
      }

      Constructor ctor = clazz.getConstructor();
      assert ctor != null : "no constructor found";
      if (!Modifier.isPublic(ctor.getModifiers())) {
        throw new Exception("the " + clazz + " constructor is not public");
      }
      return (Procedure)ctor.newInstance();
    } catch (Exception e) {
      throw new IOException("The procedure class " + className +
          " must be accessible and have an empty constructor", e);
    }
  }

  protected static void validateClass(final Procedure proc) throws IOException {
    try {
      Class clazz = proc.getClass();
      if (!Modifier.isPublic(clazz.getModifiers())) {
        throw new Exception("the " + clazz + " class is not public");
      }

      Constructor ctor = clazz.getConstructor();
      assert ctor != null;
      if (!Modifier.isPublic(ctor.getModifiers())) {
        throw new Exception("the " + clazz + " constructor is not public");
      }
    } catch (Exception e) {
      throw new IOException("The procedure class " + proc.getClass().getName() +
          " must be accessible and have an empty constructor", e);
    }
  }

  /**
   * Helper to create the ProcedureInfo from Procedure.
   */
  @InterfaceAudience.Private
  public static ProcedureInfo createProcedureInfo(final Procedure proc, final NonceKey nonceKey) {
    RemoteProcedureException exception = proc.hasException() ? proc.getException() : null;
    return new ProcedureInfo(
      proc.getProcId(),
      proc.toStringClass(),
      proc.getOwner(),
      proc.getState(),
      proc.hasParent() ? proc.getParentProcId() : -1,
      nonceKey,
      exception != null ?
          RemoteProcedureException.toProto(exception.getSource(), exception.getCause()) : null,
      proc.getLastUpdate(),
      proc.getStartTime(),
      proc.getResult());
  }

  /**
   * Helper to convert the procedure to protobuf.
   * Used by ProcedureStore implementations.
   */
  @InterfaceAudience.Private
  public static ProcedureProtos.Procedure convert(final Procedure proc)
      throws IOException {
    Preconditions.checkArgument(proc != null);
    validateClass(proc);

    ProcedureProtos.Procedure.Builder builder = ProcedureProtos.Procedure.newBuilder()
      .setClassName(proc.getClass().getName())
      .setProcId(proc.getProcId())
      .setState(proc.getState())
      .setStartTime(proc.getStartTime())
      .setLastUpdate(proc.getLastUpdate());

    if (proc.hasParent()) {
      builder.setParentId(proc.getParentProcId());
    }

    if (proc.hasTimeout()) {
      builder.setTimeout(proc.getTimeout());
    }

    if (proc.hasOwner()) {
      builder.setOwner(proc.getOwner());
    }

    int[] stackIds = proc.getStackIndexes();
    if (stackIds != null) {
      for (int i = 0; i < stackIds.length; ++i) {
        builder.addStackId(stackIds[i]);
      }
    }

    if (proc.hasException()) {
      RemoteProcedureException exception = proc.getException();
      builder.setException(
        RemoteProcedureException.toProto(exception.getSource(), exception.getCause()));
    }

    byte[] result = proc.getResult();
    if (result != null) {
      builder.setResult(ByteStringer.wrap(result));
    }

    ByteString.Output stateStream = ByteString.newOutput();
    proc.serializeStateData(stateStream);
    if (stateStream.size() > 0) {
      builder.setStateData(stateStream.toByteString());
    }

    if (proc.getNonceKey() != null) {
      builder.setNonceGroup(proc.getNonceKey().getNonceGroup());
      builder.setNonce(proc.getNonceKey().getNonce());
    }

    return builder.build();
  }

  /**
   * Helper to convert the protobuf procedure.
   * Used by ProcedureStore implementations.
   *
   * TODO: OPTIMIZATION: some of the field never change during the execution
   *                     (e.g. className, procId, parentId, ...).
   *                     We can split in 'data' and 'state', and the store
   *                     may take advantage of it by storing the data only on insert().
   */
  @InterfaceAudience.Private
  public static Procedure convert(final ProcedureProtos.Procedure proto)
      throws IOException {
    // Procedure from class name
    Procedure proc = Procedure.newInstance(proto.getClassName());

    // set fields
    proc.setProcId(proto.getProcId());
    proc.setState(proto.getState());
    proc.setStartTime(proto.getStartTime());
    proc.setLastUpdate(proto.getLastUpdate());

    if (proto.hasParentId()) {
      proc.setParentProcId(proto.getParentId());
    }

    if (proto.hasOwner()) {
      proc.setOwner(proto.getOwner());
    }

    if (proto.hasTimeout()) {
      proc.setTimeout(proto.getTimeout());
    }

    if (proto.getStackIdCount() > 0) {
      proc.setStackIndexes(proto.getStackIdList());
    }

    if (proto.hasException()) {
      assert proc.getState() == ProcedureState.FINISHED ||
             proc.getState() == ProcedureState.ROLLEDBACK :
             "The procedure must be failed (waiting to rollback) or rolledback";
      proc.setFailure(RemoteProcedureException.fromProto(proto.getException()));
    }

    if (proto.hasResult()) {
      proc.setResult(proto.getResult().toByteArray());
    }

    if (proto.getNonce() != HConstants.NO_NONCE) {
      NonceKey nonceKey = new NonceKey(proto.getNonceGroup(), proto.getNonce());
      proc.setNonceKey(nonceKey);
    }

    // we want to call deserialize even when the stream is empty, mainly for testing.
    proc.deserializeStateData(proto.getStateData().newInput());

    return proc;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy