论坛首页 综合技术论坛

修改gen_fsm源码构建更强壮的有限状态机

浏览 4337 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2009-12-09  
关于erlang提供的gen_fsm有限状态机模型,可以说是很好很强大,正是有了它,才使erlang编写复杂业务逻辑成为一件轻松愉快的事情

但是由于erlang的速错设计原则,我们的状态机进程可能在某些异常下直接退出,这点可能跟我们的需要有点背离

最常见的情况是,状态转移中的错误处理问题,如下代码:

%% ====================================================================
%% Interface Function
%% ====================================================================
join(UserId)->
	io:format("join UserId=~p~n",[UserId]),
	gen_fsm:sync_send_event(?MODULE, {join,UserId}).

start()->
	gen_fsm:send_event(?MODULE,start).

%% ====================================================================
%% Callback Function
%% ====================================================================
wait_player({join,UserId},_From, State) ->
	Count=State#state.count+1,
	if
		Count =:= 3 ->
			io:format("jump to next phrase -> start~n"),
			{reply,next_phrase,wait_start,State};
		true->
			NewState=State#state{count=Count},
			{reply,stand_still,wait_player,NewState}
	end.

wait_start(start,State)->
	 io:format("recv start event ~n"),
	 {next_state,wait_start,State}.

start_link()->
	gen_fsm:start_link({local,?MODULE}, ?MODULE, [] ,[]).

init([])->
	{ok, wait_player, #state{count=0}}.



我要实现的是一个简单的状态转移,

初始状态是 wait_player,收到 {join,UserId} 的消息后,进行简单业务逻辑处理判断,当有3个人加入进来,则转移到下一状态 wait_start,这时的StateName是 wait_start,

但是,假如这时客户端继续调用了join接口,也就是发送了{join,UserId}的消息给gen_fsm进程,那么gen_fsm实际回调的是 Mod:StateName,
也就是我们的wait_start({join,UserId},State)方法,结果就是undef 错误,然后整个gen_fsm进程退出,这就不是我想要的了,我希望我的gen_fsm进程能够不理会这种错误的消息或行为(只记录下错误调用日志即可),而不是直接退出

再来简单看一下gen_fsm处理流程(其实基本和gen_server差不多的),

gen_fsm进程启动后会进入一个main loop,等待处理mailbox里的消息,根据消息类型(接口处打上不同的label)分类处理,回调等

main_loop 代码
loop(Parent, Name, StateName, StateData, Mod, Time, Debug) ->
    Msg = receive
              Input ->
                    Input
          after Time ->
                  {'$gen_event', timeout}
          end,
    decode_msg(Msg,Parent, Name, StateName, StateData, Mod, Time, Debug, false).


跟踪到这里
handle_msg(Msg, Parent, Name, StateName, StateData, Mod, _Time) -> %No debug here
    From = from(Msg),
    case catch dispatch(Msg, Mod, StateName, StateData) of
        {next_state, NStateName, NStateData} ->
            loop(Parent, Name, NStateName, NStateData, Mod, infinity, []);
        {next_state, NStateName, NStateData, Time1} ->
            loop(Parent, Name, NStateName, NStateData, Mod, Time1, []);
        {reply, Reply, NStateName, NStateData} when From =/= undefined ->
            reply(From, Reply),
            loop(Parent, Name, NStateName, NStateData, Mod, infinity, []);
        {reply, Reply, NStateName, NStateData, Time1} when From =/= undefined ->
            reply(From, Reply),
            loop(Parent, Name, NStateName, NStateData, Mod, Time1, []);
        {stop, Reason, NStateData} ->
            terminate(Reason, Name, Msg, Mod, StateName, NStateData, []);
        {stop, Reason, Reply, NStateData} when From =/= undefined ->
            {'EXIT', R} = (catch terminate(Reason, Name, Msg, Mod,
                                           StateName, NStateData, [])),
            reply(From, Reply),
            exit(R);
        {'EXIT', What} ->
                %%{Reason,StackInfo}=What,
                %%loop(Parent, Name, StateName, StateData, Mod, infinity, []);
                terminate(What, Name, Msg, Mod, StateName, StateData, []);
        Reply ->
            terminate({bad_return_value, Reply},
                      Name, Msg, Mod, StateName, StateData, [])
    end.


dispatch函数实际就是根据消息label,调用不同回调接口的地方,然后可以看到,根据回调接口的返回,来进行不同处理,
异步的消息直接返回{next_state,...},
同步则是{reply,...}

其实最重要的是告知gen_fsm的下一状态名,也就是NStateName

实际,消息回调发生问题,或者错误的状态消息,则会返回{'EXIT',What}这个消息,gen_fsm源码是一律 terminate

我添加了2行注释,可以不直接退出
%%{Reason,StackInfo}=What,
%%loop(Parent, Name, StateName, StateData, Mod, infinity, []);


实际我们要做的就是在这个基础上完善一下就可以了,比如可以根据Reason判断那种情况该terminate,那种情况可以忽略,继续loop进入主循环处理下一消息

另外一个注意的问题是,尽管加了这2行代码后,gen_fsm不会立即由于undef function而直接退出,但是由于我们的join接口是sync同步调用,实际在gen.erl里会等待gen_fsm进程处理完该消息的返回,

而实际由于是一个错误的调用,所以我们可以返回一个自定义的错误,或者干脆不理会这个返回,如果不理会,则gen.erl由于等待消息超时一样会crash,实际crash的是调用者进程,所以要做好catch

这样,通过添加2行代码到gen_fsm就可以使我们的gen_fsm进程更稳定了

时间仓促,没有多考虑,应该会有更好的办法










   发表时间:2009-12-09  
我只看了一半,你是想在某个状态下收到意料之外的消息不崩溃而已,何必搞得那么麻烦
wait_start(start, State) -> play_game(State);
wait_start(OtherMsg, State) -> {reply, you_are_not_welcome, wait_start, State}.
0 请登录后投票
   发表时间:2009-12-09   最后修改:2009-12-09
darkdestiny 写道
我只看了一半,你是想在某个状态下收到意料之外的消息不崩溃而已,何必搞得那么麻烦
wait_start(start, State) -> play_game(State);
wait_start(OtherMsg, State) -> {reply, you_are_not_welcome, wait_start, State}.


对呀,这样处理肯定是可以的,应该算标准处理方法

只是我的fsm里的方法太多了,同步,异步,all_state_event等等一大堆,每个都写一个安全的cause也有点麻烦而已

改下gen_fsm 2行代码没觉得很麻烦呀
0 请登录后投票
   发表时间:2009-12-09  
改源码总是最后的选择才对,以后erlang升级你还得改一次,不推荐这样的方式,哪怕麻烦一点。看了下,gen_fsm确实没有提供error handle的hook方式。
0 请登录后投票
   发表时间:2009-12-09  
dennis_zane 写道
改源码总是最后的选择才对,以后erlang升级你还得改一次,不推荐这样的方式,哪怕麻烦一点。看了下,gen_fsm确实没有提供error handle的hook方式。


恩,后来想了一下,是有点脑袋短路,还是不改源码的好,上面提供的方式是最标准的解决办法
0 请登录后投票
论坛首页 综合技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics