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

com.uber.cadence.internal.common.WorkflowExecutionUtils Maven / Gradle / Ivy

/*
 *  Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 *  Modifications copyright (C) 2017 Uber Technologies, Inc.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"). You may not
 *  use this file except in compliance with the License. A copy of the License is
 *  located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 *  or in the "license" file accompanying this file. This file 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 com.uber.cadence.internal.common;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.uber.cadence.ActivityType;
import com.uber.cadence.BadRequestError;
import com.uber.cadence.Decision;
import com.uber.cadence.DecisionType;
import com.uber.cadence.DescribeWorkflowExecutionRequest;
import com.uber.cadence.DescribeWorkflowExecutionResponse;
import com.uber.cadence.EntityNotExistsError;
import com.uber.cadence.EventType;
import com.uber.cadence.GetWorkflowExecutionHistoryRequest;
import com.uber.cadence.GetWorkflowExecutionHistoryResponse;
import com.uber.cadence.History;
import com.uber.cadence.HistoryEvent;
import com.uber.cadence.HistoryEventFilterType;
import com.uber.cadence.StartWorkflowExecutionRequest;
import com.uber.cadence.TaskList;
import com.uber.cadence.WorkflowExecution;
import com.uber.cadence.WorkflowExecutionCloseStatus;
import com.uber.cadence.WorkflowExecutionContinuedAsNewEventAttributes;
import com.uber.cadence.WorkflowExecutionFailedEventAttributes;
import com.uber.cadence.WorkflowExecutionInfo;
import com.uber.cadence.WorkflowExecutionTerminatedEventAttributes;
import com.uber.cadence.WorkflowExecutionTimedOutEventAttributes;
import com.uber.cadence.WorkflowType;
import com.uber.cadence.client.WorkflowTerminatedException;
import com.uber.cadence.client.WorkflowTimedOutException;
import com.uber.cadence.common.RetryOptions;
import com.uber.cadence.common.WorkflowExecutionHistory;
import com.uber.cadence.serviceclient.IWorkflowService;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.time.Duration;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.thrift.TException;
import org.apache.thrift.async.AsyncMethodCallback;

/**
 * Convenience methods to be used by unit tests and during development.
 *
 * @author fateev
 */
public class WorkflowExecutionUtils {

  /**
   * Indentation for history and decisions pretty printing. Do not change it from 2 spaces. The gson
   * pretty printer has it hardcoded and changing it breaks the indentation of exception stack
   * traces.
   */
  private static final String INDENTATION = "  ";

  private static RetryOptions retryParameters =
      new RetryOptions.Builder()
          .setBackoffCoefficient(2)
          .setInitialInterval(Duration.ofMillis(500))
          .setMaximumInterval(Duration.ofSeconds(30))
          .setMaximumAttempts(Integer.MAX_VALUE)
          .setDoNotRetry(BadRequestError.class, EntityNotExistsError.class)
          .build();

  /**
   * Returns result of a workflow instance execution or throws an exception if workflow did not
   * complete successfully.
   *
   * @param workflowType is optional.
   * @throws TimeoutException if workflow didn't complete within specified timeout
   * @throws CancellationException if workflow was cancelled
   * @throws WorkflowExecutionFailedException if workflow execution failed
   * @throws WorkflowTimedOutException if workflow execution exceeded its execution timeout and was
   *     forcefully terminated by the Cadence server.
   * @throws WorkflowTerminatedException if workflow execution was terminated through an external
   *     terminate command.
   */
  public static byte[] getWorkflowExecutionResult(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      Optional workflowType,
      long timeout,
      TimeUnit unit)
      throws TimeoutException, CancellationException, WorkflowExecutionFailedException,
          WorkflowTerminatedException, WorkflowTimedOutException, EntityNotExistsError {
    // getIntanceCloseEvent waits for workflow completion including new runs.
    HistoryEvent closeEvent =
        getInstanceCloseEvent(service, domain, workflowExecution, timeout, unit);
    return getResultFromCloseEvent(workflowExecution, workflowType, closeEvent);
  }

  public static CompletableFuture getWorkflowExecutionResultAsync(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      Optional workflowType,
      long timeout,
      TimeUnit unit) {
    return getInstanceCloseEventAsync(service, domain, workflowExecution, timeout, unit)
        .thenApply(
            (closeEvent) -> getResultFromCloseEvent(workflowExecution, workflowType, closeEvent));
  }

  private static byte[] getResultFromCloseEvent(
      WorkflowExecution workflowExecution, Optional workflowType, HistoryEvent closeEvent) {
    if (closeEvent == null) {
      throw new IllegalStateException("Workflow is still running");
    }
    switch (closeEvent.getEventType()) {
      case WorkflowExecutionCompleted:
        return closeEvent.getWorkflowExecutionCompletedEventAttributes().getResult();
      case WorkflowExecutionCanceled:
        byte[] details = closeEvent.getWorkflowExecutionCanceledEventAttributes().getDetails();
        String message = details != null ? new String(details, UTF_8) : null;
        throw new CancellationException(message);
      case WorkflowExecutionFailed:
        WorkflowExecutionFailedEventAttributes failed =
            closeEvent.getWorkflowExecutionFailedEventAttributes();
        throw new WorkflowExecutionFailedException(
            failed.getReason(), failed.getDetails(), failed.getDecisionTaskCompletedEventId());
      case WorkflowExecutionTerminated:
        WorkflowExecutionTerminatedEventAttributes terminated =
            closeEvent.getWorkflowExecutionTerminatedEventAttributes();
        throw new WorkflowTerminatedException(
            workflowExecution,
            workflowType,
            terminated.getReason(),
            terminated.getIdentity(),
            terminated.getDetails());
      case WorkflowExecutionTimedOut:
        WorkflowExecutionTimedOutEventAttributes timedOut =
            closeEvent.getWorkflowExecutionTimedOutEventAttributes();
        throw new WorkflowTimedOutException(
            workflowExecution, workflowType, timedOut.getTimeoutType());
      default:
        throw new RuntimeException(
            "Workflow end state is not completed: " + prettyPrintHistoryEvent(closeEvent));
    }
  }

  /** Returns an instance closing event, potentially waiting for workflow to complete. */
  public static HistoryEvent getInstanceCloseEvent(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      long timeout,
      TimeUnit unit)
      throws TimeoutException, EntityNotExistsError {
    byte[] pageToken = null;
    GetWorkflowExecutionHistoryResponse response;
    // TODO: Interrupt service long poll call on timeout and on interrupt
    long start = System.currentTimeMillis();
    HistoryEvent event;
    do {
      GetWorkflowExecutionHistoryRequest r = new GetWorkflowExecutionHistoryRequest();
      r.setDomain(domain);
      r.setExecution(workflowExecution);
      r.setHistoryEventFilterType(HistoryEventFilterType.CLOSE_EVENT);
      r.setNextPageToken(pageToken);
      r.setWaitForNewEvent(true);
      try {
        response =
            Retryer.retryWithResult(retryParameters, () -> service.GetWorkflowExecutionHistory(r));
      } catch (EntityNotExistsError e) {
        throw e;
      } catch (TException e) {
        throw CheckedExceptionWrapper.wrap(e);
      }
      if (timeout != 0 && System.currentTimeMillis() - start > unit.toMillis(timeout)) {
        throw new TimeoutException(
            "WorkflowId="
                + workflowExecution.getWorkflowId()
                + ", runId="
                + workflowExecution.getRunId()
                + ", timeout="
                + timeout
                + ", unit="
                + unit);
      }
      pageToken = response.getNextPageToken();
      History history = response.getHistory();
      if (history != null && history.getEvents().size() > 0) {
        event = history.getEvents().get(0);
        if (!isWorkflowExecutionCompletedEvent(event)) {
          throw new RuntimeException("Last history event is not completion event: " + event);
        }
        // Workflow called continueAsNew. Start polling the new generation with new runId.
        if (event.getEventType() == EventType.WorkflowExecutionContinuedAsNew) {
          pageToken = null;
          workflowExecution =
              new WorkflowExecution()
                  .setWorkflowId(workflowExecution.getWorkflowId())
                  .setRunId(
                      event
                          .getWorkflowExecutionContinuedAsNewEventAttributes()
                          .getNewExecutionRunId());
          continue;
        }
        break;
      }
    } while (true);
    return event;
  }

  /** Returns an instance closing event, potentially waiting for workflow to complete. */
  private static CompletableFuture getInstanceCloseEventAsync(
      IWorkflowService service,
      String domain,
      final WorkflowExecution workflowExecution,
      long timeout,
      TimeUnit unit) {
    return getInstanceCloseEventAsync(service, domain, workflowExecution, null, timeout, unit);
  }

  private static CompletableFuture getInstanceCloseEventAsync(
      IWorkflowService service,
      String domain,
      final WorkflowExecution workflowExecution,
      byte[] pageToken,
      long timeout,
      TimeUnit unit) {
    // TODO: Interrupt service long poll call on timeout and on interrupt
    long start = System.currentTimeMillis();
    GetWorkflowExecutionHistoryRequest request = new GetWorkflowExecutionHistoryRequest();
    request.setDomain(domain);
    request.setExecution(workflowExecution);
    request.setHistoryEventFilterType(HistoryEventFilterType.CLOSE_EVENT);
    request.setNextPageToken(pageToken);
    CompletableFuture response =
        getWorkflowExecutionHistoryAsync(service, request);
    return response.thenComposeAsync(
        (r) -> {
          if (timeout != 0 && System.currentTimeMillis() - start > unit.toMillis(timeout)) {
            throw CheckedExceptionWrapper.wrap(
                new TimeoutException(
                    "WorkflowId="
                        + workflowExecution.getWorkflowId()
                        + ", runId="
                        + workflowExecution.getRunId()
                        + ", timeout="
                        + timeout
                        + ", unit="
                        + unit));
          }
          History history = r.getHistory();
          if (history == null || history.getEvents().size() == 0) {
            // Empty poll returned
            return getInstanceCloseEventAsync(
                service, domain, workflowExecution, pageToken, timeout, unit);
          }
          HistoryEvent event = history.getEvents().get(0);
          if (!isWorkflowExecutionCompletedEvent(event)) {
            throw new RuntimeException("Last history event is not completion event: " + event);
          }
          // Workflow called continueAsNew. Start polling the new generation with new runId.
          if (event.getEventType() == EventType.WorkflowExecutionContinuedAsNew) {
            WorkflowExecution nextWorkflowExecution =
                new WorkflowExecution()
                    .setWorkflowId(workflowExecution.getWorkflowId())
                    .setRunId(
                        event
                            .getWorkflowExecutionContinuedAsNewEventAttributes()
                            .getNewExecutionRunId());
            return getInstanceCloseEventAsync(
                service, domain, nextWorkflowExecution, r.getNextPageToken(), timeout, unit);
          }
          return CompletableFuture.completedFuture(event);
        });
  }

  private static CompletableFuture
      getWorkflowExecutionHistoryAsync(
          IWorkflowService service, GetWorkflowExecutionHistoryRequest r) {
    return Retryer.retryWithResultAsync(
        retryParameters,
        () -> {
          CompletableFuture result = new CompletableFuture<>();
          try {
            service.GetWorkflowExecutionHistory(
                r,
                new AsyncMethodCallback() {
                  @Override
                  public void onComplete(GetWorkflowExecutionHistoryResponse response) {
                    result.complete(response);
                  }

                  @Override
                  public void onError(Exception exception) {
                    result.completeExceptionally(exception);
                  }
                });
          } catch (TException e) {
            result.completeExceptionally(e);
          }
          return result;
        });
  }

  public static boolean isWorkflowExecutionCompletedEvent(HistoryEvent event) {
    return ((event != null)
        && (event.getEventType() == EventType.WorkflowExecutionCompleted
            || event.getEventType() == EventType.WorkflowExecutionCanceled
            || event.getEventType() == EventType.WorkflowExecutionFailed
            || event.getEventType() == EventType.WorkflowExecutionTimedOut
            || event.getEventType() == EventType.WorkflowExecutionContinuedAsNew
            || event.getEventType() == EventType.WorkflowExecutionTerminated));
  }

  public static boolean isWorkflowExecutionCompleteDecision(Decision decision) {
    return ((decision != null)
        && (decision.getDecisionType() == DecisionType.CompleteWorkflowExecution
            || decision.getDecisionType() == DecisionType.CancelWorkflowExecution
            || decision.getDecisionType() == DecisionType.FailWorkflowExecution
            || decision.getDecisionType() == DecisionType.ContinueAsNewWorkflowExecution));
  }

  public static boolean isActivityTaskClosedEvent(HistoryEvent event) {
    return ((event != null)
        && (event.getEventType() == EventType.ActivityTaskCompleted
            || event.getEventType() == EventType.ActivityTaskCanceled
            || event.getEventType() == EventType.ActivityTaskFailed
            || event.getEventType() == EventType.ActivityTaskTimedOut));
  }

  public static boolean isExternalWorkflowClosedEvent(HistoryEvent event) {
    return ((event != null)
        && (event.getEventType() == EventType.ChildWorkflowExecutionCompleted
            || event.getEventType() == EventType.ChildWorkflowExecutionCanceled
            || event.getEventType() == EventType.ChildWorkflowExecutionFailed
            || event.getEventType() == EventType.ChildWorkflowExecutionTerminated
            || event.getEventType() == EventType.ChildWorkflowExecutionTimedOut));
  }

  public static WorkflowExecution getWorkflowIdFromExternalWorkflowCompletedEvent(
      HistoryEvent event) {
    if (event != null) {
      if (event.getEventType() == EventType.ChildWorkflowExecutionCompleted) {
        return event.getChildWorkflowExecutionCompletedEventAttributes().getWorkflowExecution();
      } else if (event.getEventType() == EventType.ChildWorkflowExecutionCanceled) {
        return event.getChildWorkflowExecutionCanceledEventAttributes().getWorkflowExecution();
      } else if (event.getEventType() == EventType.ChildWorkflowExecutionFailed) {
        return event.getChildWorkflowExecutionFailedEventAttributes().getWorkflowExecution();
      } else if (event.getEventType() == EventType.ChildWorkflowExecutionTerminated) {
        return event.getChildWorkflowExecutionTerminatedEventAttributes().getWorkflowExecution();
      } else if (event.getEventType() == EventType.ChildWorkflowExecutionTimedOut) {
        return event.getChildWorkflowExecutionTimedOutEventAttributes().getWorkflowExecution();
      }
    }

    return null;
  }

  public static String getId(HistoryEvent historyEvent) {
    String id = null;
    if (historyEvent != null) {
      if (historyEvent.getEventType() == EventType.StartChildWorkflowExecutionFailed) {
        id = historyEvent.getStartChildWorkflowExecutionFailedEventAttributes().getWorkflowId();
      }
    }

    return id;
  }

  public static String getFailureCause(HistoryEvent historyEvent) {
    String failureCause = null;
    if (historyEvent != null) {
      if (historyEvent.getEventType() == EventType.StartChildWorkflowExecutionFailed) {
        failureCause =
            historyEvent
                .getStartChildWorkflowExecutionFailedEventAttributes()
                .getCause()
                .toString();
        //            } else if (historyEvent.getEventType() ==
        // EventType.SignalExternalWorkflowExecutionFailed) {
        //                failureCause =
        // historyEvent.getSignalExternalWorkflowExecutionFailedEventAttributes().getCause();
      } else {
        failureCause = "Cannot extract failure cause from " + historyEvent.getEventType();
      }
    }

    return failureCause;
  }

  /**
   * Blocks until workflow instance completes. Never use in production setting as
   * polling for worklow instance status is an expensive operation.
   *
   * @param workflowExecution result of {@link
   *     IWorkflowService#StartWorkflowExecution(StartWorkflowExecutionRequest)}
   * @return instance close status
   */
  public static WorkflowExecutionCloseStatus waitForWorkflowInstanceCompletion(
      IWorkflowService service, String domain, WorkflowExecution workflowExecution)
      throws EntityNotExistsError {
    try {
      return waitForWorkflowInstanceCompletion(
          service, domain, workflowExecution, 0, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
      throw new Error("should never happen", e);
    }
  }

  /**
   * Waits up to specified timeout for workflow instance completion. Never use in
   * production setting as polling for worklow instance status is an expensive operation.
   *
   * @param workflowExecution result of {@link
   *     IWorkflowService#StartWorkflowExecution(StartWorkflowExecutionRequest)}
   * @param timeout maximum time to wait for completion. 0 means wait forever.
   * @return instance close status
   */
  public static WorkflowExecutionCloseStatus waitForWorkflowInstanceCompletion(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      long timeout,
      TimeUnit unit)
      throws TimeoutException, EntityNotExistsError {
    HistoryEvent closeEvent =
        getInstanceCloseEvent(service, domain, workflowExecution, timeout, unit);
    return getCloseStatus(closeEvent);
  }

  public static WorkflowExecutionCloseStatus getCloseStatus(HistoryEvent event) {
    switch (event.getEventType()) {
      case WorkflowExecutionCanceled:
        return WorkflowExecutionCloseStatus.CANCELED;
      case WorkflowExecutionFailed:
        return WorkflowExecutionCloseStatus.FAILED;
      case WorkflowExecutionTimedOut:
        return WorkflowExecutionCloseStatus.TIMED_OUT;
      case WorkflowExecutionContinuedAsNew:
        return WorkflowExecutionCloseStatus.CONTINUED_AS_NEW;
      case WorkflowExecutionCompleted:
        return WorkflowExecutionCloseStatus.COMPLETED;
      case WorkflowExecutionTerminated:
        return WorkflowExecutionCloseStatus.TERMINATED;
      default:
        throw new IllegalArgumentException("Not close event: " + event);
    }
  }

  /**
   * Like {@link #waitForWorkflowInstanceCompletion(IWorkflowService, String, WorkflowExecution,
   * long, TimeUnit)} , except will wait for continued generations of the original workflow
   * execution too.
   *
   * @see #waitForWorkflowInstanceCompletion(IWorkflowService, String, WorkflowExecution, long,
   *     TimeUnit)
   */
  public static WorkflowExecutionCloseStatus waitForWorkflowInstanceCompletionAcrossGenerations(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      long timeout,
      TimeUnit unit)
      throws TimeoutException, EntityNotExistsError {

    WorkflowExecution lastExecutionToRun = workflowExecution;
    long millisecondsAtFirstWait = System.currentTimeMillis();
    WorkflowExecutionCloseStatus lastExecutionToRunCloseStatus =
        waitForWorkflowInstanceCompletion(service, domain, lastExecutionToRun, timeout, unit);

    // keep waiting if the instance continued as new
    while (lastExecutionToRunCloseStatus == WorkflowExecutionCloseStatus.CONTINUED_AS_NEW) {
      // get the new execution's information
      HistoryEvent closeEvent =
          getInstanceCloseEvent(service, domain, lastExecutionToRun, timeout, unit);
      WorkflowExecutionContinuedAsNewEventAttributes continuedAsNewAttributes =
          closeEvent.getWorkflowExecutionContinuedAsNewEventAttributes();

      WorkflowExecution newGenerationExecution = new WorkflowExecution();
      newGenerationExecution.setRunId(continuedAsNewAttributes.getNewExecutionRunId());
      newGenerationExecution.setWorkflowId(lastExecutionToRun.getWorkflowId());

      // and wait for it
      long currentTime = System.currentTimeMillis();
      long millisecondsSinceFirstWait = currentTime - millisecondsAtFirstWait;
      long timeoutInSecondsForNextWait =
          unit.toMillis(timeout) - (millisecondsSinceFirstWait / 1000L);

      lastExecutionToRunCloseStatus =
          waitForWorkflowInstanceCompletion(
              service,
              domain,
              newGenerationExecution,
              timeoutInSecondsForNextWait,
              TimeUnit.MILLISECONDS);
      lastExecutionToRun = newGenerationExecution;
    }

    return lastExecutionToRunCloseStatus;
  }

  /**
   * Like {@link #waitForWorkflowInstanceCompletion(IWorkflowService, String, WorkflowExecution,
   * long, TimeUnit)} , but with no timeout.*
   */
  public static WorkflowExecutionCloseStatus waitForWorkflowInstanceCompletionAcrossGenerations(
      IWorkflowService service, String domain, WorkflowExecution workflowExecution)
      throws InterruptedException, EntityNotExistsError {
    try {
      return waitForWorkflowInstanceCompletionAcrossGenerations(
          service, domain, workflowExecution, 0L, TimeUnit.MILLISECONDS);
    } catch (TimeoutException e) {
      throw new Error("should never happen", e);
    }
  }

  public static WorkflowExecutionInfo describeWorkflowInstance(
      IWorkflowService service, String domain, WorkflowExecution workflowExecution) {
    DescribeWorkflowExecutionRequest describeRequest = new DescribeWorkflowExecutionRequest();
    describeRequest.setDomain(domain);
    describeRequest.setExecution(workflowExecution);
    DescribeWorkflowExecutionResponse executionDetail = null;
    try {
      executionDetail = service.DescribeWorkflowExecution(describeRequest);
    } catch (TException e) {
      throw new RuntimeException(e);
    }
    WorkflowExecutionInfo instanceMetadata = executionDetail.getWorkflowExecutionInfo();
    return instanceMetadata;
  }

  public static GetWorkflowExecutionHistoryResponse getHistoryPage(
      byte[] nextPageToken,
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution) {

    GetWorkflowExecutionHistoryRequest getHistoryRequest = new GetWorkflowExecutionHistoryRequest();
    getHistoryRequest.setDomain(domain);
    getHistoryRequest.setExecution(workflowExecution);
    getHistoryRequest.setNextPageToken(nextPageToken);

    GetWorkflowExecutionHistoryResponse history;
    try {
      history = service.GetWorkflowExecutionHistory(getHistoryRequest);
    } catch (TException e) {
      throw new Error(e);
    }
    if (history == null) {
      throw new IllegalArgumentException("unknown workflow execution: " + workflowExecution);
    }
    return history;
  }

  /** Returns workflow instance history in a human readable format. */
  public static String prettyPrintHistory(
      IWorkflowService service, String domain, WorkflowExecution workflowExecution) {
    return prettyPrintHistory(service, domain, workflowExecution, true);
  }
  /**
   * Returns workflow instance history in a human readable format.
   *
   * @param showWorkflowTasks when set to false workflow task events (decider events) are not
   *     included
   */
  public static String prettyPrintHistory(
      IWorkflowService service,
      String domain,
      WorkflowExecution workflowExecution,
      boolean showWorkflowTasks) {
    Iterator events = getHistory(service, domain, workflowExecution);
    return prettyPrintHistory(events, showWorkflowTasks);
  }

  public static Iterator getHistory(
      IWorkflowService service, String domain, WorkflowExecution workflowExecution) {
    return new Iterator() {
      byte[] nextPageToken;
      Iterator current;

      {
        getNextPage();
      }

      @Override
      public boolean hasNext() {
        return current.hasNext() || nextPageToken != null;
      }

      @Override
      public HistoryEvent next() {
        if (current.hasNext()) {
          return current.next();
        }
        getNextPage();
        return current.next();
      }

      private void getNextPage() {
        GetWorkflowExecutionHistoryResponse history =
            getHistoryPage(nextPageToken, service, domain, workflowExecution);
        current = history.getHistory().getEvents().iterator();
        nextPageToken = history.getNextPageToken();
      }
    };
  }

  /**
   * Returns workflow instance history in a human readable format.
   *
   * @param showWorkflowTasks when set to false workflow task events (decider events) are not
   *     included
   * @history Workflow instance history
   */
  public static String prettyPrintHistory(History history, boolean showWorkflowTasks) {
    return prettyPrintHistory(history.getEvents().iterator(), showWorkflowTasks);
  }

  public static String prettyPrintHistory(
      Iterator events, boolean showWorkflowTasks) {
    StringBuilder result = new StringBuilder();
    result.append("{");
    boolean first = true;
    long firstTimestamp = 0;
    while (events.hasNext()) {
      HistoryEvent event = events.next();
      if (!showWorkflowTasks && event.getEventType().toString().startsWith("WorkflowTask")) {
        continue;
      }
      if (first) {
        first = false;
        firstTimestamp = event.getTimestamp();
      } else {
        result.append(",");
      }
      result.append("\n");
      result.append(INDENTATION);
      result.append(prettyPrintHistoryEvent(event, firstTimestamp));
    }
    result.append("\n}");
    return result.toString();
  }

  public static String prettyPrintDecisions(Iterable decisions) {
    StringBuilder result = new StringBuilder();
    result.append("{");
    boolean first = true;
    for (Decision decision : decisions) {
      if (first) {
        first = false;
      } else {
        result.append(",");
      }
      result.append("\n");
      result.append(INDENTATION);
      result.append(prettyPrintDecision(decision));
    }
    result.append("\n}");
    return result.toString();
  }

  /**
   * Returns single event in a human readable format
   *
   * @param event event to pretty print
   */
  public static String prettyPrintHistoryEvent(HistoryEvent event) {
    return prettyPrintHistoryEvent(event, -1);
  }

  private static String prettyPrintHistoryEvent(HistoryEvent event, long firstTimestamp) {
    String eventType = event.getEventType().toString();
    StringBuilder result = new StringBuilder();
    result.append(event.getEventId());
    result.append(": ");
    result.append(eventType);
    if (firstTimestamp > 0) {
      // timestamp is in nanos
      long timestamp = (event.getTimestamp() - firstTimestamp) / 1_000_000;
      result.append(String.format(" [%s ms]", timestamp));
    }
    result.append(" ");
    result.append(
        prettyPrintObject(
            getEventAttributes(event), "getFieldValue", true, INDENTATION, false, false));

    return result.toString();
  }

  private static Object getEventAttributes(HistoryEvent event) {
    try {
      Method m = HistoryEvent.class.getMethod("get" + event.getEventType() + "EventAttributes");
      return m.invoke(event);
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
      return event;
    }
  }

  /**
   * Returns single decision in a human readable format
   *
   * @param decision decision to pretty print
   */
  public static String prettyPrintDecision(Decision decision) {
    return prettyPrintObject(decision, "getFieldValue", true, INDENTATION, true, true);
  }

  /**
   * Not really a generic method for printing random object graphs. But it works for events and
   * decisions.
   */
  private static String prettyPrintObject(
      Object object,
      String methodToSkip,
      boolean skipNullsAndEmptyCollections,
      String indentation,
      boolean skipLevel,
      boolean printTypeName) {
    StringBuilder result = new StringBuilder();
    if (object == null) {
      return "null";
    }
    Class clz = object.getClass();
    if (Number.class.isAssignableFrom(clz)) {
      return String.valueOf(object);
    }
    if (Boolean.class.isAssignableFrom(clz)) {
      return String.valueOf(object);
    }
    if (clz.equals(String.class)) {
      return (String) object;
    }
    if (clz.equals(byte[].class)) {
      return new String((byte[]) object, UTF_8);
    }
    if (ByteBuffer.class.isAssignableFrom(clz)) {
      byte[] bytes = org.apache.thrift.TBaseHelper.byteBufferToByteArray((ByteBuffer) object);
      return new String(bytes, UTF_8);
    }
    if (clz.equals(Date.class)) {
      return String.valueOf(object);
    }
    if (clz.equals(TaskList.class)) {
      return String.valueOf(((TaskList) object).getName());
    }
    if (clz.equals(ActivityType.class)) {
      return String.valueOf(((ActivityType) object).getName());
    }
    if (clz.equals(WorkflowType.class)) {
      return String.valueOf(((WorkflowType) object).getName());
    }
    if (Map.Entry.class.isAssignableFrom(clz)) {
      result.append(
          prettyPrintObject(
              ((Map.Entry) object).getKey(),
              methodToSkip,
              skipNullsAndEmptyCollections,
              "",
              skipLevel,
              printTypeName));
      result.append("=");
      result.append(
          prettyPrintObject(
              ((Map.Entry) object).getValue(),
              methodToSkip,
              skipNullsAndEmptyCollections,
              "",
              skipLevel,
              printTypeName));
      return result.toString();
    }
    if (Map.class.isAssignableFrom(clz)) {
      result.append("{ ");

      String prefix = "";
      for (Object entry : ((Map) object).entrySet()) {
        result.append(prefix);
        prefix = ", ";
        result.append(
            prettyPrintObject(
                entry, methodToSkip, skipNullsAndEmptyCollections, "", skipLevel, printTypeName));
      }

      result.append(" }");
      return result.toString();
    }
    if (Collection.class.isAssignableFrom(clz)) {
      return String.valueOf(object);
    }
    if (!skipLevel) {
      if (printTypeName) {
        result.append(object.getClass().getSimpleName());
        result.append(" ");
      }
      result.append("{");
    }
    Method[] eventMethods = object.getClass().getDeclaredMethods();
    boolean first = true;
    for (Method method : eventMethods) {
      String name = method.getName();
      if (!name.startsWith("get")
          || name.equals("getDecisionType")
          || method.getParameterCount() != 0
          || !Modifier.isPublic(method.getModifiers())) {
        continue;
      }
      if (name.equals(methodToSkip) || name.equals("getClass")) {
        continue;
      }
      if (Modifier.isStatic(method.getModifiers())) {
        continue;
      }
      Object value;
      try {
        value = method.invoke(object, (Object[]) null);
      } catch (InvocationTargetException e) {
        throw new RuntimeException(e.getTargetException());
      } catch (RuntimeException e) {
        throw e;
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
      if (skipNullsAndEmptyCollections) {
        if (value == null) {
          continue;
        }
        if (value instanceof Map && ((Map) value).isEmpty()) {
          continue;
        }
        if (value instanceof Collection && ((Collection) value).isEmpty()) {
          continue;
        }
      }
      if (!skipLevel) {
        if (first) {
          first = false;
        } else {
          result.append(";");
        }
        result.append("\n");
        result.append(indentation);
        result.append(INDENTATION);
        result.append(name.substring(3));
        result.append(" = ");
        // Pretty print JSON serialized exceptions.
        if (name.equals("getDetails") && value instanceof byte[]) {
          String details = new String((byte[]) value, UTF_8);
          details = prettyPrintJson(details, INDENTATION + INDENTATION);
          // GSON pretty prints, but doesn't let to set an initial indentation.
          // Thus indenting the pretty printed JSON through regexp :(.
          String replacement = "\n" + indentation + INDENTATION;
          details = details.replaceAll("\\n|\\\\n", replacement);
          result.append(details);
          continue;
        }
        result.append(
            prettyPrintObject(
                value,
                methodToSkip,
                skipNullsAndEmptyCollections,
                indentation + INDENTATION,
                false,
                false));
      } else {
        result.append(
            prettyPrintObject(
                value,
                methodToSkip,
                skipNullsAndEmptyCollections,
                indentation,
                false,
                printTypeName));
      }
    }
    if (!skipLevel) {
      result.append("\n");
      result.append(indentation);
      result.append("}");
    }
    return result.toString();
  }

  public static boolean containsEvent(List history, EventType eventType) {
    for (HistoryEvent event : history) {
      if (event.getEventType() == eventType) {
        return true;
      }
    }
    return false;
  }

  /**
   * Pretty prints JSON. Not a generic utility. Used to prettify Details fields that contain
   * serialized exceptions.
   */
  private static String prettyPrintJson(String jsonValue, String stackIndentation) {
    JsonParser parser = new JsonParser();
    try {
      JsonObject json = parser.parse(jsonValue).getAsJsonObject();
      fixStackTrace(json, stackIndentation);
      Gson gson = new GsonBuilder().setPrettyPrinting().create();
      return gson.toJson(json);
    } catch (Exception e) {
      return jsonValue;
    }
  }

  private static void fixStackTrace(JsonElement json, String stackIndentation) {
    if (!json.isJsonObject()) {
      return;
    }
    for (Entry entry : json.getAsJsonObject().entrySet()) {
      if ("stackTrace".equals(entry.getKey())) {
        String value = entry.getValue().getAsString();
        String replacement = "\n" + stackIndentation;
        String fixed = value.replaceAll("\\n", replacement);
        entry.setValue(new JsonPrimitive(fixed));
        continue;
      }
      fixStackTrace(entry.getValue(), stackIndentation + INDENTATION);
    }
  }

  /** Is this an event that was created to mirror a decision? */
  public static boolean isDecisionEvent(HistoryEvent event) {
    EventType eventType = event.getEventType();
    boolean result =
        ((event != null)
            && (eventType == EventType.ActivityTaskScheduled
                || eventType == EventType.StartChildWorkflowExecutionInitiated
                || eventType == EventType.TimerStarted
                || eventType == EventType.WorkflowExecutionCompleted
                || eventType == EventType.WorkflowExecutionFailed
                || eventType == EventType.WorkflowExecutionCanceled
                || eventType == EventType.WorkflowExecutionContinuedAsNew
                || eventType == EventType.ActivityTaskCancelRequested
                || eventType == EventType.RequestCancelActivityTaskFailed
                || eventType == EventType.TimerCanceled
                || eventType == EventType.CancelTimerFailed
                || eventType == EventType.RequestCancelExternalWorkflowExecutionInitiated
                || eventType == EventType.MarkerRecorded
                || eventType == EventType.SignalExternalWorkflowExecutionInitiated));
    return result;
  }

  public static EventType getEventTypeForDecision(DecisionType decisionType) {
    switch (decisionType) {
      case ScheduleActivityTask:
        return EventType.ActivityTaskScheduled;
      case RequestCancelActivityTask:
        return EventType.ActivityTaskCancelRequested;
      case StartTimer:
        return EventType.TimerStarted;
      case CompleteWorkflowExecution:
        return EventType.WorkflowExecutionCompleted;
      case FailWorkflowExecution:
        return EventType.WorkflowExecutionFailed;
      case CancelTimer:
        return EventType.TimerCanceled;
      case CancelWorkflowExecution:
        return EventType.WorkflowExecutionCanceled;
      case RequestCancelExternalWorkflowExecution:
        return EventType.ExternalWorkflowExecutionCancelRequested;
      case RecordMarker:
        return EventType.MarkerRecorded;
      case ContinueAsNewWorkflowExecution:
        return EventType.WorkflowExecutionContinuedAsNew;
      case StartChildWorkflowExecution:
        return EventType.StartChildWorkflowExecutionInitiated;
      case SignalExternalWorkflowExecution:
        return EventType.SignalExternalWorkflowExecutionInitiated;
    }
    throw new IllegalArgumentException("Unknown decisionType");
  }

  public static WorkflowExecutionHistory readHistoryFromResource(String resourceFileName)
      throws IOException {
    ClassLoader classLoader = WorkflowExecutionUtils.class.getClassLoader();
    String historyUrl = classLoader.getResource(resourceFileName).getFile();
    File historyFile = new File(historyUrl);
    return readHistory(historyFile);
  }

  public static WorkflowExecutionHistory readHistory(File historyFile) throws IOException {
    try (Reader reader = Files.newBufferedReader(historyFile.toPath(), UTF_8)) {
      String jsonHistory = CharStreams.toString(reader);
      return WorkflowExecutionHistory.fromJson(jsonHistory);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy