2014/3/21

erlang currency errors

三個新概念:link、exit signal、system process。

process link

當 process A 依賴 process B,就需要對 B 的健康狀態保持注意。要達到此目的有兩種作法,使用 link BIF 或是使用 monitor。

當 A 呼叫 link(B) 時,兩個 process 就會互相監控,如果 A死了,B就會收到 exit signal,反之亦然。此機制可作用在單一 node ,也可以用在分散式的 erlang 系統中。

當一個行程收到 exit signal 時,如果沒有做特別的處理,接收者也會離開,但是行程可以攔截這些 exit signal,當一個行程有捕捉離開訊號,就被稱為「系統行程」。

如果連結到系統行程的行程離開,發出了 exit signal,系統行程不會跟著自動終結,反而會接收離開訊號,並作對應的處理。



on_exit 處理器

如果一個行程離開,想要作某些動作,可以寫一個 on_exit(Pid, Fun) 處理器

process_flag(trap_exit, true) 會將生成的行程轉成系統行程,參考 edoc:process_flag
當 trap_exit 設定為 true,process 收到 exit signal 後會轉成訊息 {'EXIT', From, Reason},然後就能被當成一般的訊息處理。
當 trap_exit 設定為 false,process 收到 normail 以外的 exit signal 時,process 就會死亡並將 exit signal 傳送給 linked processes。

on_exit(Pid, Fun) ->
    spawn(fun() ->
                % 會將這個新生成的行程轉成系統行程
                process_flag(trap_exit, true),
                % 將新的 process 連結到另一個 process: Pid
                link(Pid),
                % 行程 Pid 死亡時,此新的行程會收到 exit signal,Pid 是被連結的行程,剛剛才死亡
                receive
                    {'EXIT', Pid, Why} ->
                        Fun(Why)
                end
          end).

測試,因為 F 無法處理 atom,刻意傳送 atom 造成 Pid 死亡。

1> F = fun() ->
        receive
                X -> list_to_atom(X)
        end
        end.
#Fun<erl_eval.20.80484245>
2> Pid = spawn(F).
<0.34.0>
3> lib_misc:on_exit(Pid, fun(Why) -> io:format(" ~p died with:~p~n",[Pid, Why]) end).
<0.36.0>
4> Pid ! hello.
 <0.34.0> died with:{badarg,[{erlang,list_to_atom,[hello],[]}]} hello
5>
=ERROR REPORT==== 24-Jan-2014::11:21:50 ===
Error in process <0.34.0> with exit value: {badarg,[{erlang,list_to_atom,[hello]
,[]}]}

遠端處理 process 錯誤

因為某個行程的錯誤,會發送出來,並交給另一個行程處理,而 erlang 的行程可以運作在多個機器節點上,也就是說,不受機器的限制,而能遠端處理另一台機器節點上的 process 錯誤,這是 erlang 的容錯精神。

錯誤處理的細節

erlang 錯誤處理的三個底層的概念

  1. 連結 link
    link 定義了兩個行程間錯誤遞送的路徑,兩個連結在一起的行程,其中一個死掉時,另一個會收到離開訊息。目前連結到某個行程的所有行程的集合,稱為 link set

  2. 離開訊號 exit signal
    exit signal 是行程死亡前產生的,此訊號會廣播到 link set 的所有行程,此信號包含了一個參數,說明死亡的原因,該原因可能是 erlang 任何一種 term。

    可利用 exit(Reason) 設定原因,或是讓錯誤發生時自動設定。例如除以0的錯誤原因會是 badarith

    當一個行程成功地估算它的函數,正常把工作做完,就會以 normal 的原因死亡。

    行程 Pid1 可以藉由 exit(Pid2, X) 主動送出一個 exit signal 給 Pid2,但 Pid1 並沒有真的死亡。Pid2將會收到 {'EXIT, Pid1, X'} 的訊息,就像是 Pid1 死亡產生了離開的訊息一樣。因此 Pid1 可以假裝自己已經死亡了。

  3. 系統行程 system process
    當行程收到非正常的離開訊號,它也會死亡,除非它是系統行程。

    當 system process 收到來自 Pid 的離開訊號 Why,該訊號會被轉換成 {'EXIT', Pid, Why},並加入到系統行程的 mailbox 中。

    呼叫 process_flag(trap_exit, true) 會將一個正常的行程轉換成 system process

當離開訊號到達行程時,會根據訊號內容與是否為系統行程,而有不同的處理方式:

trap_exit exit signal 動作
true kill 死亡: 對 link set 廣播此 killed 離開訊號
true X 產生 {'EXIT', Pid, X} 到 mailbox
false normal 繼續: 不處理
false kill 死亡: 對 link set 廣播此 killed 離開訊號
false X 死亡: 對 link set 廣播此 X 離開訊號

kill 這個離開的原因,會產生一個無法被捕捉的離開訊號,收到 kill 的行程一定會死亡,不管是不是 system process。

OTP 裡面的 supervisor process 會用這個功能來殺死一些流氓 rogue 行程。

捕捉離開訊號的慣例

  1. 不在意自己建立的行程有沒有死掉
    單純地用 spawn 產生行程
     Pid = spawn(fun() -> ... end)
  2. 如果自己建立的行程死掉了,我也不想活了
    更明確的說法是,如果自己建立的行程,不正常離開時,我也不想活了
    建立行程的process不能被設定為要捕捉離開訊號,且要用 spawn_link 產生行程

     Pid = spawn_link(fun() -> ... end)
  3. 如果自己建立的行程死掉了,我想要處理錯誤
    使用 spawn_link 與 trap_exits

     process_flag(trap_exit, true),
     Pid = spawn_link(fun() -> ... end),
     ...
     loop(...),
    
     loop(State) ->
         receive
             {'EXIT', SomePid, Reason} ->
                 loop(State1);
         end.

捕捉離開訊號的進階資訊



這會開始三個process A,B,C。A連結到 B,B連結到C,A會捕捉 exit signal,且觀察 B 的離開。如果 Bool 為 true,B會捕捉離開訊號。當離開的原因為 M ,C 會死亡。

-module(edemo1).
-export([start/2]).

start(Bool, M) ->
    A = spawn(fun() -> a() end),
    B = spawn(fun() -> b(A, Bool) end),
    C = spawn(fun() -> c(B, M) end),

    % 這是為了要讓 C 死亡時,列印資料出來才加的
    % 正式的程式不應該使用 sleep 來達成同步
    sleep(1000),
    status(b, B),
    status(c, C).

a() ->      
    process_flag(trap_exit, true),
    wait(a).

b(A, Bool) ->
    process_flag(trap_exit, Bool),
    link(A),
    wait(b).

c(B, M) ->
    link(B),
    case M of
        {die, Reason} ->
            exit(Reason);
        {divide, N} ->
            1/N,
            wait(c);
        normal ->
            true
    end.

wait(Prog) ->
    receive
        Any ->
            io:format("Process ~p received ~p~n",[Prog, Any]),
            wait(Prog)
    end.

sleep(T) ->
    receive
    after T -> true
    end.

status(Name, Pid) ->        
    case erlang:is_process_alive(Pid) of
        true ->
            io:format("process ~p (~p) is alive~n", [Name, Pid]);
        false ->
            io:format("process ~p (~p) is dead~n", [Name,Pid])
    end.

測試1:B 是正常行程,C 執行了 exit(abc),B 會死亡,死亡後再廣播,A會收到訊息 {'EXIT',<0.34.0>,abc}。

1> edemo1:start(false, {die, abc}).
Process a received {'EXIT',<0.34.0>,abc}
process b (<0.34.0>) is dead
process c (<0.35.0>) is dead
ok

測試2:B 是正常行程,C 執行了 exit(normal),B 收到 C 正常離開的訊號,沒有死亡

2> edemo1:start(false, {die, normal}).
process b (<0.38.0>) is alive
process c (<0.39.0>) is dead
ok

