I. Business background

Marketing automation platform supports a variety of different types of operational activity strategies (such as SMS Push strategy, wechat text Push strategy, App Push strategy), and each type of activity has its own different execution process and activity status. For example, the execution process of SMS activities is as follows:

(Figure 1-1: SMS activity status transfer)

The whole SMS activity has gone through several state change processes: not starting → data preparation → data ready → activity push → activity end. Not only that, we found the following similar characteristics in the active business logic processing:

  • Each time you add a new activity business type, you add the corresponding activity state and the logic to handle state changes.

  • When a type of activity business process is modified, it may be necessary to change the original state transition process;

  • When each business writes its own business code for state transition, the core business logic and control logic are highly coupled, poorly extensible, and costly.

For the flow management of system state, there is a standard theoretical scheme model in computer field — finite state machine.

Understand state machines

2.1 Definition of state machines

Finite-state Machine (FSM), abbreviated as State Machine. Is a model that represents a finite number of states and transitions and trigger actions between those states.

  • State describes the state of a system object at some point in time.

  • Transitions indicate a state change, typically triggered conditionally by an external event.

  • An action is an operation to be performed in a given state.

In short, a state machine is composed of events, states and actions. The relationship among the three is as follows: the event triggers the transfer of state, and the transfer of state triggers the execution of subsequent actions. The action is not required, and only state transition can be performed without any operation.

(Figure 2-1: State generation)

Therefore, the state machine model can be used to describe the above [Figure 1-1: SMS activity state transfer] :

(Figure 2-2: SMS activity state machine)

A state machine is essentially a mathematical model of a system, which systematically expresses the solution to a problem. Let’s take a look at some of the ways that state machines can be implemented in real development.

2.2 Implementation of the state machine

2.2.1 Implementation based on conditional judgment

This is the most straightforward way to implement conditional judgments, which are hardcoded using if-else or switch-case branch judgments. For the above SMS activities, the code example based on conditional judgment is as follows:

/** * Enumeration of SMS activity status */
public enum ActivityState {
    NOT_START(0), // The activity has not started
    DATA_PREPARING(1), // Data is being prepared
    DATA_PREPARED(2), // The data is ready
    DATA_PUSHING(3), // Active push
    FINISHED(4); // End of activity
}
 
/** * SMS active state machine */
public class ActivityStateMachine {
    // Active status
    private ActivityState currentState;
    public ActivityStateMachine(a) {
        this.currentState = ActivityState.NOT_START;
    }
    /** * The activity starts */
    public void begin(a) {
        if (currentState.equals(ActivityState.NOT_START)) {
            this.currentState = ActivityState.DATA_PREPARING;
            // Send notification to operations personnel
            notice();
        }
        // do nothing or throw exception ...
    }
 
    /** * Data is calculated */
    public void finishCalData(a) {
        if (currentState.equals(ActivityState.DATA_PREPARING)) {
            this.currentState = ActivityState.DATA_PREPARED;
            // Send notification to operations personnel
            notice();
        }
        // do nothing or throw exception ...
    }
 
     /** * Activity push starts */
    public void beginPushData(a) {
        / / to omit
    }
     /** * Data push completed */
    public void finishPushData(a) {
        / / to omit}}Copy the code

State transfer and action triggering can be controlled through conditional branch judgment. The above if judgment condition can also be changed into switch statement, which controls the operations that can be executed in the state with the current state as the branch.

Applicable scenario

This method is applicable to the scenario where the number of service states is small or the jump between states is simple.

defects

When the corresponding relationship between triggering event and business state is not a simple one-to-one, multiple conditional branch judgments need to be nested, and the branch logic will become extremely complex. When the state flow changes, the branch logic also needs to be changed, which does not conform to the open and close principle, and the code readability and extensibility are very poor.

2.2.2 Implementation based on state mode

Those who understand design patterns can easily relate the concepts of state machine and state mode, and state mode can be used as an implementation of state machine. The main idea is to separate the behavior of different states through state mode, and call different methods corresponding to different states according to the change of state variables. A code example is as follows:

/** * Active interface */
interface IActivityState {
    ActivityState getName(a);
    // Trigger the event
    void begin(a);
    void finishCalData(a);
    void beginPushData(a);
    void finishPushData(a);
}
 
 /** * Specific state class - activity not started state */
public class ActivityNotStartState implements IActivityState {
    private ActivityStateMachine stateMachine;
    public ActivityNotStartState(ActivityStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    @Override
    public ActivityState getName(a) {
        return ActivityState.NOT_START;
    }
 
    @Override
    public void begin(a) {
        stateMachine.setCurrentState(new ActivityDataPreparingState(stateMachine));
        // Send notifications
        notice();
    }
 
    @Override
    public void finishCalData(a) {
        // do nothing or throw exception ...
    }
    @Override
    public void beginPushData(a) {
        // do nothing or throw exception ...
    }
    @Override
    public void finishPushData(a) {
        // do nothing or throw exception ...}}/** * Specific status class - Data preparation status */
public class ActivityDataPreparingState implements IActivityState {
    private ActivityStateMachine stateMachine;
    public ActivityNotStartState(ActivityStateMachine stateMachine) {
        this.stateMachine = stateMachine;
    }
 
    @Override
    public ActivityState getName(a) {
        return ActivityState.DATA_PREPARING;
    }
    @Override
    public void begin(a) {
        // do nothing or throw exception ...
    }
    public void finishCalData(a) {
        stateMachine.setCurrentState(new ActivityDataPreparedState(stateMachine));
        //TODO:Send a notification
    }
   @Override
    public void beginPushData(a) {
        // do nothing or throw exception ...
    }
    @Override
    public void finishPushData(a) {
        // do nothing or throw exception ...}}... (For reasons of length, other specific activities are omitted)/** * state machine */
public class ActivityStateMachine {
    private IActivityState currentState;
    public ActivityStateMachine(IActivityState currentState) {
        this.currentState = new ActivityNotStartState(this);
    }
    public void setCurrentState(IActivityState currentState) {
        this.currentState = currentState;
    }
    public void begin(a) {
        currentState.begin();
    }
    public void finishCalData(a) {
        currentState.finishCalData();
    }
    public void beginPushData(a) {
        currentState.beginPushData();
    }
    public void finishPushData(a) { currentState.finishCalData(); }}Copy the code

State patterns define the state-behavior correspondence and encapsulate the behavior of each state in corresponding state classes. We only need to extend or modify the specific state class to meet the requirements of the corresponding process state.

Applicable scenario

It is applicable to scenarios with few service states and simple state transfer. Compared with the previous if/ Switch conditional branching method, when new or modified service state flows are added or modified, the impact granularity is smaller, the scope is controllable, and the expansibility is stronger.

defects

It is also difficult to deal with complex scenarios of business process state transitions, where the use of state patterns introduces many state classes and methods, and when the state logic changes, the code becomes difficult to maintain.

It can be seen that although the above two methods can realize the trigger, transfer and action processes of the state machine, the reusability is very low. If you want to build an abstract state component that can satisfy most business scenarios, you can’t.

2.2.3 Implementation based on DSL

2.2.3.1 DSL is introduced

Domain-specific Languages (DSL) is a computer programming language with limited expression in a Specific field. Unlike general-purpose programming languages, DSLS work only in a specific domain and focus on solving a particular piece of the system in that domain. DSLs are classified into Internal DSLs and external DSLs.

  • Internal DSL: system-based host language, DSLS written and processed by the host language, such as java-based internal DSL, C++ based internal DSL, Javascript based internal DSL.

  • External DSLS: DSLS written and handled by custom languages or other programming languages, as opposed to system host languages, that have independent parsers. For example, regular expressions, XML, SQL, and HTML.

(Read more about DSLS: Domain Specific Languages by Martin Fowler).

2.2.3.2 Selection of DSL and implementation of state machine

Using A DSL as a development tool allows you to describe the behavior of the system in a clearer and more expressive form. DSL is also the recommended way to implement the state machine. You can choose an internal DSL or an external DSL to implement it according to your own needs.

  • Internal DSLS: Business systems that only want to configure state machines directly through code can choose to use internal DSLS, which are simple and straightforward and do not rely on additional parsers and components.

Java internal DSL generally uses Builder Pattern and Fluent Interface (Builder Pattern and streaming Interface). Examples are as follows:

StateMachineBuilder builder = new StateMachineBuilder();
                     builder.sourceState(States.STATE1)
                            .targetState(States.STATE2)
                            .event(Events.EVENT1)
                            .action(action1());
Copy the code
  • External DSLS: Leverage the parsing capabilities of external storage and common scripting languages to enable dynamic configuration at run time, support visual configuration, and cross-language application scenarios.

An external DSL essentially describes the state transition process in other external languages, such as using XML:

<state id= "STATE1">
  <transition event="EVENT1"  target="STATE2">
    <action method="action1()"/>
  </transition>
</state>
 
<state id= "STATE2">
</state>
Copy the code

External DSLS are generally stored in external storage such as configuration files or databases. After the corresponding text parser, the configuration of external DSLS can be parsed into a model similar to internal DSLS for flow processing. At the same time, because of the independence and persistence of external storage, dynamic change and visual configuration can be easily supported at run time.

Java open source state machine frameworks are basically dSL-based implementations.

Open source state machine framework

We use three open source state machine frameworks respectively to complete the state transfer process of SMS activities.

3.1 Spring the Statemachine

enum ActivityState {
    NOT_START(0),
    DATA_PREPARING(1),
    DATA_PREPARED(2),
    DATA_PUSHING(3),
    FINISHED(4);
 
    private int state;
    private ActivityState(int state) {
        this.state = state; }}enum ActEvent {
    ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
}
 
@Configuration
@EnableStateMachine
public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<ActivityState.ActEvent> {
    @Override
    public void configure(StateMachineStateConfigurer<ActivityState, ActEvent> states)
            throws Exception {
                states
                .withStates()
                .initial(ActivityState.NOT_START)
                .states(EnumSet.allOf(ActivityState.class));
    }
    @Override
    public void configure(StateMachineTransitionConfigurer<ActivityState, ActEvent> transitions)
            throws Exception { transitions .withExternal() .source(ActivityState.NOT_START).target(ActivityState.DATA_PREPARING) .event(ActEvent.ACT_BEGIN).action(notice()) .and() .withExternal() .source(ActivityState.DATA_PREPARING).target(ActivityState.DATA_PREPARED) .event(ActEvent.FINISH_DATA_CAL).action(notice()) .and() .withExternal() .source(ActivityState.DATA_PREPARED).target(ActivityState.DATA_PUSHING) .event(ActEvent.FINISH_DATA_PREPARE).action(notice()) .and() .withExternal() .source(ActivityState.DATA_PUSHING).target(ActivityState.FINISHED) .event(ActEvent.FINISH_DATA_PUSHING).action(notice())  .and() ; }@Override
    public void configure(StateMachineConfigurationConfigurer<ActivityState, ActEvent> config)
            throws Exception {
        config.withConfiguration()
                .machineId("ActivityStateMachine");
    }
    public Action<ActivityState, ActEvent> notice(a) {
        return context -> System.out.println([State before change] :+context.getSource().getId()+"; [Changed status] :"+context.getTarget().getId());
    }
 
   / / test class
   class DemoApplicationTests {
    @Autowired
    private StateMachine<ActivityState, ActEvent> stateMachine;
 
    @Test
    void contextLoads(a) { stateMachine.start(); stateMachine.sendEvent(ActEvent.ACT_BEGIN); stateMachine.sendEvent(ActEvent.FINISH_DATA_CAL); stateMachine.sendEvent(ActEvent.FINISH_DATA_PREPARE); stateMachine.sendEvent(ActEvent.FINISH_DATA_PUSHING); stateMachine.stop(); }}Copy the code

The state machine of Java internal DSL is realized by rewriting three configure methods of configuration template class and completing state initialization, state transition and state machine declaration in the form of streaming Api. External state machines are triggered by sendEvent events to promote the automatic flow of state machines.

advantage

  • Spring Statemachine is an official Spring product with a strong ecological community.

  • In addition to supporting the basic state machine configuration, it also has a variety of features such as nested sub-state machines, zK-based distributed state machines and external storage persistence.

defects

  • Spring Statemachine stores the context-related properties of the current Statemachine inside each Statemachine instance, that is, stateful (as you can see from the fact that the Statemachine flow is triggered only by an event as an argument), so a singleton Statemachine instance is not thread-safe. The only way to ensure thread-safety is to create a new state machine instance each time in factory mode, which will affect the overall system performance in high concurrency scenarios.

  • The code hierarchy is slightly complex, the cost of secondary development and transformation is large, and there is no need to use so many functions in general scenarios, which makes it heavy to use.

3.2 Squirrel Foundation.

public class SmsStatemachineSample {
    // 1. State definition
     enum ActivityState {
        NOT_START(0),
        DATA_PREPARING(1),
        DATA_PREPARED(2),
        DATA_PUSHING(3),
        FINISHED(4);
 
        private int state;
        private ActivityState(int state) {
            this.state = state; }}// 2. Event definition
    enum ActEvent {
        ACT_BEGIN, FINISH_DATA_CAL,FINISH_DATA_PREPARE,FINISH_DATA_PUSHING
    }
 
    // 3. The text on the status machine
    class StatemachineContext {}@StateMachineParameters(stateType=ActivityState.class, eventType=ActEvent.class, contextType=StatemachineContext.class)
    static class SmsStatemachine extends AbstractUntypedStateMachine {
        protected void notice(ActivityState from, ActivityState to, ActEvent event, StatemachineContext context) {
            System.out.println([State before change] :+from+"; [Changed status] :"+to); }}public static void main(String[] args) {
        // 4. Build state transitionsUntypedStateMachineBuilder builder = StateMachineBuilderFactory.create(SmsStatemachine.class); builder.externalTransition().from(ActivityState.NOT_START).to(ActivityState.DATA_PREPARING).on(ActEvent.ACT_BEGIN).callM ethod("notice"); builder.externalTransition().from(ActivityState.DATA_PREPARING).to(ActivityState.DATA_PREPARED).on(ActEvent.FINISH_DATA_ CAL).callMethod("notice"); builder.externalTransition().from(ActivityState.DATA_PREPARED).to(ActivityState.DATA_PUSHING).on(ActEvent.FINISH_DATA_PR EPARE).callMethod("notice"); builder.externalTransition().from(ActivityState.DATA_PUSHING).to(ActivityState.FINISHED).on(ActEvent.FINISH_DATA_PUSHING ).callMethod("notice");
 
        // 5. Trigger state machine flow
        UntypedStateMachine fsm = builder.newStateMachine(ActivityState.NOT_START);
        fsm.fire(ActEvent.ACT_BEGIN,  null);
        fsm.fire(ActEvent.FINISH_DATA_CAL, null);
        fsm.fire(ActEvent.FINISH_DATA_PREPARE, null);
        fsm.fire(ActEvent.FINISH_DATA_PUSHING, null); }}Copy the code

Squirrel – Foundation is a lightweight state library designed to provide a lightweight, highly flexible, extensible, easy-to-use, type-safe, programmable state machine implementation for enterprise use.

advantage

  • In line with the goal concept, compared with Spring Statemachine, it does not rely on the Spring framework, and is more lightweight in design and implementation. Although it is also a stateful design, the cost of creating Statemachine instances is less, and the function is more concise, which is relatively suitable for secondary development.

  • The corresponding documentation and test cases are also rich, easy to use developers.

defects

  • Too much emphasis on the concept of “convention over configuration”, many default processing, such as state transfer after the action is invoked by method name, is not conducive to operation management.

  • Community activity is not high.

3.3 Cola the Statemachine

/** * State machine factory */
public class StatusMachineEngine {
    private StatusMachineEngine(a) {}private static final Map<OrderTypeEnum, String> STATUS_MACHINE_MAP = new HashMap();
 
    static {
        // SMS push status
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.SMS, "smsStateMachine");
        //PUSH state
        STATUS_MACHINE_MAP.put(ChannelTypeEnum.PUSH, "pushStateMachine");
        / /...
    }
 
    public static String getMachineEngine(ChannelTypeEnum channelTypeEnum) {
        return STATUS_MACHINE_MAP.get(channelTypeEnum);
    }
 
   /** * Triggers state transition *@param channelTypeEnum
     * @paramStatus Current status *@paramEventType Trigger event *@paramContext Context parameter */
    public static void fire(ChannelTypeEnum channelTypeEnum, String status, EventType eventType, Context context) {
        StateMachine orderStateMachine = StateMachineFactory.get(STATUS_MACHINE_MAP.get(channelTypeEnum));
        // Push the state machine to flow
        orderStateMachine.fireEvent(status, eventType, context);
    }
 
/** * Initializes the SMS push activity state machine */
@Component
public class SmsStateMachine implements ApplicationListener<ContextRefreshedEvent> {
 
    @Autowired
    private  StatusAction smsStatusAction;
    @Autowired
    private  StatusCondition smsStatusCondition;
 
    // Based on the DSL build state configuration that triggers event transitions and subsequent actions
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { StateMachineBuilder<String, EventType, Context> builder = StateMachineBuilderFactory.create(); builder.externalTransition() .from(INIT) .to(NOT_START) .on(EventType.TIME_BEGIN) .when(smsStatusAction.checkNotifyCondition()) .perform(smsStatusAction.doNotifyAction()); builder.externalTransition() .from(NOT_START) .to(DATA_PREPARING) .on(EventType.CAL_DATA) .when(smsStatusCondition.doNotifyAction()) .perform(smsStatusAction.doNotifyAction()); builder.externalTransition() .from(DATA_PREPARING) .to(DATA_PREPARED) .on(EventType.PREPARED_DATA) .when(smsStatusCondition.doNotifyAction()) .perform(smsStatusAction.doNotifyAction()); . (omit the other state) builder. Build (StatusMachineEngine. GetMachineEngine ChannelTypeEnum. SMS ()); }/ / call end
   public class Client {
     public static void main(String[] args){
          // Build the active context
          Context context = newContext(...) ;// Trigger state flowStatusMachineEngine.fire(ChannelTypeEnum.SMS, INIT, EventType.SUBMIT, context); }}}Copy the code

Cola Statemachine is a Statemachine framework within the open source framework of alibaba Cola. The biggest difference from the previous two is: Stateless design — The current state needs to be taken as an input parameter when state machine flow is triggered. There is no need to keep the current state context message in the state machine instance. There is only one state machine instance, which directly ensures thread safety and high performance.

advantage

  • Lightweight stateless, safe and high performance.

  • Simple design, easy to expand.

  • Community activity is high.

defects

  • Advanced functions such as nesting and parallelism are not supported.

3.4 summary

The comparison of the three open source state machine frameworks is as follows:

Systems that want to directly leverage the capabilities of the open source state machine can be selected according to their own business requirements and process complexity.

Iv. Marketing automation business case practice

4.1 Design and Selection

The business characteristics of Vivo marketing automation are:

  • There are many types of operation activities, large business flow, relatively simple process and high performance requirements.

  • Processes change frequently, and service status needs to be added frequently. Rapid configuration and change need to be supported.

  • After status is triggered, there will be a variety of different service operations, such as message notification after status change, service processing after status completion, etc. Asynchronous operation and easy expansion need to be supported.

In view of the above business characteristics, in the actual project development, we are based on the implementation scheme of open source state — based on the way of internal DSL development. At the same time, it draws on the characteristics of the above open source framework and selects a lightweight design with stateless high performance, simple functions and asynchronous execution support.

  • Stateless high performance: To ensure high performance, it adopts stateless state machine design and only needs one state machine instance to run.

  • Functional simplicity: Minimal design principles, preserving only core design, such as event triggering, basic flow of state, subsequent operations, and context parameter handling.

  • Asynchronous execution of actions: Asynchronous decoupling of asynchronous business processes in the form of thread pools or message queues.

4.2 Core Process

  • Using the internal DSL streaming interface design of open source state machine, the definition of state machine is scanned when the application is started.

  • Create asynchronous processing thread pools to support post-action of business;

  • Parse the DSL configuration of the state machine and initialize the state machine instance;

  • Build execution context to store instances of each state machine and other execution process information;

  • When the state machine is triggered, the transfer process is automatically matched according to the trigger conditions and the current state to promote the state machine flow.

  • Perform post-synchronous/asynchronous processing operations.

(Figure 4-1: Core process design)

4.3 Practical Thinking

1) State machine configuration visualization, combined with external DSL mode (such as JSON mode, stored in the database), support faster configuration.

2) Currently, only simple flow of state is supported, and the flow interface extension point is added in the flow process to cope with complex scenes that may occur in the future.

Five, the summary

A state machine is composed of events, states and actions. The relationship among the three is as follows: the event triggers the transfer of state, and the transfer of state triggers the execution of subsequent actions. Using state machines to manage system state can improve service scalability and cohesion. State machines can be implemented using conditional branching, state patterns, and DSL-based implementations, of which the more expressive DSLS are also the implementations of many open source state machines. Suitable selection can be made based on the characteristics of the open source state machine and its own project requirements, or state components can be customized based on the previous scheme.

This is the third in a series of articles on marketing automation:

Deciphering marketing Automation Technology | Introduction

“How to improve the Development of Vivo marketing automation business design Mode | Engine 01”

We’ll continue with the rest of the series, each with a detailed breakdown of the technical practices.

Author: Vivo Internet Server Team -Chen Wangrong