Design of general programmable order state machine engine for Gaode taxi

Time:2021-7-24

Introduction: order status flow is the core work of the transaction system. The order system often has the characteristics of multiple states, long links and complex logic, as well as business characteristics such as multiple scenarios, multiple types and multiple business dimensions. On the premise of ensuring the stability of order status flow, scalability and maintainability are the problems we need to focus on and solve.
Design of general programmable order state machine engine for Gaode taxi

Author Liang Yan
Source: Ali technical official account

One background

Order status flow is the core work of the transaction system. Order systems often have the characteristics of many states, long links and complex logic, as well as business characteristics such as multiple scenarios, multiple types and multiple business dimensions. On the premise of ensuring the stability of order status flow, scalability and maintainability are the problems we need to focus on and solve.

Take the order status of Gaode taxi business as an example. The order status includes passenger placing an order, driver receiving the order, driver has arrived at the boarding point, starting the trip, ending the trip, confirming the fee, successful payment, order cancellation, order closing, etc; The order models include special cars, express cars, taxis and other models, and special cars are divided into comfort type, luxury type, business type and so on; Business scenarios include pick-up and drop off machines, enterprise cars, intercity carpooling, etc.

When the order status, type, scenario, and some other dimension combinations, each combination may have different processing logic or common business logic. In this case, various if else in the code must be unimaginable. How to deal with this complex order status flow business of “multi status + multi type + multi scenario + multi dimension” and ensure the scalability and maintainability of the whole system, the solution and scheme of this paper will be discussed with you.

II. Implementation scheme

To solve the complex order status flow business of “multi status + multi type + multi scenario + multi dimension”, we design it from vertical and horizontal dimensions. Vertically, it mainly solves problems from the perspective of business isolation and process arrangement, while horizontally, it mainly solves problems from the perspective of logic reuse and business expansion.

1 vertical solution to business isolation and process arrangement
Application of state mode

Generally, when dealing with a multi-state or multi-dimensional business logic, we will use the state mode or policy mode to solve it. We will not discuss the similarities and differences between the two design modes here. In fact, its core can be summarized as one word “divide and conquer”, abstracting a basic logic interface, and each state or type implements the interface, During business processing, call the corresponding business implementation according to different states or types to achieve the purpose of logic independence, mutual interference and code isolation.

This is not just from the perspective of scalability and maintainability. In fact, we do architecture stability and isolation as a basic means to reduce the impact. Similar isolation environments do gray-scale and batch release, and there is no expansion here.
Design of general programmable order state machine engine for Gaode taxi

/**
 *State machine processor interface
 */
public interface StateProcessor {
    /**
     *Entry to perform state migration
     */
    void action(StateContext context) throws Exception;
}
/**
 *Status processor corresponding to status a
 */
public class StateAProcessor interface StateProcessor {
    /**
     *Entry to perform state migration
     */
    @Override
    public void action(StateContext context) throws Exception {
    }
}

Single status or type can be solved through the above methods. Of course, this mode or idea can also be used to solve the combined business of “multi status + multi type + multi scenario + multi dimension”. Firstly, in the development stage, different dimensions are combined through an annotation @ orderporcessor to develop multiple corresponding specific implementation classes. In the system operation stage, the specific implementation class is dynamically selected by judging the [email protected] In orderprocessor, state is defined to represent the state to be processed by the current processor, bizcode and sceneid represent the business type and scenario respectively. These two fields are left for business expansion. For example, bizcode can be used to represent the product or order type, and sceneid can represent the business form or source scenario. If you want to expand the combination of multiple dimensions You can also assign the string spliced by multiple dimensions to bizcode and sceneid.

Limited by the specification that Java enumeration cannot inherit, if you want to develop general functions, enumeration cannot be used in annotations, so you have to use string here.

Design of general programmable order state machine engine for Gaode taxi

/**
 *Processor annotation ID of the state machine engine
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Component
public @interface OrderProcessor {
    /**
     *Specify the state. State cannot exist at the same time
     */
    String[] state() default {};
    /**
     *Business
     */
    String[] bizCode() default {};
    /**
     *Scene
     */
    String[] sceneId() default {};
}
/**
*Create status processor corresponding to order status
 */
@OrderProcessor(state = "INIT", bizCode = {"CHEAP","POPULAR"}, sceneId = "H5")
public class StateCreateProcessor interface StateProcessor {
}

Think again, because it involves state flow, it is impossible that a state a can only flow to state B. state a may flow to state B, State C and state D in different scenarios; In addition, although they are all transferred from state a to state B, the processing processes in different scenarios may be different. For example, the processes of paying orders from the pending state, the user’s initiative to initiate payment and the system secret free payment may be different. For the above two cases, we uniformly encapsulate the “scenario” here as “event”, and control the flow direction of the state in an “event driven” way. A state encounters a specific processing event to determine the business processing process and final state flow direction of the state. We can summarize that the state machine mode is simply to execute the next step of process processing logic and set a target state according to the source state and events under certain specific businesses and scenarios.

Some people may have some questions here. What is the difference between this “event” and the “multi scene” and “multi-dimensional” mentioned above. To explain, what we are talking about here is that “event” is a specific business action to be performed. For example, a user’s order is a business event, a user’s cancellation of an order is a business event, and a user’s payment order is also a business event. While “multi scenario” and “multi dimension” are dimensions that can be extended by the business itself, such as orders from self owned standard mode sources, orders from open platform APIs, and orders from third-party standard sources. The sources of XX applet and XX app can be defined as different scenarios, while pick-up, enterprise car and carpooling can be defined as dimensions.

Design of general programmable order state machine engine for Gaode taxi

public @interface OrderProcessor {
    /**
     *Specify status
     */
    String[] state() default {};
    /**
     *Order operation event
     */
    String event();
    ......
}
/**
 *Order status migration event
 */
public interface OrderStateEvent {
    /**
     *Order status event
     */
    String getEventType();
    /**
     *Order ID
     */
    String getOrderId();
    /**
     *If orderstate is not empty, it means that the order will be migrated only if it is in the current state
     */
    default String orderState() {
        return null;
    }
    /**
     *Do you want to create a new order
     */
    boolean newCreate();
}

Encapsulation of state migration process

After meeting the business scenario of multi-dimensional combination mentioned above and developing multiple implementation classes for execution, we consider whether these implementation classes can be abstracted and encapsulated again in the process, so as to reduce the R & D workload and realize the general process as much as possible. Through observation and abstraction, we find that there are three processes in each order status flow process: verification, business logic execution, and data update persistence; So again, a state flow can be divided into six stages: data prepare – > check – > get next state – > business logic execution – > data save – > after; Then, the six stage methods are connected in series through a template method to form a sequential execution logic. In this way, the whole shapeDesign of general programmable order state machine engine for Gaode taxi
It is clear and simple, and its maintainability has been improved to a certain extent.

![ Uploading...] ()

/**
 *State migration action processing steps
 */
public interface StateActionStep<T, C> {
    /**
     *Prepare data
     */
    default void prepare(StateContext<C> context) {
    }
    /**
     *Check
     */
    ServiceResult<T> check(StateContext<C> context);
    /**
     *Gets the next state that the current state processor is in after processing
     */
    String getNextState(StateContext<C> context);
    /**
     *State action method, main state migration logic
     */
    ServiceResult<T> action(String nextState, StateContext<C> context) throws Exception;
    /**
     *State data persistence
     */
    ServiceResult<T> save(String nextState, StateContext<C> context) throws Exception;
    /**
     *Subsequent processing after state migration is successful and persistent
     */
    void after(StateContext<C> context);
}
/**
 *State machine processor template class
 */
@Component
public abstract class AbstractStateProcessor<T, C> implements StateProcessor<T, C>, StateActionStep<T, C> {
    @Override
    public final ServiceResult<T> action(StateContext<C> context) throws Exception {
        ServiceResult<T> result = null;
        try {
            //Data preparation
            this.prepare(context);
            //Serial calibrator
            result = this.check(context);
            if (!result.isSuccess()) {
                return result;
            }
            //Getnextstate cannot be before prepare because some nextstates are converted from the data in prepare
            String nextState = this.getNextState(context);
            //Business logic
            result = this.action(nextState, context);
            if (!result.isSuccess()) {
                return result;
            }
            //Persistence
            result = this.save(nextState, context);
            if (!result.isSuccess()) {
                return result;
            }
            // after
            this.after(context);
            return result;
        } catch (Exception e) {
            throw e;
        }
    }
/**
 *Status processor corresponding to status a
 */
@OrderProcessor(state = "INIT", bizCode = {"CHEAP","POPULAR"}, sceneId = "H5")
public class StateCreateProcessor extends AbstractStateProcessor<String, CreateOrderContext> {
    ......
}

(1) Calibrator

As mentioned above, we all know that the flow of any state and even the call of the interface are in fact inseparable from some verification rules, especially for complex businesses, their verification rules and verification logic will be more complex. So how to decouple these verification rules? We should not only decouple the verification logic from the complex business process, but also simplify the complex verification rules to make the whole verification logic more scalable and maintainable. In fact, the method is also relatively simple. Referring to the above logic, you only need to abstract a verifier interface checker, disassemble the complex verification logic and form multiple verifier implementation classes with single logic. When calling check, the state processor only needs to call one interface and the verifier executes a collection of multiple checkers. After encapsulating the verifier checker, it is very simple to add a new verification logic. You only need to write a new verifier implementation class to add the verifier, and there is basically no change to other codes.

/**
 *State machine verifier
 */
public interface Checker<T, C> {
    ServiceResult<T> check(StateContext<C> context);
    /**
     *Execution order of multiple checkers
     */
    default int order() {
        return 0;
    }
}

When the logic is simple, the scalability and maintainability are solved, and the performance problems will appear. The serial execution performance of multiple verifier checkers must be poor. At this time, it is easy to think of using parallel execution. Yes, using multiple threads to execute multiple verifier checkers in parallel can significantly improve the execution efficiency. However, we should also be aware that some verifier logic may depend on before and after (in fact, it should not appear). In addition, in writing business processes, it is required that the execution of some verifiers must have a sequence before and after. In other processes, it is not required that the execution sequence of verifiers, but the return sequence in case of error. So how to ensure the sequence on the premise of parallelism Here you can use order + future. After a series of thinking and summary, we divide the verifier into three types: paramchecker, syncchecker and asyncchecker. Among them, paramchecker needs to be executed at the beginning of the state processor. Why do you do this? Because the parameters are illegal, there must be no need to continue to execute downward.

Design of general programmable order state machine engine for Gaode taxi

/**
 *State machine verifier
 */
public interface Checkable {
    /**
     *Parameter verification
     */
    default List<Checker> getParamChecker() {
        return Collections.EMPTY_LIST;
    }
    /**
     *Status checker to be executed synchronously
     */
    default List<Checker> getSyncChecker() {
        return Collections.EMPTY_LIST;
    }
    /**
     *Asynchronous verifier
     */
    default List<Checker> getAsyncChecker() {
        return Collections.EMPTY_LIST;
    }
}
/**
 *Actuator of calibrator
 */
public class CheckerExecutor {
    /**
     *Execute parallel verifier,
     *Judge the return according to the order of task delivery.
     */
    public ServiceResult<T, C> parallelCheck(List<Checker> checkers, StateContext<C> context) {
        if (!CollectionUtils.isEmpty(checkers)) {
            if (checkers.size() == 1) {
                return checkers.get(0).check(context);
            }
            List<Future<ServiceResult>> resultList = Collections.synchronizedList(new ArrayList<>(checkers.size()));
            checkers.sort(Comparator.comparingInt(Checker::order));
            for (Checker c : checkers) {
                Future<ServiceResult> future = executor.submit(() -> c.check(context));
                resultList.add(future);
            }
            for (Future<ServiceResult> future : resultList) {
                try {
                    ServiceResult sr = future.get();
                    if (!sr.isSuccess()) {
                        return sr;
                    }
                } catch (Exception e) {
                    log.error("parallelCheck executor.submit error.", e);
                    throw new RuntimeException(e);
                }
            }
        }
        return new ServiceResult<>();
    }
}
Use of checkable in template methods.

public interface StateActionStep<T, C> {
    Checkable getCheckable(StateContext<C> context);
    ....
}
public abstract class AbstractStateProcessor<T, C> implements StateProcessor<T>, StateActionStep<T, C> {
    @Resource
    private CheckerExecutor checkerExecutor;
    @Override
    public final ServiceResult<T> action(StateContext<C> context) throws Exception {
        ServiceResult<T> result = null;
        Checkable checkable = this.getCheckable(context);
        try {
            //Parameter calibrator
            result = checkerExecutor.serialCheck(checkable.getParamChecker(), context);
            if (!result.isSuccess()) {
                return result;
            }
            //Data preparation
            this.prepare(context);
            //Serial calibrator
            result = checkerExecutor.serialCheck(checkable.getSyncChecker(), context);
            if (!result.isSuccess()) {
                return result;
            }
            //Parallel verifier
            result = checkerExecutor.parallelCheck(checkable.getAsyncChecker(), context);
            if (!result.isSuccess()) {
                return result;
            }
        ......
}
An example of code application of checkable in a specific state processor.

@OrderProcessor(state = "INIT", bizCode = {"CHEAP","POPULAR"}, sceneId = "H5")
public class OrderCreatedProcessor extends AbstractStateProcessor<String, CreateOrderContext> {
    @Resource
    private CreateParamChecker createParamChecker;
    @Resource
    private UserChecker userChecker;
    @Resource
    private UnfinshChecker unfinshChecker;
    @Override
    public Checkable getCheckable(StateContext<CreateOrderContext> context) {
        return new Checkable() {
            @Override
            public List<Checker> getParamChecker() {
                return Arrays.asList(createParamChecker);
            }
            @Override
            public List<Checker> getSyncChecker() {
                return Collections.EMPTY_LIST;
            }
            @Override
            public List<Checker> getAsyncChecker() {
                return Arrays.asList(userChecker, unfinshChecker);
            }
        };
    }
......
The location of the checker is the verifier, which is responsible for verifying the legitimacy of parameters or businesses. However, in the actual coding process, there may be some temporary state operations in the checker, such as counting or locking before verification, and releasing according to the results after verification. Therefore, a unified release function needs to be supported.

public interface Checker<T, C> {
    ......
    /**
     *Is release required
     */
    default boolean needRelease() {
        return false;
    }
    /**
     *Release method after business execution,
     *For example, some businesses will add some status operations to the checker, and choose to process these status operations according to the results after the business is executed,
     *The most typical example is to add a lock to the checker and release the lock according to the result
     */
    default void release(StateContext<C> context, ServiceResult<T> result) {
    }
}
public class CheckerExecutor {
    /**
     *Release the checker
     */
    public <T, C> void releaseCheck(Checkable checkable, StateContext<C> context, ServiceResult<T> result) {
        List<Checker> checkers = new ArrayList<>();
        checkers.addAll(checkable.getParamChecker());
        checkers.addAll(checkable.getSyncChecker());
        checkers.addAll(checkable.getAsyncChecker());
        checkers.removeIf(Checker::needRelease);
        if (!CollectionUtils.isEmpty(checkers)) {
            if (checkers.size() == 1) {
                checkers.get(0).release(context, result);
                return;
            }
            CountDownLatch latch = new CountDownLatch(checkers.size());
            for (Checker c : checkers) {
                executor.execute(() -> {
                    try {
                        c.release(context, result);
                    } finally {
                        latch.countDown();
                    }
                });
            }
            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2) Context

It can be seen from the above code that several methods of the whole state migration are concatenated using the context object. There are three types of objects in the context object: (1) the basic information of the order (order ID, status, business attribute and scenario attribute), (2) the event object (whose parameters are basically the input parameters of the state migration behavior), and (3) the generic class determined by the specific processor. Generally, there are two schemes to transfer data in multiple methods: one is to use ThreadLocal for packaging, and each method can assign and value the current ThreadLocal; The other is to use a context object as the input parameter of each method. This scheme has some advantages and disadvantages. Using ThreadLocal is actually an “implicit call”. Although it can be called “everywhere”, it is not obvious to the user, it will be widely used in middleware, and it needs to be avoided in developing business code as much as possible; Using context as a parameter to pass in the method can effectively reduce the problem of “unknowability”.

Whether ThreadLocal or context is used as the parameter transfer, there are two schemes for the actual data carrier. The common one is to use map as the carrier. The business can set any kV as needed, but this situation poses a great challenge to the maintainability and readability of the code. Therefore, generic classes are used to fix the data format, What data needs to be transferred in a specific state processing process needs to be clearly defined. In fact, the principle is the same. In business development, try to use visibility to avoid unknowability.

public class StateContext<C> {
    /**
     *Order operation event
     */
    private OrderStateEvent orderStateEvent;
    /**
     *Basic order information required by state machine
     */
    private FsmOrder fsmOrder;
    /**
     *Business definable context generic object
     */
    private C context;
    public StateContext(OrderStateEvent orderStateEvent, FsmOrder fsmOrder) {
        this.orderStateEvent = orderStateEvent;
        this.fsmOrder = fsmOrder;
    }
    ......
/**
 *Order information base class information required by the state machine engine
 */
public interface FsmOrder {
    /**
     *Order ID
     */
    String getOrderId();
    /**
     *Order status
     */
    String getOrderState();
    /**
     *Business attributes of the order
     */
    String bizCode();
    /**
     *Scenario properties of the order
     */
    String sceneId();
}

(3) Status determination of migration to

Why should the next state (getnextstate) be abstracted as a single step instead of being set by the business itself? The reason is that the next state to be migrated is not necessarily fixed, that is, it may flow to different states according to the current state and events and more detailed logic. For example, the current status is that the user has placed an order and the event to occur is that the user cancels the order. At this time, according to different logic, the order may flow to the cancellation status, the cancellation pending approval status, or even the cancellation pending payment status. Of course, it depends on the thickness of the business system’s definition of States and events and the complexity of the state machine. As a state machine engine, the next state is determined by the business according to the context object.

Examples of using getnextstate() and state migration persistence:

@OrderProcessor(state = OrderStateEnum.INIT, event = OrderEventEnum.CREATE, bizCode = "BUSINESS")
public class OrderCreatedProcessor extends AbstractStateProcessor<String, CreateOrderContext> {
    
    ........
    
    @Override
    public String getNextState(StateContext<CreateOrderContext> context) {
    // if (context.getOrderStateEvent().getEventType().equals("xxx")) {
    //     return OrderStateEnum.INIT;
    //  }
        return OrderStateEnum.NEW;
    }
    @Override
    public ServiceResult<String> save(String nextState, StateContext<CreateOrderContext> context) throws Exception {
        OrderInfo orderInfo = context.getContext().getOrderInfo();
        //Update status
        orderInfo.setOrderState(nextState);
        //Persistence
//        this.updateOrderInfo(orderInfo);
        log.info("save BUSINESS order success, userId:{}, orderId:{}", orderInfo.getUserId(), orderInfo.getOrderId());
        Return new serviceresult < > (orderinfo. Getorderid(), "business order succeeded");
    }
}

Status message

Generally speaking, all state migrations should send corresponding messages, which are subscribed by downstream consumers for corresponding business processing.

(1) Status message content

There are usually two ways to send the content of a status migration message. One is to send only the notification of status migration. For example, only several key fields such as “order ID, status before change and status after change” are sent. The specific content required by the specific downstream business is checked by calling the corresponding interface; The other is to send all fields out, which is similar to sending a snapshot of the order content after status change. After receiving the message, the downstream almost does not need to check back in the calling interface.

(2) Timing of status messages

State migration has time sequence, so many downstream dependents also need to judge the order of messages. One implementation scheme is to use sequential messages (rocketmq, Kafka, etc.), but this scheme is rarely used based on the consideration of concurrent throughput; Generally, the “message sending time” or “status change time” fields are added to the message body, which can be processed by the consumer.

(3) Database state change and message consistency

Does the status change need to be consistent with the message?

It is often necessary. If the database state change is successful, but the status message is not sent, it will lead to the lack of processing logic of some downstream dependent parties. We know that the database and message system cannot guarantee 100% consistency. What we want to ensure is that when the main database state changes, the message should be sent as close as possible to 100% success.

So how do you guarantee it?

In fact, there are usually several schemes:

a) Use the two-stage message submission method supported by rocketmq:

Send a preprocessed message to the message server first
After the local database change is submitted, a message confirming the sending is sent to the message server
If the local database change fails, a cancel message is sent to the message server
If no confirmation message is sent to the message server for a long time, the message system will call back a pre agreed interface to check whether the local business is successful, so as to determine whether the message really occurs
Design of general programmable order state machine engine for Gaode taxi

b) Use the database transaction scheme to ensure that:

Create a message sending table, insert the message to be sent into the table, and submit it in a database transaction with the local business
Then, a scheduled task polls the transmission until the transmission succeeds, and deletes the current table record
c) Or use the database transaction scheme to ensure:

Create a message sending table, insert the message to be sent into the table, and submit it in a database transaction with the local business
Send message to message server
If the sending is successful, the current table record will be deleted
For messages that have not been successfully sent (that is, records that have not been deleted in the table), the scheduled task polls and sends them
image.png

Are there any other plans? yes , we have.

d) Reconciliation of data and compensation in case of inconsistency, so as to ensure the final consistency of data. In fact, no matter which scheme is used to ensure the consistency of database status changes and messages, the data reconciliation scheme is a “must” scheme.

So, are there any other plans? Still, there are many details about database state change and message consistency, and each scheme has corresponding advantages and disadvantages. This paper mainly introduces the design of state machine engine, but there is not much about message consistency. There may be a separate article to introduce and discuss database change and message consistency later.

2. Horizontally solve logic reuse and realize business expansion
After implementing the code separation governance based on “multi type + multi scenario + multi dimension” and the state machine model of the standard processing process template, it will be found that for the process processing of the same state with different types and different dimensions, sometimes some processes in multiple processing logic are the same or similar, For example, whether the payment phase is secret free or other methods, the processing logic of writing off coupons and setting invoice amount are the same; Sometimes, the processing logic of multiple types is mostly the same, but the difference is small. For example, the basic logic of the processing logic of the order placing process is almost the same, while the taxi may have more differences in individual processes such as taxi red envelope and no estimated price compared with online car Hailing.

For the above situation, in fact, it is necessary to support the reuse of a small part of logic or code segments or most processes on the basis of vertically solving business isolation and process arrangement, so as to reduce repeated construction and development. For this, we support two solutions in the state machine engine:

Plug-in based solution

The main logic of the plug-in is that the corresponding plug-in class can be loaded before the business logic execution (action) and data persistence (save) nodes for execution. It is mainly to operate the context object or initiate different process calls according to the context parameters, so as to achieve the purpose of changing the business data or process.

(1) Standard process + differentiated plug-in

As mentioned above, under the same state model and different types or dimensions, some logic or processing processes are the same, and a small part of logic is different. Therefore, we can define a processing process as standard or default processing logic, write differentiated code as plug-ins, and call different plug-ins for processing when the business is executed to specific differentiated logic. In this way, we only need to write plug-ins with differentiated logic for different types or dimensions, and the standard processing process is executed by the default processor.

Design of general programmable order state machine engine for Gaode taxi

(2) Difference process + common plug-in

Of course, for scenarios where a small part of logic and code can be shared, plug-in solutions can also be used. For example, we can package the same logic or code into a plug-in for multiple processors under maintenance in the same state. The plug-in can be identified and loaded for execution in multiple processors, so as to realize multiple different processes and use the form of the desired plug-in.

/**
 *Plug in annotation
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Component
public @interface ProcessorPlugin {
    /**
     *Specify the state. State cannot exist at the same time
     */
    String[] state() default {};
    /**
     *Order operation event
     */
    String event();
    /**
     *Business
     */
    String[] bizCode() default {};
    /**
     *Scene
     */
    String[] sceneId() default {};
}
 *Plug in processor
 */
public interface PluginHandler<T, C> extends StateProcessor<T, C> {
}
The execution logic of the plug in the processor template.

public abstract class AbstractStateProcessor<T, C> implements StateProcessor<T>, StateActionStep<T, C> {
    @Override
    public final ServiceResult<T> action(StateContext<C> context) throws Exception {
        ServiceResult<T> result = null;
        try {
            ......
            //Business logic
            result = this.action(nextState, context);
            if (!result.isSuccess()) {
                return result;
            }
            
            //Execute plug-in logic between action and save
            this.pluginExecutor.parallelExecutor(context);
            //Persistence
            result = this.save(nextState, context));
            if (!result.isSuccess()) {
                return result;
            }
            ......
        } catch (Exception e) {
            throw e;
        }
    }
Examples of plug-ins:

/**
 *Estimated price plug-in
 */
@ProcessorPlugin(state = OrderStateEnum.INIT, event = OrderEventEnum.CREATE, bizCode = "BUSINESS")
public class EstimatePricePlugin implements PluginHandler<String, CreateOrderContext> {
    @Override
    public ServiceResult action(StateContext<CreateOrderContext> context) throws Exception {
//        String price = priceSerive.getPrice();
        String price = "";
        context.getContext().setEstimatePriceInfo(price);
        return new ServiceResult();
    }
}

Solution based on code inheritance

When it is found that a new processing flow with different dimensions of status is the same as most of the logic of an existing processor, the newly written processor B can inherit the existing processor A. only processor B needs to overwrite the logic of different methods in a and realize the replacement of different logic. This scheme is easy to understand, but processor a has planned some extensible points, and other processors can overwrite and replace based on these extension points. Of course, a better solution is to implement a default processor first, encapsulate all standard processing flows and extensibility points, and inherit, overwrite and replace other processors.

@OrderProcessor(state = OrderStateEnum.INIT, event = OrderEventEnum.CREATE, bizCode = "CHEAP")
public class OrderCreatedProcessor extends AbstractStateProcessor<String, CreateOrderContext> {
    @Override
    public ServiceResult action(String nextState, StateContext<CreateOrderContext> context) throws Exception {
        CreateEvent createEvent = (CreateEvent) context.getOrderStateEvent();
        //Promotion information
        String promtionInfo = this.doPromotion();
        ......
    }
    
    /**
     *Extension points related to promotion
     */
    protected String doPromotion() {
        return "1";
    }
}
@OrderProcessor(state = OrderStateEnum.INIT, event = OrderEventEnum.CREATE, bizCode = "TAXI")
public class OrderCreatedProcessor4Taxi extends OrderCreatedProcessor<String, CreateOrderContext>  {
    @Override
    protected String doPromotion() {
        return "taxt1";
    }
}

3. Implementation process of status migration process
Execution process of state machine engine

Through the above introduction, we generally understand how to realize state process choreography, business isolation and expansion, but how does the state machine engine connect this process in series? In short, it is divided into two stages: initialization stage and runtime stage.

(1) State machine engine initialization phase

First, in the code writing stage, according to the above analysis, the business implements its own multiple required specific state processors by implementing the abstractstateprocessor template class and adding the @ orderprocessor annotation.

In the system initialization phase, all implementation classes annotated with @ orderprocessor will be managed by spring as spring beans. The state machine engine loads these state processor processors into its own managed container by listening to the registration of spring beans (beanpostprocessor). To put it bluntly, the state processor container is actually implemented by a multi-layer map. The key of the first layer map is the state, the key of the second layer map is the event corresponding to the state, a state can have multiple events to be processed, and the key of the third layer map is the specific scene code (that is, the combination of bizcode and sceneid), The final value is the abstractstateprocessor collection.

public class DefaultStateProcessRegistry implements BeanPostProcessor {

/**
 *The first level key is the order status.
 *The second level key is the event corresponding to the order status. A status can have multiple events.
 *The third layer key is the specific scene code. Multiple processors corresponding to the scene need to be filtered later to select a specific execution.
*/
    private static Map<String, Map<String, Map<String, List<AbstractStateProcessor>>>> stateProcessMap = new ConcurrentHashMap<>();
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof AbstractStateProcessor && bean.getClass().isAnnotationPresent(OrderProcessor.class)) {
            OrderProcessor annotation = bean.getClass().getAnnotation(OrderProcessor.class);
            String[] states = annotation.state();
            String event = annotation.event();
            String[] bizCodes = annotation.bizCode().length == 0 ? new String[]{"#"} : annotation.bizCode();
            String[] sceneIds = annotation.sceneId().length == 0 ? new String[]{"#"} : annotation.sceneId();
            initProcessMap(states, event, bizCodes, sceneIds, stateProcessMap, (AbstractStateProcessor) bean);
        }
        return bean;
    }
    private <E extends StateProcessor> void initProcessMap(String[] states, String event, String[] bizCodes, String[] sceneIds,
            Map<String, Map<String, Map<String, List<E>>>> map, E processor) {
        for (String bizCode : bizCodes) {
            for (String sceneId : sceneIds) {
                Arrays.asList(states).parallelStream().forEach(orderStateEnum -> {
                    registerStateHandlers(orderStateEnum, event, bizCode, sceneId, map, processor);
                });
            }
        }
    }
    /**
     *Initialize state machine processor
     */
    public <E extends StateProcessor> void registerStateHandlers(String orderStateEnum, String event, String bizCode, String sceneId,
                                      Map<String, Map<String, Map<String, List<E>>>> map, E processor) {
        //State dimension
        if (!map.containsKey(orderStateEnum)) {
            map.put(orderStateEnum, new ConcurrentHashMap<>());
        }
        Map<String, Map<String, List<E>>> stateTransformEventEnumMap = map.get(orderStateEnum);
        //Event dimension
        if (!stateTransformEventEnumMap.containsKey(event)) {
            stateTransformEventEnumMap.put(event, new ConcurrentHashMap<>());
        }
        // bizCode and sceneId
        Map<String, List<E>> processorMap = stateTransformEventEnumMap.get(event);
        String bizCodeAndSceneId = bizCode + "@" + sceneId;
        if (!processorMap.containsKey(bizCodeAndSceneId)) {
            processorMap.put(bizCodeAndSceneId, new CopyOnWriteArrayList<>());
        }
        processorMap.get(bizCodeAndSceneId).add(processor);
    }
}

(2) State machine engine runtime phase

After initialization, all state processors are loaded into the container. At runtime, a call to the state machine is initiated through an entry. The main parameters of the method are operation event and business input parameters. If it is a newly created order request, it needs to carry business (bizcode) and scene (sceneid) information. If it is an updated order, the state machine engine will automatically obtain business (bizcode) according to the oderid Scene (sceneid) and current state (state). Then, the engine obtains the corresponding specific processor processor from the state processor container according to state + Event + bizcode + sceneid, so as to carry out state migration.

/**
 *State machine execution engine
 */
public interface OrderFsmEngine {
    /**
     *The status migration event is not transmitted to fsmorder. By default, it is obtained from the fsmordservice interface according to the OrderID
     */
    <T> ServiceResult<T> sendEvent(OrderStateEvent orderStateEvent) throws Exception;
    /**
     *Execute the status migration event, which can carry the fsmorder parameter
     */
    <T> ServiceResult<T> sendEvent(OrderStateEvent orderStateEvent, FsmOrder fsmOrder) throws Exception;
}
@Component
public class DefaultOrderFsmEngine implements OrderFsmEngine {
    @Override
    public <T> ServiceResult<T> sendEvent(OrderStateEvent orderStateEvent) throws Exception {
        FsmOrder fsmOrder = null;
        if (orderStateEvent.newCreate()) {
            fsmOrder = this.fsmOrderService.getFsmOrder(orderStateEvent.getOrderId());
            if (fsmOrder == null) {
                throw new FsmException(ErrorCodeEnum.ORDER_NOT_FOUND);
            }
        }
        return sendEvent(orderStateEvent, fsmOrder);
    }
    @Override
    public <T> ServiceResult<T> sendEvent(OrderStateEvent orderStateEvent, FsmOrder fsmOrder) throws Exception {
        //Construct the current event context
        StateContext context = this.getStateContext(orderStateEvent, fsmOrder);
        //Gets the current event handler
        StateProcessor<T> stateProcessor = this.getStateProcessor(context);
        //Execute processing logic
        return stateProcessor.action(context);
    }
    private <T> StateProcessor<T, ?> getStateProcessor(StateContext<?> context) {
        OrderStateEvent stateEvent = context.getOrderStateEvent();
        FsmOrder fsmOrder = context.getFsmOrder();
        //Obtain the corresponding business processor set according to the status + event object
        List<AbstractStateProcessor> processorList = stateProcessorRegistry.acquireStateProcess(fsmOrder.getOrderState(),
                stateEvent.getEventType(), fsmOrder.bizCode(), fsmOrder.sceneId());
        if (processorList == null) {
            //Order status changed
            if (!Objects.isNull(stateEvent.orderState()) && !stateEvent.orderState().equals(fsmOrder.getOrderState())) {
                throw new FsmException(ErrorCodeEnum.ORDER_STATE_NOT_MATCH);
            }
            throw new FsmException(ErrorCodeEnum.NOT_FOUND_PROCESSOR);
        }
        if (CollectionUtils.isEmpty(processorResult)) {
            throw new FsmException(ErrorCodeEnum.NOT_FOUND_PROCESSOR);
        }
        if (processorResult.size() > 1) {
            throw new FsmException(ErrorCodeEnum.FOUND_MORE_PROCESSOR);
        }
        return processorResult.get(0);
    }
    private StateContext<?> getStateContext(OrderStateEvent orderStateEvent, FsmOrder fsmOrder) {
        StateContext<?> context = new StateContext(orderStateEvent, fsmOrder);
        return context;
    }
}

How does the actuator handle when multiple states are detected

It should be noted that multiple status processors may be obtained according to the state + Event + bizcode + sceneid information. It is possible that the business needs to rely solely on the bizcode and sceneid attributes and cannot effectively identify and locate the unique processor. Here, let’s open an interface for the business and decide to select one suitable for the current context from multiple processors, Specifically, the business processor uses the filter method to judge whether the call conditions are met according to the current context.

private <T> StateProcessor<T, ?> getStateProcessor(StateContext<?> context) {
    //Obtain the corresponding business processor set according to the status + event object
    List<AbstractStateProcessor> processorList = ...
    ......
    
    List<AbstractStateProcessor> processorResult = new ArrayList<>(processorList.size());
    //Get unique business processor according to context
    for (AbstractStateProcessor processor : processorList) {
        if (processor.filter(context)) {
            processorResult.add(processor);
        }
    }
    ......
}

Examples of the use of filter in specific state processor:

@OrderProcessor(state = OrderStateEnum.INIT, event = OrderEventEnum.CREATE, bizCode = "BUSINESS")
public class OrderCreatedProcessor extends AbstractStateProcessor<String, CreateOrderContext> {
    ......
    @Override
    public boolean filter(StateContext<CreateOrderContext> context) {
        OrderInfo orderInfo = (OrderInfo) context.getFsmOrder();
        if (orderInfo.getServiceType() == ServiceType.TAKEOFF_CAR) {
            return true;
        }
        return false;
    }
    ......
}

Of course, if there are still multiple state processors qualified after the business filter, then we can only throw exceptions here. This requires detailed planning of state and multi-dimensional processors during development.

4 state machine engine execution summary

State machine engine processing flow

Simple state machine engine execution process sorting, mainly introduces the running state machine execution process.

Design of general programmable order state machine engine for Gaode taxi

Principle of state processor

The principle and dependency sorting of simple state machine processor mainly introduces the process and details of state processor.

Design of general programmable order state machine engine for Gaode taxi

III. others

Any other questions? Think about it.

1. How to deal with the concurrency of state flow?

If an order is currently in status a and different event requests are initiated from different dimensions or entries, how to deal with it?

For example, if the current order is in the newly created status, the user initiates cancellation and the customer service also initiates cancellation. If the order is in the pending payment status, the system initiates secret free payment and the customer service or the user initiates price change. In these scenarios, whether the concurrency is caused by the system or business operations, concurrency is real. In this case, the principle is that an order can only have one status change event at the same time, and other requests can either be queued or returned to the upstream for processing or retry.

Our approach is:

At the SendEvent entry of the state machine orderfsmeengine, lock the same order dimension (redis distributed lock), and only one state change operation is allowed at the same time, while other requests are queued.

Verify the current state in the database layer, similar to optimistic locking. Finally, other requests are thrown into error and processed by the upstream business.

2 can you dynamically switch and arrange the state process?

At the beginning, we have a version. The definition of state processor is not implemented by annotation, but saves state, event, bizcode, sceneid and processor through database tables. During initialization, the processor is loaded from the database. At the same time, the corresponding relationships of state, event, bizcode, sceneid and processor can be dynamically adjusted through a background to achieve the effect of dynamic and flexible process configuration. However, with the launch of the business, dynamic changes have never been made, and in fact, they dare not operate. It is inconceivable to complete the core business of state flow once the change leads to failure.

3. General issues

In fact, not only the order system, but also the state machine logic can be handled with these ideas. Many other multi-dimensional businesses in daily life can be handled with these schemes.

4 combination with TMF

In fact, this set of state machine engine is relatively simple and not very friendly to the definition of business extension points. At present, we are also customizing extension points in combination with TMF framework. TMF achieves the effect of separating standard processes from specific business logic from the implementation of specific extension points.

Of course, regardless of the solution, the definition of extension point is the core concern and friendly encapsulation of the business.

Original link
This article is the original content of Alibaba cloud and cannot be reproduced without permission.