測試3:B 是正常行程,C 產生了算術錯誤,B 會死亡,然後廣播,A 會收到訊號,並轉換為訊息 {badarith, ...

3> edemo1:start(false, {divide,0}).
Process a received {'EXIT',<0.42.0>,
                       {badarith,
                           [{edemo1,c,2,
                                [{file,
                                     "d:/projectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}}

=ERROR REPORT==== 24-Jan-2014::15:20:06 ===
Error in process <0.43.0> with exit value: {badarith,[{edemo1,c,2,[{file,"d:/projectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}

process b (<0.42.0>) is dead
process c (<0.43.0>) is dead
ok

測試4:B 是正常行程,C 執行了 exit(kill),B 會死亡,死亡後再廣播,A會收到訊息 {'EXIT',<0.34.0>,killed}。

4> edemo1:start(false, {die,kill}).
Process a received {'EXIT',<0.46.0>,killed}
process b (<0.46.0>) is dead
process c (<0.47.0>) is dead
ok

測試5:將 B 改成系統行程,重複上面四個測試,B 都會捕捉來自 C 的錯誤

5> edemo1:start(true, {die, abc}).
Process b received {'EXIT',<0.51.0>,abc}
process b (<0.50.0>) is alive
process c (<0.51.0>) is dead
ok
6> edemo1:start(true, {die, normal}).
Process b received {'EXIT',<0.55.0>,normal}
process b (<0.54.0>) is alive
process c (<0.55.0>) is dead
ok
7> edemo1:start(true, {divide,0}).
Process b received {'EXIT',<0.59.0>,
                       {badarith,
                           [{edemo1,c,2,
                                [{file,
                                     "d:/projectcase/erlang/erlangotp/src/edemo1.erl"}, {line,39}]}]}}

=ERROR REPORT==== 24-Jan-2014::15:25:23 ===
Error in process <0.59.0> with exit value: {badarith,[{edemo1,c,2,[{file,"d:/pro
jectcase/erlang/erlangotp/src/edemo1.erl"},{line,39}]}]}

process b (<0.58.0>) is alive
process c (<0.59.0>) is dead
ok
8> edemo1:start(true, {die,kill}).
Process b received {'EXIT',<0.63.0>,kill}
edemo1:start(true, {die,kill}).process b (<0.62.0>) is alive
process c (<0.63.0>) is dead
ok

如果把 c/2 改成下面這樣,以 exit(B,M) 將 B 停止,觀察 A、B、C 的狀況

c(B, M) ->
    process_flag(trap_exit, true),
    link(B),
    exit(B, M),
    wait(c).

測試

1> edemo2:start(false, abc).
Process c received {'EXIT',<0.34.0>,abc}
Process a received {'EXIT',<0.34.0>,abc}
process a (<0.33.0>) is alive
process b (<0.34.0>) is dead
process c (<0.35.0>) is alive
ok
2> edemo2:start(false, normal).
process a (<0.37.0>) is alive
process b (<0.38.0>) is alive
process c (<0.39.0>) is alive
ok
3> edemo2:start(false, kill).
Process c received {'EXIT',<0.42.0>,killed}
Process a received {'EXIT',<0.42.0>,killed}
process a (<0.41.0>) is alive
process b (<0.42.0>) is dead
process c (<0.43.0>) is alive
ok
4> edemo2:start(true,abc).
Process b received {'EXIT',<0.47.0>,abc}
process a (<0.45.0>) is alive
process b (<0.46.0>) is alive
process c (<0.47.0>) is alive
ok
5> edemo2:start(true,normal).
Process b received {'EXIT',<0.51.0>,normal}
process a (<0.49.0>) is alive
process b (<0.50.0>) is alive
process c (<0.51.0>) is alive
ok
6> edemo2:start(true,kill).
Process c received {'EXIT',<0.54.0>,killed}
Process a received {'EXIT',<0.54.0>,killed}
process a (<0.53.0>) is alive
process b (<0.54.0>) is dead
process c (<0.55.0>) is alive
ok

跟錯誤處理相關的函數

  1. @spec spawn_link(Fun) -> Pid
    建立行程時,同時建立兩個 process 的連結,這兩個是不能分割的步驟,所以並不等同於 spawn 之後再 link

  2. @spec process_flag(trap_exit, true)
    可將行程設定為系統行程

  3. @spec link(Pid) -> true
    建立對稱雙向的連結,在 A 上 link(B),跟在 B 上 link(A) 結果是一樣的,如果 Pid 不存在,會產生 noproc 例外,連結關係只會產生一次,不會重複兩次

  4. @spec unlink(Pid) -> true
    將 目前行程 與 Pid 行程 之間的所有連結移除

  5. @spec exit(Why) -> none()
    會造成目前的行程以 Why 原因終結,如果執行此敘述的時候,不在 catch 裡面,那麼此行程就會以 Why 為參數,廣播離開訊號到 link set

  6. @spec exit(Pid, Why) -> true
    送出離開訊號到 Pid ,原因是 Why

  7. @spec erlang:monitor(process, Item) -> MonitorRef
    設定一個監控器,Item 是行程的 Pid 或是 註冊名稱

erlang 的容錯機制

容錯必須至少要有兩台機器,一個失敗,另一個馬上接手。

erlang 的容錯機制就是,一個行程做事,另一個監控,出錯時馬上接手。在分散式erlang中,「做事的行程」和「監控的行程」可以放在不同的機器上。

此模式稱為 worker-supervisor

monitor

因為 link 是雙向的,如果要阻止其中一個死亡,就要設定為 system process,但有時不想這樣處理,就可以使用 monitor。

monitor 是非對稱連結,如果 process A 監控 process B,B 死亡會送 exit signal 給 A ,但 A 死亡,不會送訊號給 B。

keep-alive process

要讓一個 process 一直活著,如果死亡就馬上重新啟動。

keep_alive 會估算 Fun,啟動一個行程,並註冊名稱為 Name,同時會以 on_exit,產生新的 process 監控 Name 行程,如果接收到離開訊號,就會攔截並再次估算 Fun,而 Fun 的內容是再呼叫 keep_alive,重新產生一個 Name 的行程。

keep_alive(Name, Fun) ->
    register(Name, Pid = spawn(Fun)),
    on_exit(Pid, fun(_Why) -> keep_alive(Name, Fun) end).

on_exit(Pid, Fun) ->
    spawn(fun() ->
                process_flag(trap_exit, true),
                link(Pid),
                receive
                    {'EXIT', Pid, Why} ->
                        Fun(Why)
                end
          end).

register 跟 on_exit 之間,可能會因為 race condition 而造成問題。當程式裡面使用到 spawn、spawn_link、register 時,要注意會不會遇到 race condition。

OTP 內建了server、supervision tree 的功能,這些功能不會遇到 race condition,所以要盡量改使用 OTP library。

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World