Persisting & Restoring Current State in Spring Statemachine
Asked Answered
E

1

6

I'm introducing Spring Statemachine into an existing project, with the hope of amalgamating and clarifying our business logic. We have various JPA entities with interconnected states and I'm having some trouble with setting a persisted state as the current state of an existing state machine.

I'm using a StateMachineFactory to create a new StateMachine instance for each entity instance. I'm storing the current state of the StateMachine in a separate field for Hibernate to persist and ideally need to sync the value of the persisted field with the StateMachine. My question is around how this should be typically achieved in Spring Statemachine.

@Entity
@EntityListeners(MyEntityListener.class)
public class MyEntity {

    @Column
    private MyState internalState; // Using AttributeConverter

    @Transient
    private StateMachine<MyState, Event> stateMachine;

}
public class MyEntityListener {

    @PostLoad
    public void postLoad(MyEntity entity) {
        // TODO Set StateMachine's current state to entity's internal state
    );

}
  1. One approach may be to define local transitions to move the initial state into the persisted state. I could then do a conditional check to find an event tied to a local transition, which would move the source state into the target state. This seems a little messy to me and I'd like to keep my state machine's configuration as clean as possible.

  2. I can't see how I can set the StateMachine's current state through a public API without moving through a transition and so another approach I explored is to wrap the StateMachine instance to expose the following method (as it's conveniently default scope):

package org.springframework.statemachine.support;

public abstract class AbstractStateMachine<S, E> extends StateMachineObjectSupport<S, E> implements StateMachine<S, E>, StateMachineAccess<S, E> {

    void setCurrentState(State<S, E> state, Message<E> message, Transition<S, E> transition, boolean exit, StateMachine<S, E> stateMachine)

}
package org.springframework.statemachine.support;

public class MyStateMachineWrapper<S, E> {

    private AbstractStateMachine<S, E> stateMachine;

    public MyStateMachineWrapper(StateMachine<S, E> stateMachine) {
        if (stateMachine instanceof AbstractStateMachine) {
            this.stateMachine = (AbstractStateMachine<S, E>)stateMachine;
        } else {
            throw new IllegalArgumentException("Provided StateMachine is not a valid type");
        }
    }

    public void setCurrentState(S status) {
        stateMachine.setCurrentState(findState(status), null, null, false, stateMachine);
    }

    private State<S, E> findState(S status) {
        for (State<S, E> state : stateMachine.getStates()) {
            if (state.getId() == status) {
                return state;
            }
        }

        throw new IllegalArgumentException("Specified status does not equate to valid State");
    }
}

I could then throw the following code into MyEntityListener.postLoad:

MyStateMachineWrapper<MyState, Event> myStateMachineWrapper = new MyStateMachineWrapper<>(entity.getStateMachine());
myStateMachineWrapper.setCurrentState(entity.getInternalState());

The above approach seems to work fine but I can't imagine this is how it was envisioned to work. Surely there's a cleaner method to achieve this or maybe the project isn't mature enough and doesn't include this functionality yet?

Thanks for any thoughts and opinions.

Elspet answered 2/6, 2015 at 12:1 Comment(0)
E
2

I've cleaned up option #2 above, changing the wrapper class to a utils class. To be clear, this approach takes advantage of the setCurrentState method having a default accessor and so this may end up being a brittle solution.

package org.springframework.statemachine.support;

public abstract class MyStateMachineUtils extends StateMachineUtils {

    public static <S, E> void setCurrentState(StateMachine<S, E> stateMachine, S state) {
        if (stateMachine instanceof AbstractStateMachine) {
            setCurrentState((AbstractStateMachine<S, E>)stateMachine, state);
        } else {
            throw new IllegalArgumentException("Provided StateMachine is not a valid type");
        }
    }

    public static <S, E> void setCurrentState(AbstractStateMachine<S, E> stateMachine, S state) {
        stateMachine.setCurrentState(findState(stateMachine, state), null, null, false, stateMachine);
    }

    private static <S, E> State<S, E> findState(AbstractStateMachine<S, E> stateMachine, S stateId) {
        for (State<S, E> state : stateMachine.getStates()) {
            if (state.getId() == stateId) {
                return state;
            }
        }

        throw new IllegalArgumentException("Specified State ID is not valid");
    }
}

This can then be used quite nicely like so:

MyStateMachineUtils.setCurrentState(entity.getStateMachine(), entity.getInternalState());
Elspet answered 2/6, 2015 at 16:54 Comment(4)
What you're trying to do is not yet possible because we're missing some internal features. github.com/spring-projects/spring-statemachine/issues/34 and github.com/spring-projects/spring-statemachine/issues/35 directly relates to these features. State machine synchronization needs more than just setting a current state. We're trying to implement SPI's which a distributed state machine can use to sync and persist its current state and everything else what it needs to work properly. I'm now planning to start this work now that second milestone is out.Yarn
Thanks for clarifying that. As there's no correct approach just yet, I'll thoroughly test the above workaround as our use case is pretty limited right now and hopefully we can do without your upcoming changes for now.Elspet
Alan, entity.getStateMachine() gives null, which is normal, since it's Transient. How does this work if you pass null in the MyStateMachineUtils.setCurrentState(entity.getStateMachine(), entity.getInternalState());Dowski
@Petar, at the time the entity created a new StateMachine instance from the StateMachineFactory in it's constructor, and set the initial state to mimic the persisted state. This project has moved on a lot since then and I don't think my 'hack' solution is still relevant. I'll try and put together a quick example of how it all works today soon, as by chance I need to a state-machine for a different project this week.Elspet

© 2022 - 2024 — McMap. All rights reserved.