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

com.spotify.styx.state.RunState Maven / Gradle / Ivy

There is a newer version: 2.0.24
Show newest version
/*
 * -\-\-
 * Spotify Styx Common
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * 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 com.spotify.styx.state;

import com.google.auto.value.AutoValue;

import com.spotify.styx.model.Event;
import com.spotify.styx.model.EventVisitor;
import com.spotify.styx.model.ExecutionDescription;
import com.spotify.styx.model.WorkflowInstance;
import com.spotify.styx.util.Time;

import java.time.Instant;
import java.util.Optional;

import static com.spotify.styx.state.OutputHandler.fanOutput;
import static com.spotify.styx.state.RunState.State.AWAITING_RETRY;
import static com.spotify.styx.state.RunState.State.DONE;
import static com.spotify.styx.state.RunState.State.ERROR;
import static com.spotify.styx.state.RunState.State.FAILED;
import static com.spotify.styx.state.RunState.State.PREPARE;
import static com.spotify.styx.state.RunState.State.RUNNING;
import static com.spotify.styx.state.RunState.State.SUBMITTED;
import static com.spotify.styx.state.RunState.State.SUBMITTING;
import static com.spotify.styx.state.RunState.State.TERMINATED;
import static java.lang.System.currentTimeMillis;
import static java.util.Optional.empty;
import static java.util.Optional.of;

/**
 * State machine for run states.
 *
 * This implements the following Finite State Transducer (Mealy machine) where the inputs are
 * defined by the {@link Event} enum and outputs defined by the methods of {@link OutputHandler}.
 */
@AutoValue
public abstract class RunState {

  public static final int MISSING_DEPS_EXIT_CODE = 20;

  public static final int INITIAL_TRIES = 0;
  public static final int INITIAL_EXIT = -1;
  public static final double INITIAL_RETRY_COST = 0.0;
  public static final double FAILURE_COST = 1.0;
  public static final double MISSING_DEPS_COST = 0.1;

  private final EventVisitor visitor = new TransitionVisitor();

  public enum State {
    NEW(false),
    PREPARE(false),
    SUBMITTING(false),
    SUBMITTED(false),
    RUNNING(false),
    TERMINATED(false),
    FAILED(false),
    AWAITING_RETRY(false),
    ERROR(true),
    DONE(true);

    private final boolean terminal;

    State(boolean terminal) {
      this.terminal = terminal;
    }

    public boolean isTerminal() {
      return terminal;
    }
  }

  public abstract WorkflowInstance workflowInstance();
  public abstract State state();
  public abstract long timestamp();
  public abstract int tries();
  public abstract double retryCost();
  public abstract long retryDelayMillis();
  public abstract int lastExit();
  public abstract Optional executionId();
  public abstract Optional executionDescription();

  abstract Time time();
  abstract OutputHandler outputHandler();

  public static RunState fresh(
      WorkflowInstance workflowInstance,
      Time time,
      OutputHandler outputHandler) {
    return create(workflowInstance, State.NEW, time, outputHandler);
  }

  public static RunState fresh(
      WorkflowInstance workflowInstance,
      Time time,
      OutputHandler... outputHandlers) {
    return fresh(workflowInstance, time, fanOutput(outputHandlers));
  }

  public static RunState fresh(WorkflowInstance workflowInstance, OutputHandler... outputHandlers) {
    return fresh(workflowInstance, Instant::now, fanOutput(outputHandlers));
  }

  public RunState transition(Event event) {
    return event.accept(visitor);
  }

  public RunState withHandlers(OutputHandler[] outputHandlers) {
    return new AutoValue_RunState(
        workflowInstance(), state(), timestamp(), tries(), retryCost(), retryDelayMillis(),
        lastExit(), executionId(), executionDescription(), time(), fanOutput(outputHandlers));
  }

  public RunState withTime(Time time) {
    return new AutoValue_RunState(
        workflowInstance(), state(), timestamp(), tries(), retryCost(), retryDelayMillis(),
        lastExit(), executionId(), executionDescription(), time, outputHandler());
  }

  private RunState state(State state) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries(), retryCost(),
        retryDelayMillis(), lastExit(), executionId(), executionDescription(), time(), outputHandler());
  }

  private RunState state(State state, ExecutionDescription executionDescription) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries(), retryCost(),
        retryDelayMillis(), lastExit(), executionId(), of(executionDescription), time(), outputHandler());
  }

  private RunState state(State state, String executionId) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries(), retryCost(),
        retryDelayMillis(), lastExit(), of(executionId), executionDescription(), time(), outputHandler());
  }

  private RunState state(State state, String executionId, ExecutionDescription executionDescription) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries(), retryCost(),
        retryDelayMillis(), lastExit(), of(executionId), of(executionDescription), time(), outputHandler());
  }

  private RunState state(State state, long retryDelayMillis) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries(), retryCost(),
        retryDelayMillis, lastExit(), executionId(), executionDescription(), time(), outputHandler());
  }

  private RunState state(State state, int tries) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries, retryCost(),
        retryDelayMillis(), lastExit(), executionId(), executionDescription(), time(), outputHandler());
  }

  private RunState state(State state, int tries, double retryCost, int lastExit) {
    return new AutoValue_RunState(
        workflowInstance(), state, time().get().toEpochMilli(), tries, retryCost,
        retryDelayMillis(), lastExit, executionId(), executionDescription(), time(), outputHandler());
  }

  private class TransitionVisitor implements EventVisitor {

    @Override
    public RunState timeTrigger(WorkflowInstance workflowInstance) {
      switch (state()) {
        case NEW:
          return state(SUBMITTED); // for backwards compatibility

        default:
          throw illegalTransition("timeTrigger");
      }
    }

    @Override
    public RunState triggerExecution(WorkflowInstance workflowInstance, String triggerId) {
      switch (state()) {
        case NEW:
          return state(PREPARE);

        default:
          throw illegalTransition("triggerExecution");
      }
    }

    @Override
    public RunState created(WorkflowInstance workflowInstance, String executionId, String dockerImage) {
      switch (state()) {
        case PREPARE:
          return state(SUBMITTED, executionId, ExecutionDescription.forImage(dockerImage));

        default:
          throw illegalTransition("created");
      }
    }

    @Override
    public RunState submit(WorkflowInstance workflowInstance, ExecutionDescription executionDescription) {
      switch (state()) {
        case PREPARE:
          return state(SUBMITTING, executionDescription);

        default:
          throw illegalTransition("submit");
      }
    }

    @Override
    public RunState submitted(WorkflowInstance workflowInstance, String executionId) {
      switch (state()) {
        case SUBMITTING:
          return state(SUBMITTED, executionId);

        default:
          throw illegalTransition("submitted");
      }
    }

    @Override
    public RunState started(WorkflowInstance workflowInstance) {
      switch (state()) {
        case SUBMITTED:
        case PREPARE:
          return state(RUNNING);

        default:
          throw illegalTransition("started");
      }
    }

    @Override
    public RunState terminate(WorkflowInstance workflowInstance, int exitCode) {
      switch (state()) {
        case RUNNING:
          return (exitCode == MISSING_DEPS_EXIT_CODE)
                 ? state(TERMINATED, tries() + 1, retryCost() + MISSING_DEPS_COST, exitCode)
                 : state(TERMINATED, tries() + 1, retryCost() + FAILURE_COST, exitCode);

        default:
          throw illegalTransition("terminate");
      }
    }

    @Override
    public RunState runError(WorkflowInstance workflowInstance, String message) {
      switch (state()) {
        case SUBMITTED:
        case RUNNING:
        case PREPARE:
          return state(FAILED, tries() + 1, retryCost() + FAILURE_COST, INITIAL_EXIT);

        default:
          throw illegalTransition("runError");
      }
    }

    @Override
    public RunState success(WorkflowInstance workflowInstance) {
      switch (state()) {
        case TERMINATED:
          return state(DONE);

        default:
          throw illegalTransition("success");
      }
    }

    @Override
    public RunState retryAfter(WorkflowInstance workflowInstance, long delayMillis) {
      switch (state()) {
        case TERMINATED:
        case FAILED:
          return state(AWAITING_RETRY, delayMillis);

        default:
          throw illegalTransition("retryAfter");
      }
    }

    @Override
    public RunState retry(WorkflowInstance workflowInstance) {
      switch (state()) {
        case TERMINATED: // for backwards compatibility
        case FAILED:     // for backwards compatibility
        case AWAITING_RETRY:
          return state(PREPARE);

        default:
          throw illegalTransition("retry");
      }
    }

    @Override
    public RunState stop(WorkflowInstance workflowInstance) {
      switch (state()) {
        case TERMINATED:
        case FAILED:
          return state(ERROR);

        default:
          throw illegalTransition("stop");
      }
    }

    @Override
    public RunState timeout(WorkflowInstance workflowInstance) {
      return state(FAILED, tries() + 1);
    }

    @Override
    public RunState halt(WorkflowInstance workflowInstance) {
      return state(ERROR);
    }
  }

  private IllegalStateException illegalTransition(String event) {
    final String key = workflowInstance().toKey();
    return new IllegalStateException(key + " received " + event + " while in " + state());
  }

  // todo: clean constructors

  public static RunState create(WorkflowInstance workflowInstance, State state) {
    return create(workflowInstance, state, INITIAL_TRIES, INITIAL_EXIT, Instant::now, OutputHandler.NOOP);
  }

  public static RunState create(WorkflowInstance workflowInstance, String executionId, State state) {
    return create(workflowInstance, state, INITIAL_TRIES, INITIAL_EXIT, executionId, Instant::now, OutputHandler.NOOP);
  }

  public static RunState newSubmitting(
      WorkflowInstance workflowInstance,
      ExecutionDescription executionDescription) {
    return new AutoValue_RunState(
        workflowInstance, State.SUBMITTING, currentTimeMillis(), INITIAL_TRIES, INITIAL_RETRY_COST,
        0L, INITIAL_EXIT, empty(), of(executionDescription), Instant::now, OutputHandler.NOOP);
  }

  public static RunState newSubmitted(
      WorkflowInstance workflowInstance,
      String execId,
      ExecutionDescription executionDescription) {
    return new AutoValue_RunState(
        workflowInstance, State.SUBMITTED, currentTimeMillis(), INITIAL_TRIES, INITIAL_RETRY_COST,
        0L, INITIAL_EXIT, of(execId), of(executionDescription), Instant::now, OutputHandler.NOOP);
  }

  public static RunState newRunning(
      WorkflowInstance workflowInstance,
      String execId,
      ExecutionDescription executionDescription) {
    return new AutoValue_RunState(
        workflowInstance, State.RUNNING, currentTimeMillis(), INITIAL_TRIES, INITIAL_RETRY_COST,
        0L, INITIAL_EXIT, of(execId), of(executionDescription), Instant::now, OutputHandler.NOOP);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      Time time,
      OutputHandler outputHandler) {
    return create(workflowInstance, state, INITIAL_TRIES, INITIAL_EXIT, time, outputHandler);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      OutputHandler outputHandler) {
    return new AutoValue_RunState(
        workflowInstance, state, currentTimeMillis(), INITIAL_TRIES, INITIAL_RETRY_COST, 0L,
        INITIAL_EXIT, empty(), empty(), Instant::now, outputHandler);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      int tries,
      int lastExit) {
    return new AutoValue_RunState(
        workflowInstance, state, currentTimeMillis(), tries, INITIAL_RETRY_COST, 0L, lastExit,
        empty(), empty(), Instant::now, OutputHandler.NOOP);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      int tries,
      double retryCost,
      int lastExit,
      OutputHandler outputHandler) {
    return new AutoValue_RunState(
        workflowInstance, state, currentTimeMillis(), tries, retryCost, 0L, lastExit,
        empty(), empty(), Instant::now, outputHandler);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      int tries,
      int lastExit,
      Time time,
      OutputHandler outputHandler) {
    return new AutoValue_RunState(
        workflowInstance, state, time.get().toEpochMilli(), tries, INITIAL_RETRY_COST, 0L, lastExit,
        empty(), empty(), time, outputHandler);
  }

  public static RunState create(
      WorkflowInstance workflowInstance,
      State state,
      int tries,
      int lastExit,
      String executionId,
      Time time,
      OutputHandler outputHandler) {
    return new AutoValue_RunState(
        workflowInstance, state, time.get().toEpochMilli(), tries, INITIAL_RETRY_COST, 0L, lastExit,
        of(executionId), empty(), time, outputHandler);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy