Introduction and use of state machine



1. Introduction to state machine


1.1 Definition
Let's first give the basic definition of a state machine. In a word:
State machine is the abbreviation of finite state automaton, which is a mathematical model abstracted from the operation rules of real things.
Let's first explain what is "state" (State). Real things have different states. For example, an automatic door has two states: open and closed. What we usually refer to as a state machine is a finite state machine, that is, the number of states of the described thing is limited. For example, the state of an automatic door is two open and closed.
State machine, that is, State Machine, does not refer to an actual machine, but refers to a mathematical model . To put it bluntly, it generally refers to a state transition diagram. For example, according to the operating rules of automatic doors, we can abstract the following graph.
 The automatic door has two states, open and closed. In the closed state, if the door open signal is read, the state will switch to open. If the door close signal is read in the open state, the state will switch to closed.
The full name of state machine is finite state automata, and the word automatic also contains important meanings. Given a state machine, given its current state and its inputs, the output state can be computed explicitly . For example, for an automatic door, given the initial state closed and given the input "open the door", then the next state can be computed.
In this way, we have introduced the basic definition of the state machine. Repeat: state machine is the abbreviation of finite state automata , and it is a mathematical model abstracted from the operation rules of real things.

1.2 Four concepts

The four concepts of the state machine are given below.
The first one is State, state. A state machine must contain at least two states. For example, in the example of the automatic door above, there are two states: open and closed.
The second is Event, event. An event is a trigger condition or password to perform an operation. For automatic doors, "press the door open button" is an event.
The third is Action, action. Actions to be performed after an event occurs. For example, the event is "press the button to open the door", and the action is "open the door". When programming, an Action generally corresponds to a function.
The fourth is Transition, transformation. That is, changing from one state to another. For example, "door opening process" is a transformation.


Two, DSL


2.1 DSL

A DSL is a tool whose core value is that it provides a means to more clearly communicate the intent of some part of the system.

This clarity is not just an aesthetic pursuit. The easier a piece of code is to understand, the easier it is to find bugs and make changes to the system. Therefore, we encourage meaningful variable names, clear documentation, and clear code structure. For the same reason, we should also encourage the adoption of DSLs.

By definition, a DSL is a computer programming language with limited expressiveness for a specific domain.
This definition contains 3 key elements:
Language nature : A DSL is a programming language, so it must be able to express coherently—whether it is a single expression or a combination of multiple expressions.
Limited expressiveness (limited expressiveness) : General-purpose programming languages ​​provide a wide range of capabilities: support a variety of data, control, and abstract structures. These abilities are useful, but they can also make languages ​​difficult to learn and use. A DSL supports only the minimal set of features required for a particular domain. Using DSL, it is impossible to build a complete system, on the contrary, it can solve the problem of a certain aspect of the system.
Domain focus : A language of this limited capability is only useful in a well-defined small domain. This field is what makes the language worth using.
For example, the regular expression, /\d{3}-\d{3}-\d{4}/ is a typical DSL, which solves the problem of string matching in this specific field.

2.2 Classification of DSL

按照类型,DSL可以分为三类:内部DSL(Internal DSL)、外部DSL(External DSL)、以及语言工作台(Language Workbench)。
Internal DSL是一种通用语言的特定用法。用内部DSL写成的脚本是一段合法的程序,但是它具有特定的风格,而且只用到了语言的一部分特性,用于处理整个系统一个小方面的问题。用这种DSL写出的程序有一种自定义语言的风格,与其所使用的宿主语言有所区别。例如我们的状态机就是Internal DSL,它不支持脚本配置,使用的时候还是Java语言,但并不妨碍它也是DSL。
builder.externalTransition()                .from(States.STATE1)                .to(States.STATE2)                .on(Events.EVENT1)                .when(checkCondition())                .perform(doAction());
External DSL是一种“不同于应用系统主要使用语言”的语言。外部DSL通常采用自定义语法,不过选择其他语言的语法也很常见(XML就是一个常见选 择)。比如像Struts和Hibernate这样的系统所使用的XML配置文件。
Workbench是一个专用的IDE,简单点说,工作台是DSL的产品化和可视化形态。
三个类别DSL从前往后是有一种递进关系,Internal DSL最简单,实现成本也低,但是不支持“外部配置”。Workbench不仅实现了配置化,还实现了可视化,但是实现成本也最高。他们的关系如下图所示:
2.3 DSL示例

2.3.1 内部DSL示例

HTML: 通过自然语言编写
在Groovy中,通过DSL可以用易读的写法生成XML
  
  
  
  
  
def s = new StringWriter()def xml = new MarkupBuilder(s)xml.html{    head{        title("Hello - DSL")        script(ahref:"https://xxxx.com/vue.js")        meta(author:"marui116")    }    body{        p("JD-ILT-ITMS")    }}println s.toString()
最后将生成
  
  
  
  
  
<html>  <head>    <title>Hello - DSL</title>    <script ahref='https://xxxx.com/vue.js' />    <meta author='marui116' />  </head>  <body>    <p>JD-ILT-ITMS</p>  </body></html>
MarkupBuilder的作用说明:
  
  
  
  
  
A helper class for creating XML or HTML markup. The builder supports various 'pretty printed' formats.Example:  new MarkupBuilder().root {    a( a1:'one' ) {      b { mkp.yield( '3 < 5' ) }      c( a2:'two', 'blah' )    }  }  Will print the following to System.out:  <root>    <a a1='one'>      <b>3 < 5</b>      <c a2='two'>blah</c>    </a>  </root>
这里相对于Java这样的动态语言,最为不同的就是xml.html这个并不存在的方法居然可以通过编译并运行,它内部重写了invokeMethod方法,并进行闭包遍历,少写了许多POJO对象,效率更高。

2.3.2 外部DSL

以plantUML为例,外部DSL不受限于宿主语言的语法,对用户很友好,尤其是对于不懂宿主语言语法的用户。但外部DSL的自定义语法需要有配套的语法分析器。常见的语法分析器有:YACC、ANTLR等。
 https://github.com/plantuml/plantuml 
 https://plantuml.com/zh/ 

2.3.3 DSL & DDD(领域驱动)

DDD和DSL的融合有三点:面向领域、模型的组装方式、分层架构演进。DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。
 它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。 外部 DSL 就是对领域模型的一种组装方式。


三、状态机实现的调研


3.1 Spring Statemachine

官网: https://spring.io/projects/spring-statemachine#learn 
源码: https://github.com/spring-projects/spring-statemachine 
API: https://docs.spring.io/spring-statemachine/docs/3.2.0/api/ 
Spring Statemachine is a framework for application developers to use state machine concepts with Spring applications. Spring Statemachine 是应用程序开发人员在Spring应用程序中使用状态机概念的框架。
Spring Statemachine 提供如下特色:
  • Easy to use flat one level state machine for simple use cases.(易于使用的扁平单级状态机,用于简单的使用案例。)
  • Hierarchical state machine structure to ease complex state configuration.(分层状态机结构,以简化复杂的状态配置。)
  • State machine regions to provide even more complex state configurations.(状态机区域提供更复杂的状态配置。)
  • Usage of triggers, transitions, guards and actions.(使用触发器、transitions、guards和actions。)
  • Type safe configuration adapter.(应用安全的配置适配器。)
  • Builder pattern for easy instantiation for use outside of Spring Application context(用于在Spring Application上下文之外使用的简单实例化的生成器模式)
  • Recipes for usual use cases(通常用例的手册)
  • Distributed state machine based on a Zookeeper State machine event listeners.(基于Zookeeper的分布式状态机状态机事件监听器。)
  • UML Eclipse Papyrus modeling.(UML Eclipse Papyrus 建模)
  • Store machine config in a persistent storage.(存储状态机配置到持久层)
  • Spring IOC integration to associate beans with a state machine.(Spring IOC集成将bean与状态机关联起来)
 Spring StateMachine提供了papyrus的Eclipse Plugin,用来辅助构建状态机。
 更多Eclipse建模插件可参见文档:https://docs.spring.io/spring-statemachine/docs/3.2.0/reference/#sm-papyrus 
Spring状态机的配置、定义、事件、状态扩展、上下文集成、安全性、错误处理等,可以参看如下文档:
 https://docs.spring.io/spring-statemachine/docs/3.2.0/reference/#statemachine 

3.2 COLA状态机DSL实现

COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向对象分层架构”。目前COLA已经发展到COLA v4。COLA提供了一个DDD落地的解决方案,其中包含了一个开源、简单、轻量、性能极高的状态机DSL实现,解决业务中的状态流转问题。
COLA状态机组件实现一个仅支持简单状态流转的状态机,该状态机的核心概念如下图所示,主要包括:
  1. State:状态
  2. Event:事件,状态由事件触发,引起变化
  3. Transition:流转,表示从一个状态到另一个状态
  4. External Transition:外部流转,两个不同状态之间的流转
  5. Internal Transition:内部流转,同一个状态之间的流转
  6. Condition:条件,表示是否允许到达某个状态
  7. Action:动作,到达某个状态之后,可以做什么
  8. StateMachine:状态机
 整个状态机的核心语义模型(Semantic Model):


四、状态机DEMO


4.1 Spring状态机示例

例如,起始节点为SI、结束节点为SF,起始节点后续有S1、S2、S3三个节点的简单状态机。
Spring Boot项目需引入Spring状态机组件。
  
  
  
  
  
<dependency>    <groupId>org.springframework.statemachine</groupId>    <artifactId>spring-statemachine-core</artifactId>    <version>3.2.0</version></dependency>

4.1.1 构造状态机

  
  
  
  
  
@Configuration@EnableStateMachine@Slf4jpublic class SimpleStateMachineConfiguration extends StateMachineConfigurerAdapter<String, String> {    /**     * 定义初始节点、结束节点和状态节点     * @param states the {@link StateMachineStateConfigurer}     * @throws Exception     */    @Override    public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {        states.withStates()            .initial("SI")            .end("SF")            .states(new HashSet<String>(Arrays.asList("S1", "S2", "S3")));    }
/** * 配置状态节点的流向和事件 * @param transitions the {@link StateMachineTransitionConfigurer} * @throws Exception */ @Override public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception { transitions.withExternal() .source("SI").target("S1").event("E1").action(initAction()) .and() .withExternal() .source("S1").target("S2").event("E2").action(s1Action()) .and() .withExternal() .source("S2").target("SF").event("end"); }
/** * 初始节点到S1 * @return */ @Bean public Action<String, String> initAction() { return ctx -> log.info("Init Action -- DO: {}", ctx.getTarget().getId()); }
/** * S1到S2 * @return */ @Bean public Action<String, String> s1Action() { return ctx -> log.info("S1 Action -- DO: {}", ctx.getTarget().getId()); }}
4.1.2 状态机状态监听器
  
  
  
  
  
@Component@Slf4jpublic class StateMachineListener extends StateMachineListenerAdapter<String, String> {     @Override    public void stateChanged(State from, State to) {        log.info("Transitioned from {} to {}", from == null ? "none" : from.getId(), to.getId());    }}

4.1.3 状态机配置

  
  
  
  
  
@Configuration@Slf4jpublic class StateMachineConfig implements WebMvcConfigurer {    @Resource    private StateMachine<String, String> stateMachine;
@Resource private StateMachineListener stateMachineListener;
@PostConstruct public void init() { stateMachine.addStateListener(stateMachineListener); }}

4.1.4 接口示例

4.1.4.1 获取状态机状态列表

  
  
  
  
  
@RequestMapping("info")public String info() {    return StringUtils.collectionToDelimitedString(            stateMachine.getStates()                    .stream()                    .map(State::getId)                    .collect(Collectors.toList()),                    ",");}

4.1.4.2 状态机开启

在对Spring状态机进行事件操作之前,必须先开启状态机
  
  
  
  
  
@GetMapping("start")public String start() {    stateMachine.startReactively().block();    return state();}

4.1.4.3 事件操作

  
  
  
  
  
@PostMapping("event")public String event(@RequestParam(name = "event") String event) {    Message<String> message = MessageBuilder.withPayload(event).build();    return stateMachine.sendEvent(Mono.just(message)).blockLast().getMessage().getPayload();}

4.1.4.4 获取状态机当前状态

  
  
  
  
  
@GetMapping("state")public String state() {    return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId())).block();}

4.1.4.5 一次状态转换的控制台输出

  
  
  
  
  
: Completed initialization in 0 ms: Transitioned from none to SI: Init Action -- DO: S1: Transitioned from SI to S1: S1 Action -- DO: S2: Transitioned from S1 to S2: Transitioned from S2 to SF
可以看到,状态从none到SI开始节点,再到S1、S2,然后S2通过E2事件到SF结束节点。

4.2 COLA状态机示例

例如: iTMS中的运输需求单的状态目前有: 待分配、已分配、运输中、部分妥投、全部妥投、全部拒收、已取消。

4.2.1 构造状态机

com.jd.ilt.component.statemachine.demo.component.statemachine.TransNeedStateMachine
  
  
  
  
  
StateMachineBuilder<TransNeedStatusEnum, TransNeedEventEnum, Context> builder = StateMachineBuilderFactory.create();
// 接单后,运输需求单生成运输规划单builder.externalTransition() .from(None) .to(UN_ASSIGN_CARRIER) .on(Create_Event) .when(checkCondition()) .perform(doAction());
// 运输规划单生成调度单,调度单绑定服务商builder.externalTransition() .from(UN_ASSIGN_CARRIER) .to(UN_ASSIGN_CAR) .on(Assign_Carrier_Event) .when(checkCondition()) .perform(doAction());
// 服务商分配车辆、司机builder.externalTransition() .from(UN_ASSIGN_CAR) .to(ASSIGNED_CAR) .on(Assign_Car_Event) .when(checkCondition()) .perform(doAction());
// 货物揽收builder.externalTransition() .from(ASSIGNED_CAR) .to(PICKUPED) .on(Trans_Job_Status_Change_Event) .when(checkCondition()) .perform(doAction());
// 揽收货物更新到运输中builder.externalTransition() .from(ASSIGNED_CAR) .to(IN_TRANSIT) .on(Trans_Job_Status_Change_Event) .when(checkCondition()) .perform(doAction());
// 运输中更新到过海关builder.externalTransition() .from(IN_TRANSIT) .to(PASS_CUSTOMS) .on(Trans_Job_Status_Change_Event) // 检查是否需要过海关 .when(isTransNeedPassCustoms()) .perform(doAction());
// 妥投builder.externalTransition() .from(PASS_CUSTOMS) .to(ALL_DELIVERIED) .on(All_Delivery_Event) .when(checkCondition()) .perform(doAction());
// 车辆揽收、运输、过海关的运输状态,都可以直接更新到妥投Stream.of(PICKUPED, IN_TRANSIT, PASS_CUSTOMS) .forEach(status -> builder.externalTransition() .from(status) .to(ALL_DELIVERIED) .on(Trans_Job_Status_Change_Event) .when(checkCondition()) .perform(doAction()) );
// 待分配、待派车、已派车可取消Stream.of(UN_ASSIGN_CARRIER, UN_ASSIGN_CAR, ASSIGNED_CAR) .forEach(status -> builder.externalTransition() .from(status) .to(CANCELED) .on(Order_Cancel_Event) .when(checkCondition()) .perform(doAction()) );
// 妥投、和取消可结束归档Stream.of(ALL_DELIVERIED, CANCELED) .forEach(status -> builder.externalTransition() .from(status) .to(FINISH) .on(Order_Finish) .when(checkCondition()) .perform(doAction()) );
stateMachine = builder.build("TransNeedStatusMachine");
从代码中,可以方便的扩展状态和对应的事件,状态机自动进行业务状态的流转。生成的状态流转图如下所示:
  
  
  
  
  
@startumlNone --> UN_ASSIGN_CARRIER : Create_EventUN_ASSIGN_CARRIER --> UN_ASSIGN_CAR : Assign_Carrier_EventUN_ASSIGN_CAR --> ASSIGNED_CAR : Assign_Car_EventASSIGNED_CAR --> CANCELED : Order_Cancel_EventASSIGNED_CAR --> PICKUPED : Trans_Job_Status_Change_EventASSIGNED_CAR --> IN_TRANSIT : Trans_Job_Status_Change_EventIN_TRANSIT --> PASS_CUSTOMS : Trans_Job_Status_Change_EventPASS_CUSTOMS --> ALL_DELIVERIED : Trans_Job_Status_Change_EventPASS_CUSTOMS --> ALL_DELIVERIED : All_Delivery_EventIN_TRANSIT --> ALL_DELIVERIED : Trans_Job_Status_Change_EventALL_DELIVERIED --> FINISH : Order_FinisUN_ASSIGN_CAR --> CANCELED : Order_Cancel_EventUN_ASSIGN_CARRIER --> CANCELED : Order_Cancel_EventPICKUPED --> ALL_DELIVERIED : Trans_Job_Status_Change_EventCANCELED --> FINISH : Order_Finis@enduml

4.2.2 状态机事件处理

  
  
  
  
  
/** * 一种是通过Event来进行事件分发,不同Event通过EventBus走不同的事件响应* 另一种是在构造状态机时,直接配置不同的Action * @return */private Action<TransNeedStatusEnum, TransNeedEventEnum, Context> doAction() {    log.info("do action");    return (from, to, event, ctx) -> {        log.info(ctx.getUserName()+" is operating trans need bill "+ctx.getTransNeedId()+" from:"+from+" to:"+to+" on:"+event);        if (from != None) {            TransNeed transNeed = ctx.getTransNeed();            transNeed.setStatus(to.name());            transNeed.setUpdateTime(LocalDateTime.now());            transNeedService.update(transNeed);        }
eventBusService.invokeEvent(event, ctx); };}
Event和EventBus简单Demo示例:
/** * @author marui116 * @version 1.0.0 * @className TransNeedAssignCarrierEvent * @description TODO* @date 2023/3/28 11:08 */@Component@EventAnnonation(event = TransNeedEventEnum.Assign_Carrier_Event)@Slf4jpublic class TransNeedAssignCarrierEvent implements EventComponent {
@Override public void invokeEvent(Context context) { log.info("分配了服务商,给服务商发邮件和短信,让服务商安排"); }}
  
  
  
  
  
/** * @author marui116 * @version 1.0.0 * @className TransNeedAssignCarEvent * @description TODO* @date 2023/3/28 11:05 */@Component@EventAnnonation(event = TransNeedEventEnum.Assign_Car_Event)@Slf4jpublic class TransNeedAssignCarEvent implements EventComponent {    @Override    public void invokeEvent(Context context) {        log.info("分配了车辆信息,给运单中心发送车辆信息");    }}
/** * @author marui116 * @version 1.0.0 * @className EventServiceImpl * @description TODO* @date 2023/3/28 10:57 */@Servicepublic class EventBusServiceImpl implements EventBusService {    @Resource    private ApplicationContextUtil applicationContextUtil;
private Map<TransNeedEventEnum, EventComponent> eventComponentMap = new ConcurrentHashMap<>();
@PostConstruct private void init() { ApplicationContext context = applicationContextUtil.getApplicationContext(); Map<String, EventComponent> eventBeanMap = context.getBeansOfType(EventComponent.class); eventBeanMap.values().forEach(event -> { if (event.getClass().isAnnotationPresent(EventAnnonation.class)) { EventAnnonation eventAnnonation = event.getClass().getAnnotation(EventAnnonation.class); eventComponentMap.put(eventAnnonation.event(), event); } }); }
@Override public void invokeEvent(TransNeedEventEnum eventEnum, Context context) { if (eventComponentMap.containsKey(eventEnum)) { eventComponentMap.get(eventEnum).invokeEvent(context); } }}

4.2.3 状态机上下文

  
  
  
  
  
@Data@NoArgsConstructor@AllArgsConstructor@Builderpublic class Context {    private String userName;    private Long transNeedId;    private TransNeed transNeed;}

4.2.4 状态枚举

  
  
  
  
  
public enum TransNeedStatusEnum {    /**     * 开始状态     */    None,    /**     * 待分配陆运服务商     */    UN_ASSIGN_CARRIER,    /**     * 待分配车辆和司机     */    UN_ASSIGN_CAR,    /**     * 订单已处理,已安排司机提货     */    ASSIGNED_CAR,    /**     * 已完成提货     */    PICKUPED,    /**     * 运输中     */    IN_TRANSIT,    /**     * 已通过内地海关     */    PASS_CUSTOMS,    /**     * 您的货物部分妥投部分投递失败     */    PARTIAL_DELIVERIED,    /**     * 您的货物妥投     */    ALL_DELIVERIED,    /**     * 您的货物被拒收     */    ALL_REJECTED,    /**     * 委托订单被取消     */    CANCELED,    /**     * 单据结束归档     */    FINISH;
}

4.2.5 事件枚举

  
  
  
  
  
public enum TransNeedEventEnum {        // 系统事件        Create_Event,        Normal_Update_Event,        /**         * 分配服务商事件         */        Assign_Carrier_Event,        /**         * 派车事件         */        Assign_Car_Event,
// 车辆任务(trans_jbo)执行修改调度单(trans_task)状态的事件 Trans_Job_Status_Change_Event,
// 派送事件 Partial_Delivery_Event, All_Delivery_Event, Partial_Reject_Event, All_Reject_Event,
// 调度单中的任务单取消事件 Order_Cancel_Event,
// 单据结束 Order_Finish;
public boolean isSystemEvent() { return this == Create_Event || this == Normal_Update_Event; }}

4.2.6 接口Demo

4.2.6.1 创建需求单

  
  
  
  
  
/** *  接单* @return */@RequestMapping("/start/{fsNo}/{remark}")public Context start(@PathVariable("fsNo") String fsNo, @PathVariable("remark") String remark) {    Context context = contextService.getContext();    Object newStatus = stateMachine.getStateMachine().fireEvent(TransNeedStatusEnum.None, TransNeedEventEnum.Create_Event, context);    TransNeed transNeed = transNeedService.createTransNeed(fsNo, remark, newStatus.toString());    context.setTransNeed(transNeed);    context.setTransNeedId(transNeed.getId());    return context;}

4.2.6.2 分配服务商

  
  
  
  
  
/** * 运输规划单生成调度单,调度单绑定服务商*/@RequestMapping("/assignCarrier/{id}")public Context assignCarrier(@PathVariable("id") Long id) {    Context context = contextService.getContext(id);    TransNeedStatusEnum prevStatus = TransNeedStatusEnum.valueOf(context.getTransNeed().getStatus());    stateMachine.getStateMachine().fireEvent(prevStatus, TransNeedEventEnum.Assign_Carrier_Event, context);    return context;}

4.2.6.3 分配车辆

  
  
  
  
  
@RequestMapping("/assignCar/{id}")public Context assignCar(@PathVariable("id") Long id) {    Context context = contextService.getContext(id);    TransNeedStatusEnum prevStatus = TransNeedStatusEnum.valueOf(context.getTransNeed().getStatus());    log.info("trans need id: {}, prev status: {}", id, prevStatus);    stateMachine.getStateMachine().fireEvent(prevStatus, TransNeedEventEnum.Assign_Car_Event, context);    return context;}


五、状态机对比


综上,如果是直接使用状态机的组件库,可以考虑使用Spring的状态机,如果是要渐进式的使用状态机,逐步按照自己的需求去定制化状态机以满足业务需求,建议使用COLA的状态机。


六、iTMS使用状态机的计划


iTMS准备渐进式的使用COLA的状态机组件,先轻量级使用状态机进行运输相关域的状态变更,后续按照DDD的状态和事件的分析,使用CQRS的设计模式对命令做封装,调用状态机进行业务流转。
-end-

本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

比 Protocol Buffers 快无限倍,开源十年后 Cap'n Proto 1.0 终发布 华中科技大学博士后复现 LK-99 磁悬浮现象 龙芯中科研制成功新一代处理器龙芯 3A6000 miniblink 108 版本成功编译,全球最小 Chromium 内核 ChromeOS 将浏览器和操作系统拆分独立 特斯拉中国商城上架 1TB 固态硬盘,售价 2720 元 华为正式发布 HarmonyOS 4 火绒安全升级版本,导致所有基于 Electron 的应用卡顿 AWS 明年开始对 IPv4 公网地址收取费用 Nim v2.0 正式发布,命令式编程语言
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10092914