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

io.statusmachina.core.stdimpl.MachineInstanceImpl Maven / Gradle / Ivy

Go to download

Core functionality for Status Machina, a small, simple and pragmatic state machine for resilient microservices orchestration.

The newest version!
/*
 *  Copyright 2019 <---> Present Status Machina Contributors (https://github.com/entzik/status-machina/graphs/contributors)
 *
 *  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
 *
 *  This software 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.statusmachina.core.stdimpl;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.statusmachina.core.api.*;
import io.statusmachina.core.spi.MachinePersistenceCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.util.*;

public class MachineInstanceImpl implements Machine {
    private final Logger LOGGER = LoggerFactory.getLogger(MachineInstanceImpl.class);

    private final String id;

    private final MachineDefinition def;
    private final ImmutableMap context;
    private final ImmutableList> history;
    private final ErrorType errorType;
    private final Optional error;

    private final S currentState;
    private final MachinePersistenceCallback persistenceCallback;
    private final long eventCounter;
    private final Optional crtEvent;

    public MachineInstanceImpl(
            MachineDefinition def,
            MachinePersistenceCallback persistenceCallback,
            Map context
    ) throws Exception {
        this(def, UUID.randomUUID().toString(), persistenceCallback, context);
    }

    public MachineInstanceImpl(
            MachineDefinition def,
            String id,
            MachinePersistenceCallback persistenceCallback,
            Map context
    ) throws Exception {
        this(def, id, context, persistenceCallback, 0, Optional.empty());
    }

    public MachineInstanceImpl(
            MachineDefinition def,
            String id,
            Map context, MachinePersistenceCallback persistenceCallback,
            long eventCounter,
            Optional crtEvent
    ) throws Exception {
        this.def = def;

        this.id = id;
        this.eventCounter = eventCounter;
        this.crtEvent = crtEvent;
        this.history = ImmutableList.>builder().build();
        this.currentState = def.getInitialState();
        this.context = ImmutableMap.builder().putAll(context).build();
        this.error = Optional.empty();
        this.errorType = ErrorType.NONE;
        this.persistenceCallback = persistenceCallback;

        if (LOGGER.isDebugEnabled())
            LOGGER.debug("creating a new state machine instance of type {}, with ID {}, in initial state, preparing to persist", def.getName(), id);
        persistenceCallback.runInTransaction(() -> persistenceCallback.saveNew(this));
        if (LOGGER.isDebugEnabled())
            LOGGER.debug("state machine instance of type {}, with ID {} persisted", def.getName(), id);
    }


    public MachineInstanceImpl(
            String id,
            MachineDefinition def,
            S currentState,
            Map context,
            List> history,
            ErrorType errorType,
            Optional error,
            MachinePersistenceCallback persistenceCallback
    ) {
        this(id, def, currentState, context, history, errorType, error, persistenceCallback, 0, Optional.empty());
    }

    public MachineInstanceImpl(
            String id,
            MachineDefinition def,
            S currentState,
            Map context,
            List> history,
            ErrorType errorType,
            Optional error,
            MachinePersistenceCallback persistenceCallback,
            long eventCounter,
            Optional crtEvent
    ) throws TransitionException {
        this.def = def;

        this.id = id;
        this.history = ImmutableList.>builder().addAll(history).build();
        this.error = error;
        this.currentState = currentState;
        this.context = ImmutableMap.builder().putAll(context).build();
        this.errorType = errorType;
        this.persistenceCallback = persistenceCallback;
        this.eventCounter = eventCounter;
        this.crtEvent = crtEvent;
        if (LOGGER.isDebugEnabled())
            LOGGER.debug("creating a new state machine instance of type {}, with ID {} in state {}", def.getName(), id, def.getStateToString().apply(currentState));
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public S getCurrentState() {
        return currentState;
    }

    @Override
    public ImmutableMap getContext() {
        return ImmutableMap.builder().putAll(context).build();
    }

    @Override
    public List> getHistory() {
        return new ArrayList<>(history);
    }

    @Override
    public ErrorType getErrorType() {
        return errorType;
    }

    @Override
    public Optional getError() {
        return error;
    }

    @Override
    public MachineDefinition getDefinition() {
        return def;
    }

    @Override
    public boolean isErrorState() {
        return error.isPresent();
    }

    public long getTransitionEventCounter() {
        return eventCounter;
    }

    public Optional getCurrentEvent() {
        return crtEvent;
    }

    public Machine start() {
        if (LOGGER.isDebugEnabled())
            LOGGER.debug("starting state machine instance of type {}, with ID {}", def.getName(), id);
        if (currentState.equals(def.getInitialState()))
            return tryStp();
        else
            throw new IllegalStateException("machine is already started");
    }

    public Machine resume() {
        if (LOGGER.isDebugEnabled())
            LOGGER.debug("resuming state machine instance of type {}, with ID {}, out of state {}", def.getName(), id, def.getStateToString().apply(currentState));
        if (currentState.equals(def.getInitialState())) {
            throw new IllegalStateException("machine cannot be resumed out of the initial state, call start()");
        } else if (isErrorState()) {
            throw new IllegalStateException("machine cannot be resumed out of error state:  type " + def.getName() + ", id " + id + "  error " + error.get());
        } else
            return tryStp();
    }

    @Override
    public Machine sendEvent(E event) throws TransitionException {
        if (LOGGER.isDebugEnabled())
            LOGGER.debug("state machine instance of type {}, with ID {} receives event {}", def.getName(), id, def.getEventToString().apply(event));
        if (def.getTerminalStates().contains(currentState)) {
            LOGGER.debug("state machine instance of type {}, with ID {} receives event {} while in terminal state {}. Aborting.", def.getName(), id, def.getEventToString().apply(event), currentState);
            throw new IllegalStateException(new IllegalStateException("state machine of type " + def.getName() + " with ID " + id + " event " + event.toString() + " has received an event while in ternminal state " + currentState + ". Aborting."));
        } else if (isErrorState()) {
            LOGGER.error("a state machine cannot accept event when in error state:  type {}, id {}, error '{}'", def.getName(), id, error.get());
            throw new IllegalStateException("a state machine cannot accept event when in error state:  type " + def.getName() + ", id " + id + "  error " + error.get());
        } else {
            Transition transition = def.findEventTransition(currentState, event).orElseThrow(() -> new IllegalStateException("for machines of type " + def.getName() + " event " + event.toString() + " does not trigger any transition out of state " + currentState.toString()));
            if (this.eventCounter > 1 && this.crtEvent.map(e -> e.equals(event)).orElse(false)) {
                throw new IllegalStateException("an event of type " + event.getClass().getName() + " with id " + this.getId() + " has been delivered to a machine in state " + currentState + " while the machine was expecting an event of type " + this.crtEvent.getClass().getName());
            }
            if (this.eventCounter + 1 == transition.getEventCardinality() && this.crtEvent.map(e -> e.equals(event)).orElse(true)) {
                if (LOGGER.isDebugEnabled())
                    LOGGER.debug("a transition was found for machine instance of type {}, with ID {} from state {} to state {} on event {}",
                            def.getName(),
                            id,
                            def.getEventToString().apply(event),
                            def.getStateToString().apply(currentState),
                            def.getStateToString().apply(transition.getTo())
                    );

                return applyTransition(transition, null);
            } else {
                MachineInstanceImpl newMachine = new MachineInstanceImpl<>(id, def, currentState, context, history, errorType, error, persistenceCallback, this.eventCounter + 1, this.crtEvent);
                Machine updated = persistenceCallback.update(newMachine, Instant.now().toEpochMilli());
                return updated;
            }
        }
    }

    @Override
    public 

Machine sendEvent(E event, P param) throws TransitionException { if (LOGGER.isDebugEnabled()) LOGGER.debug("state machine instance of type {}, with ID {} receives event {} with parameter {}", def.getName(), id, def.getEventToString().apply(event), param.toString()); if (def.getTerminalStates().contains(currentState)) { LOGGER.debug("state machine instance of type {}, with ID {} receives event {} while in terminal state {}. Aborting.", def.getName(), id, def.getEventToString().apply(event), currentState); throw new IllegalStateException(new IllegalStateException("state machine of type " + def.getName() + " with ID " + id + " event " + event.toString() + " has received an event while in ternminal state " + currentState + ". Aborting.")); } else if (isErrorState()) { LOGGER.error("a state machine cannot accept event when in error state: type {}, id {}, error '{}'", def.getName(), id, error.get()); throw new IllegalStateException("a state machine cannot accept event when in error state: type " + def.getName() + ", id " + id + " error " + error.get()); } { final Transition transition = def.findEventTransition(currentState, event).orElseThrow(() -> new IllegalStateException("for machines of type " + def.getName() + " event " + event.toString() + " does not trigger any transition out of state " + currentState.toString())); if (LOGGER.isDebugEnabled()) LOGGER.debug("a transition was found for machine instance of type {}, with ID {} from state {} to state {} on event {}", def.getName(), id, def.getEventToString().apply(event), def.getStateToString().apply(currentState), def.getStateToString().apply(transition.getTo()) ); return applyTransition(transition, param); } } @Override public Machine recoverFromError(S state, Map context) { if (LOGGER.isDebugEnabled()) LOGGER.debug("state machine instance of type {}, with ID {} recovering from error to state {}, with context", def.getName(), id, def.getStateToString().apply(state)); Optional newError = Optional.empty(); ImmutableMap newContext = ImmutableMap.builder().putAll(context).build(); return new MachineInstanceImpl(id, def, state, newContext, history, ErrorType.NONE, newError, persistenceCallback); } private Machine tryStp() throws TransitionException { if (LOGGER.isDebugEnabled()) LOGGER.debug("checking stp transitions for state machine instance of type {}, with ID {}, out of state {}", def.getName(), id, def.getStateToString().apply(currentState)); if (this.isErrorState()) return this; else { return def.findStpTransition(currentState, context).map(t -> { if (LOGGER.isDebugEnabled()) LOGGER.debug("stp transitions for state machine instance of type {}, with ID {}, out of state {}, to state {}", def.getName(), id, def.getStateToString().apply(currentState), def.getStateToString().apply(t.getTo())); return applyTransition(t, null); }).orElse(this); } } private

Machine applyTransition(Transition transition, P param) throws TransitionException { S newState = transition.getTo(); if (LOGGER.isDebugEnabled()) LOGGER.debug("preparing to apply transition for state machine instance of type {}, with ID {}, out of state {} to state {}", def.getName(), id, def.getStateToString().apply(currentState), def.getStateToString().apply(newState)); if (LOGGER.isDebugEnabled()) LOGGER.debug("starting persistence transaction for state machine instance of type {}, with ID {}", def.getName(), id); final MachineAndStash machineAndStash; try { machineAndStash = persistenceCallback.runInTransaction(() -> { final Optional> action = transition.getAction(); try { ImmutableMap newContext = action.map( mapConsumer -> { if (LOGGER.isDebugEnabled()) LOGGER.debug("executing transition action for state machine instance of type {}, with ID {}, out of state {} to state {}", def.getName(), id, def.getStateToString().apply(currentState), def.getStateToString().apply(newState)); return ((TransitionAction

) mapConsumer).apply(context, param); } ).orElse(context); Optional newError = Optional.empty(); final MachineInstanceImpl newMachine = new MachineInstanceImpl<>(id, def, newState, newContext, history, ErrorType.NONE, newError, persistenceCallback); final ImmutableMap stashStore = action.map(TransitionAction::getStashStore).orElseGet(() -> ImmutableMap.builder().build()); if (LOGGER.isDebugEnabled()) LOGGER.debug("transition for state machine instance of type {}, with ID {}, out of state {} to state {} completed, preparing to save", def.getName(), id, def.getStateToString().apply(currentState), def.getStateToString().apply(newState)); final Machine updatedMachine = update(newMachine, transition, param, null); return new MachineAndStash<>(updatedMachine, stashStore); } catch (Throwable t) { if (LOGGER.isDebugEnabled()) LOGGER.debug("transition for state machine instance of type {}, with ID {}, out of state {} to state {} failed, switching to error state and saving", def.getName(), id, def.getStateToString().apply(currentState), def.getStateToString().apply(newState)); if (LOGGER.isDebugEnabled()) LOGGER.debug("commited persistence transaction for state machine instance of type {}, with ID {} - invoking error handler and saving", def.getName(), id); final Machine updatedMachine = applyErrorState(transition, param, t, ErrorType.TRANSITION); return new MachineAndStash<>(updatedMachine, t); } }); } catch (Exception e) { throw new TransitionException(ErrorType.TRANSITION, e); } if (LOGGER.isDebugEnabled()) LOGGER.debug("commited persistence transaction for state machine instance of type {}, with ID {}", def.getName(), id); final Machine machine = machineAndStash.getMachine(); if (machine.isErrorState()) { if (LOGGER.isDebugEnabled()) LOGGER.debug("machine instance of type {}, with ID {} is in error state, skipping post action", def.getName(), id); return machine; //throw new TransitionException(machineAndStash.getMachine(), transition, ErrorType.TRANSITION, machineAndStash.getErrorCause()); } else { transition.getPostAction().ifPresent(pa -> { if (LOGGER.isDebugEnabled()) LOGGER.debug("executing post transition action for state machine instance of type {}, with ID {}", def.getName(), id); final TransitionPostAction

postAction = (TransitionPostAction

) pa; try { postAction.setStash(ImmutableMap.builder().putAll(machineAndStash.getStashStore()).build()); postAction.accept(machineAndStash.getMachine().getContext(), param); } catch (Throwable t) { applyErrorState(transition, param, t, ErrorType.POST_TRANSITION); throw new TransitionException(machine, transition, ErrorType.POST_TRANSITION, t); } }); return ((MachineInstanceImpl) machine).tryStp(); } } private

Machine applyErrorState(Transition transition, P param, Throwable t, ErrorType newErrorType) { final String message = t.getMessage(); Optional newError = Optional.of(message == null ? t.getClass().getSimpleName() : message); final MachineInstanceImpl newMachine = new MachineInstanceImpl<>(id, def, currentState, context, history, newErrorType, newError, persistenceCallback); return update(newMachine, transition, param, t); } private

Machine update(Machine newMachine, Transition transition, P param, Throwable t) { long now = Instant.now().toEpochMilli(); if (newMachine.getErrorType() == ErrorType.TRANSITION) { def.getErrorHandler().accept(new DefaultErrorData<>(now, transition, param, t, false)); } else if (newMachine.getErrorType() == ErrorType.POST_TRANSITION) { def.getErrorHandler().accept(new DefaultErrorData<>(now, transition, param, t, true)); } else if (newMachine.getErrorType() == ErrorType.NONE && def.findStpTransition(newMachine.getCurrentState(), context).isEmpty()) { def.getTransitionHandler().accept(new DefaultTransitionData<>(now, transition, param)); } return persistenceCallback.update(newMachine, now); } @Override public boolean isTerminalState() { return def.getTerminalStates().contains(currentState); } @Override public boolean isIdleState() { return def.getIdleStates().contains(currentState); } private class DefaultErrorData implements ErrorData { private final Transition transition; private final P param; private final Throwable t; private final boolean isPostActionError; private final long lastModifiedEpoch; public DefaultErrorData(long lastModifiedEpoch, Transition transition, P param, Throwable t, boolean isPostActionError) { this.lastModifiedEpoch = lastModifiedEpoch; this.transition = transition; this.param = param; this.t = t; this.isPostActionError = isPostActionError; } @Override public String getStateMachineId() { return getId(); } @Override public String getStateMachineType() { return def.getName(); } @Override public long getLastModifiedEpoch() { return lastModifiedEpoch; } @Override public boolean isPostActionError() { return isPostActionError; } @Override public S getFrom() { return transition.getFrom(); } @Override public S getTo() { return transition.getTo(); } @Override public Optional getEvent() { return transition.getEvent(); } @Override public Map getContext() { return new HashMap<>(context); } @Override public P getEventParameter() { return param; } @Override public String getErrorMessage() { return t.getMessage(); } @Override public Throwable getCause() { return t; } } private class DefaultTransitionData implements TransitionData { private final Transition transition; private final P param; private final long lastModifiedEpoch; public DefaultTransitionData(long lastModifiedEpoch, Transition transition, P param) { this.lastModifiedEpoch = lastModifiedEpoch; this.transition = transition; this.param = param; } @Override public String getStateMachineId() { return getId(); } @Override public String getStateMachineType() { return def.getName(); } @Override public long getLastModifiedEpoch() { return lastModifiedEpoch; } @Override public S getFrom() { return transition.getFrom(); } @Override public S getTo() { return transition.getTo(); } @Override public Optional getEvent() { return transition.getEvent(); } @Override public Map getContext() { return new HashMap<>(context); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy