Blind Date Model and Finite State Machine

The article " Blind Date Model and Finite State Machine " was first published at: blog.ihypo.net/16573725253…

If someone asks you how many steps you need to put the elephant in the refrigerator, Aunt Song's smile will come to your mind and blurt out: 3 steps.

If someone asks you how many steps it takes to go from single to life, you really need to count with your fingers.

blind date model

Once you get into programming, it is like the sea, and the end of the code is a blind date. Some seniors have said that programmers who have not had a blind date are incomplete, and even the code they write is less profound. Although I haven't experienced it myself, if you are counting your life with your fingers, you can start with a blind date.

Starting with a blind date

You cleared your throat, swallowed, and quickly rubbed the corners of the menu with your two fingers, looking up at the strange and beautiful lady across the table: "What do you want to eat?"

"Just eat whatever you want".

You continue to bury your head in the menu, order a few random dishes, and forget to order drinks in a hurry.

"Well, let me introduce myself first." You push the menu, thinking that it's a big deal to be an HR interview, and you start to introduce yourself. By the way, you mention that you don't write Java.

When I came home after dinner, I recalled the details of the meal, and embarrassment surged, but fortunately, the conversation was very enjoyable, as if there was a drama. After you're not finished, you open a green chat app, click on the familiar and unfamiliar avatar, and start the second half.

The following month, it was the restaurant again. You decide to pierce that layer of window paper, and the cafe and the script kill have left a good memory in common, so the time is ripe.

You cleared your throat, swallowed, and quickly rubbed the corner of the menu with your two fingers, looking up at the young lady across the table: "What do you want to eat?"

Blind date status flow chart

I'm afraid only a lame person like me can write such a story without ups and downs, why? Because emotion is one of the most complicated things in the world, and the blind date that uses chat as the way of play and promotes emotional development as the underlying logic is also a battle with the most variables. Although it is impossible to exhaustively enumerate the blind date process, if you try to abstract and quantify the blind date scene process, you must be able to get a complex state flow diagram.

The above picture, Yue Lao looked at it and called it an expert. There are only many intermediate states in the blind date process, so how many steps does it take to go from being single to settling for life?

Finite State Machine

A finite state machine (FSM) is a mathematical model of cybernetics. Used to represent behaviors such as transitions and actions between states of an enumerable kind. Human speech is used to control changes in the state of a machine.

basic concept

从相亲的模型中可以看到,一个有限状态机包含『状态』和『行为』两大基本概念。

『状态』包括两个核心元素:

  • 第一个是 State 即状态,一个状态机至少要包含两个状态。比如最简单的状态机就是电灯,包含了 “亮” 和 “灭” 两个状态。
  • 第二个是 Transition 即状态流转,也就是从一个状态变化为另一个状态。比如 “开灯的过程” 就是一个状态流转。

『行为』也包含两个核心元素

  • 第一个是 Event 即事件,事件就是执行某个操作,也可以看做外部系统的输入。比如 “按下灯开关” 就是一个事件。
  • 第二个是 Action 即动作,事件发生后引发的变更,也可以看做状态机对外部的输出。比如 “按下灯开关” 事件后,会触发 “灯泡亮起” 这个动作。

适用场景

有限状态机的适用场景很多,尤其是状态复杂的场景,比如订单、任务管理等。有限状态机的本质是维护状态流转图,使得在复杂的用户输入中,依然保持状态的合法和安全。

(图来自《京东京麦交易平台设计与实现》)

除了复杂状态流转的场景,当状态无法明确的情况下,有限状态机也可以被考虑。仿佛和『有限』这个字眼相悖,这里的无法明确是指需求无法明确,也许今天 3 个状态就满足需求了,但下个版本可能需要 5 个状态。对于有限状态机来说,多加两种状态只不过是在状态流转图了多几条边而已。

有限状态机实现

本节的实现以 GoFlow 为例,GoFlow 是一个用 Golang 实现的任务引擎 代码可见: github.com/basenana/go…

MIT 数位系统概论中总结了一套实现 FSM 的方法论:

  • 明确状态类型(identify distinct states)
  • 创建状态流转图(create state transition diagram)
  • 编写状态流转逻辑(write for next-state logic)
  • 编写输出信号逻辑(write for output signals)

我们就从这个四个角度讲下 GoFlow 的 FSM 实现。

枚举状态类型

首先是明确『有限』的状态,对于 GoFlow 这种任务管理系统来说,状态肯定是围绕任务展开的:

const (
	CreatingStatus     fsm.Status = "creating"
	InitializingStatus            = "initializing"
	RunningStatus                 = "running"
	SucceedStatus                 = "succeed"
	FailedStatus                  = "failed"
	ErrorStatus                   = "error"
	PausedStatus                  = "paused"
	CanceledStatus                = "canceled"
)

状态流转图

状态流转图是一个有向图,每个状态都是一个点,两点之间的边即是事件,边的方向是流转方向。最简单存储方式有两种:

  1. 十字链表法:以当前状态为维度维护指向其前驱节点的边。也就是以当前状态为 Key,保存的 Value 即是事件类型和可以流转的下一级状态
  2. 边存储法:以事件为维度维护所有的边。也就是事件类型为 Key,Value 是上级状态和下级状态

两种存储方式各有好处,因为 GoFlow 状态流转图的边的数量比较少,也就选择了第二种方式:基于 Map 以事件类型为 Key,存储所有的边。这种存储方式可以快速通过一个事件,找到能执行的所有操作。

type edge struct {
	from Status
	to   Status
	when EventType
	do   Handler
	next *edge
}

type FSM struct {
	obj   Stateful
	graph map[EventType]*edge

	crtBuilder *edgeBuilder
	mux        sync.Mutex
	logger     log.Logger
}

从 GoFlow 的状态枚举可以看出当前的引擎相对简单,因此它的状态流转图也并不复杂:

构建状态流转图的逻辑:

func buildFlowTaskFSM(r *runner, t flow.Task) *fsm.FSM {
	m := fsm.New(fsm.Option{
		Obj:    t,
		Logger: r.logger.With(fmt.Sprintf("task.%s.fsm", t.Name())),
	})

	m.From([]fsm.Status{flow.InitializingStatus}).
		To(flow.RunningStatus).
		When(flow.TaskTriggerEvent).
		Do(r.handleTaskRun)

	m.From([]fsm.Status{flow.RunningStatus}).
		To(flow.SucceedStatus).
		When(flow.TaskExecuteFinishEvent).
		Do(r.handleTaskSucceed)

	m.From([]fsm.Status{flow.RunningStatus}).
		To(flow.PausedStatus).
		When(flow.TaskExecutePauseEvent).
		Do(r.handleTaskPaused)

	m.From([]fsm.Status{flow.PausedStatus}).
		To(flow.RunningStatus).
		When(flow.TaskExecuteResumeEvent).
		Do(r.handleTaskResume)

	m.From([]fsm.Status{flow.InitializingStatus, flow.RunningStatus, flow.PausedStatus}).
		To(flow.CanceledStatus).
		When(flow.TaskExecuteCancelEvent).
		Do(r.handleTaskCanceled)

	m.From([]fsm.Status{flow.InitializingStatus, flow.RunningStatus}).
		To(flow.FailedStatus).
		When(flow.TaskExecuteErrorEvent).
		Do(r.handleTaskFailed)

	return m
}

状态流转逻辑与触发动作

状态的流转是 FSM 的核心,GoFlow 的状态流转,基本流程:

  1. 根据当前事件获取该事件影响的边
  2. 基于边的初始状态与当前状态匹配
  3. 如果存在匹配的边,则把当前状态机状态置为下级状态
  4. 状态转换完成后,执行对应的动作
func (m *FSM) Event(event Event) error {
	m.mux.Lock()
	defer m.mux.Unlock()

	m.logger.Debugf("handler fsm event: %s", event.Type)
	head := m.graph[event.Type]
	if head == nil {
		return nil
	}

	defer func() {
		if panicErr := recover(); panicErr != nil {
			m.logger.Errorf("event %s handle panic: %v", event, panicErr)
		}
	}()

	for head != nil {
		if m.obj.GetStatus() == head.from {
			m.logger.Infof("change obj status from %s to %s with event: %s", head.from, head.to, event.Type)
			m.obj.SetStatus(head.to)
			if event.Message != "" {
				m.obj.SetMessage(event.Message)
			}

			if head.do != nil {
				if handleErr := head.do(event); handleErr != nil {
					m.logger.Errorf("event %s handle failed: %s", event.Type, handleErr.Error())
					return handleErr
				}
			}
			return nil
		}
		head = head.next
	}
	return fmt.Errorf("get event %s and current status is %s, no change path matched", event.Type, m.obj.GetStatus())
}

有状态的状态机

GoFlow 的状态机是一种非常简单或者说偷懒的实现,主要问题在于这是一个有状态的状态机。听起来很奇怪,状态机本来不就是用来维护状态的吗。这里的状态其实指的是状态机本身的状态。简单来说,当 FSM 被实例化之后,这个用来维护任务的状态的 FSM 只能在当前进程(或者说副本)中使用,因为其他副本并没有这个 FSM 实例对象。

对于一个长时间运行的任务引擎来说,这件事情本身也无可厚非,但是换个场景,比如一个电商订单被创建之后,状态机实例也随之创建。如果这个订单只能被创建该订单的副本进程执行,其他副本均无法处理该订单,这是不可接受的。

状态机的无状态化有两种实现,一种是『集中处理』的方式,一种是『用时构造』的方式。

"Centralized processing" is suitable for long-tail tasks, such as task engines, where a state machine exists for a long time. This method allows multiple replicas to select a specific replica to instantiate the FSM by selecting the master. When other replicas need to perform state transfer, the replicas holding the FSM are processed by RPC or events. The event-based implementation is optimal, after all, the state machine itself is an event-driven model. The advantage of this design is that it is simple and easy to understand. Of course, the disadvantage is also obvious. It relies on distributed message components.

"Time Construct" is suitable for short-term tasks, such as API requests for e-commerce orders. The state machine only needs to exist when it is used. This method is that the state machine no longer maintains the current state, but only stores the state flow graph. When the state flow is required, the state machine is reconstructed according to the current real-time state, and the state machine is destroyed after the state flow ends.

at last

The state machine itself is not a complex model, or even simple. However, the additional conditions extended by the state machine are cumbersome, such as state nesting, state parallelism, etc. Many open source projects cover more comprehensive scenarios, but also lead to their implementation being very complex and inefficient. The heavy implementation actually hinders the courage to use the state machine in small scenes. After all, using NASA's methodology to make a wooden bench is a weird thing to do. So it's better to try to give it a go and write an FSM that is close to your own scene. Advise you to be fooled, be fooled once.

no-qr.jpg

Guess you like

Origin juejin.im/post/7118405104369139719