顯示具有 otp 標籤的文章。 顯示所有文章
顯示具有 otp 標籤的文章。 顯示所有文章

2014/07/21

erlang - otp application release tool

OTP application 最後發布時,需要一個工具幫我們分析 application 的相關 dependency libraries,發布正確的 OTP application。

目前有看到三個 release 工具: reltool, rebar, relx,雖然 reltool 是 OTP 官方提供的封裝工具,前面的部份先簡述 reltool 與 relx,relx 是因為 survey 了 cowboy 的關係,而去了解,功能跟 rebar 類似,但提供了更高階的封裝簡化工具,這個工具是 erlware 這個組織開發提供的。rebar 提供了比 reltool 高階的封裝方式,我們就以 rebar 為主,因為 rebar 的文件說明比 relx 還清楚。

reltool

reltool提供 GUI 與 CLI 兩種界面

reltool user guide
reltool doc
reltool使用指南
reltool使用入門

erlang.mk and relx

建構 OTP releases 一直以來都是個很麻煩的工作,除了 reltool, rebar 之外 cowboy 作者提供了另一個建立 application 的方式。

建立 OTP release 需要兩個步驟:

  1. compile source
    erlang.mk 用來處理第一個步驟,erlang.mk 是 GNU Makefile 的一個 include file,它可以用來 build project, 取得 building dependencies,產生文件,做 dialyzer 靜態分析。
  2. create a release
    relx 是個 release creation tool,它是一個執行檔。用來封裝OTP application,裡面包含了Erlang runtime, a boot script to start the node and all its applications, configuration files

最簡單的 Makefile,就只需要一個 PORJECT name 並 include erlang.mk

PROJECT = my_project

include erlang.mk

DEPS 定義本專案相關的 dependencies,條列出來後,在下面以 dep_cowboy 的方式定義該 library 的 repository URL 以及 commit number, tag 或 branch。 .PHONY 表示有兩個 build target,預設是 release。 release 最後會使用 relx 將 project 建構在 rel 目錄中。

PROJECT = ninenines

DEPS = cowboy erlydtl
dep_cowboy = https://github.com/extend/cowboy.git 0.8.5
dep_erlydtl = https://github.com/evanmiller/erlydtl.git 4d0dc8fb

.PHONY: release clean-release

release: clean-release all projects
    relx -o rel/$(PROJECT)

clean-release: clean-projects
    rm -rf rel/$(PROJECT)

include erlang.mk

這是 relx.config 檔,第一行定義 release name: ninenines,版本號碼1,裡面包含一個 application: ninenines,extended_start_script 告訴 relx 要建立一個可啟動 application 的 script,下一行 sys.config 代表可指定 erlang vm 的起始參數,

{release, {ninenines, "1"}, [ninenines]}.

{extended_start_script, true}.
{sys_config, "rel/sys.config"}.

{overlay, [
    {mkdir, "log"},
    {copy, "rel/vm.args",
        "releases/\{\{release_name\}\}-\{\{release_version\}\}/vm.args"}
]}.

參考文件 Build Erlang releases with erlang.mk and relx

rebar

編譯與安裝

  1. 下載 rebar source code
    git clone https://github.com/basho/rebar.git

  2. 編譯
    cd rebar
    ./bootstrap

    產生出 rebar 執行檔後,可以將 rebar 複製到任何地方,都可以使用。我們可以將 rebar 複製到 /usr/local/bin 或是 ~/bin 目錄,放在隨時可使用到的 PATH 裡面,就完成 rebar 的安裝了。

  3. 直接下載 rebar
    因為 rebar 是完全由 erlang 撰寫的,而且有整合成一個獨立的 escript,我們也可以直接下載編譯好的 rebar http://cloud.github.com/downloads/basho/rebar/rebar ,然後就能使用了。

  4. windows
    把在 linux 環境編譯出來的 rebar 複製到 windows 環境,把 rebar.cmd 與 rebar 放在 windows 的 PATH 路徑中可以存取到的地方,就可以直接使用 rebar 了。

測試專案

  1. mkdir myapp
    建立專案目錄

  2. rebar create-app appid=myapp
    產生專案的檔案骨架
    完成後會看到一個 src 目錄,裡面有三個檔案:myapp.app.src、myapp_app.erl、myapp_sup.erl

  3. rebar compile
    編譯 myall project,完成後會產生 ebin 目錄,也會得到一個 OTP 專案檔 myapp.app

  4. rebar clean
    清除編譯結果

命令列的參數

在開發過程中,最常用的功能有

  1. 編譯
  2. 單元測試和覆蓋分析
  3. 靜態分析(通過Dialyzer和Xref)
  4. 生成文檔
  5. 依賴管理

Rebar commands 提供了指令與參數列表

常見指令 description
compile 編譯
eunit 執行 eunit
doc 使用 edoc 產生文件
clean 清除所有生成的資料
一般指令 description
check_deps 檢查 rebar.config 的 dep libs
create 建立專案,必須要給予 template 的設定
create-app 根據 simpleapp.template 建立 app 專案
create-lib 根據 simplelib.template 建立 OTP library
create-node 根據 simplenode.template 建立一個 prototypical OTP embedded system
ct 執行 common_test suites
delete-deps 刪除依賴的專案原始碼
escriptize 以 ebin 目錄的 beam files 產生 escript 的執行檔
generate 使用 reltool 建立一個embedded system
generate-upgrade 產生 upgrade package
get-deps 根據 rebar.config 取得依賴的專案原始碼
list-deps 依賴的專案列表
update-deps 更新依賴的專案原始碼
xref 使用 xref 分析依賴

compile 支援的 source code 格式

source file destination file description
src/*.erl ebin/*.beam erlang source
src/*.app.src ebin/*.app otp application
c_src/*.c priv/.so port driver的c語言源代碼或者NIF共享鏈接庫
mibs/*.mib priv/mibs/*.bin SNMP 的 MIB 檔案
src/*.xrl src/*.erl leex 產生的檔案
src/*.yrl src/*.erl Yecc 產生的檔案
asn1/*.asn1 src/*.erl ASN-1文件
templates/*.dtl ebin/*_dtl.beam ErlyDTL模板文件 (需要額外安裝 ErlyDTL)
src/*.lfe ebin/*.beam LFE source code (需要額外安裝LFE)
src/*.peg ebin/*.beam Neotoma PEG 語法 source code (需要額外安裝Neotoma)
src/*.proto ebin/_pb.beam, include/_pb.hrl Protocol Buffers 參數(需要額外安裝protobuffs)

rebar.config 設定的選項

命令 設定的選項 description
compile erl_first_files 需要提前編譯的erlang源文件(例如behavior模塊)
compile erl_opts 編譯器支援的其他設定,請參閱文件
compile mib_first_files 需要提前編譯的mib文件列表 (例如, mib 文件中import部分的引用的RFC文件
compile src_dirs 列出其他包含erlang源文件的目錄
compile erlydtl_opts erlydtl 更多的支援的設定 ErlyDTL Templates
clean clean_files 需要在clean步驟刪除的文件列表,列出那些需要clean指令刪除的其他模塊的文件
doc edoc_opts edoc 支援的指令
eunit eunit_opts eunit支援的指令
eunit cover_enabled 開啟erlang的覆蓋率分析
eunit eunit_compile_opts Eunit編譯時用到的其他的選項
analyze dialyzer_opts 指定Dialyzer PLT 文件
build_plt dialyzer_opts 指定Dialyzer PLT 文件
check_plt dialyzer_opts 指定 Dialyzer PLT 文件
get-deps, delete-deps base_dir 為deps_dir 指定一個候選的目錄
get-deps, delete-deps deps_dir 設定存儲依賴檔的資料夾
get-deps, delete-deps deps 依賴的列表
generate target_dir 目標資料夾
generate overlay_vars Overlay variables file
xref xref_warnings 打開xref的警告
xref xref_checks Xref模塊中analyze/3支持的選項

目錄結構

rebar 遵循 OTP 的建議,包含了下列的資料夾:

  1. src
  2. ebin
  3. priv
  4. include
  5. test: eunit test source code
  6. c_src: C 語言寫的 Port Driver

Testing

rebar 支援 eunit 與 common test 兩種 testing frameworks。

首先撰寫一些 eunit 測試的程式碼,在 apps/myapp/src/myapp_app.erl 的 -export 跟 程式的最後面,增加兩段 -ifdef(TEST) 到 -endif 的測試區塊。

-module(myapp_app).
-behaviour(application).

%% Application callbacks
-export([start/2, stop/1]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

%% ===================================================================
%% Application callbacks
%% ===================================================================

start(_StartType, _StartArgs) ->
    myapp_sup:start_link().

stop(_State) ->
    ok.

-ifdef(TEST).

simple_test() ->
    ok = application:start(myapp),
    ?assertNot(undefined == whereis(myapp_sup)).

-endif.

因為直接執行 rebar eunit,連帶會對 deps 進行 eunit 的測試,所以增加 skip_deps 參數,只針對我們自己寫的程式進行測試。

rebar compile eunit skip_deps=true

如果要檢查 code coverage 的統計資料,我們必須在 rebar.config 增加以下設定

{cover_enabled, true}.

再執行一次 rebar compile eunit skip_deps=true ,就可以看到 code coverage analysis 結果寫在 apps/myapp/.eunit/index.html 檔案裡面。

其它

有關 deps, 建構 release package 以及升級的問題,就放在下一篇文章裡面說明。

2014/05/12

erlang otp gen_fsm

前面提到了 otp 中,最常被提到的 gen_server gen_event supervisor,但其實還有另一個 gen_fsm behavior,fsm 就是 finite state machine 有限狀態機的縮寫。

可以在 http://erldocs.com/ 搜尋並取得 gen_fsm 的 API 文件。

FSM finite state machine

finite state machine 有限狀態機,是由一組狀態、一個起始狀態、 輸入、將輸入與現在狀態轉換為下一個狀態的轉換函數所組成。

有許多東西可以用有限狀態來表達,例如紅綠燈、自動販賣機等等,以紅綠燈為例,首先,我們要知道只會有紅、黃、綠三種燈號狀態,這就是有限狀態的條件,絕對不會出現第四種狀態。

在不同狀態下,再搭配不同的輸入條件,就會讓機器從狀態1變化到狀態2。以紅綠燈來說,就是紅燈出現時,在20秒後,就自動變化到綠燈。但狀態變化也是有限制的,因為綠燈 30秒後,會變成黃燈,然後再經過 5秒變紅燈,絕對不會由綠燈直接跳到紅燈,變成黃燈,就只會再變紅燈,絕對不會突然變成綠燈。

state diagram, state transition table

要描述一個 FSM,可以使用 state diagram 或是 state transition table。State Transition Table wiki 裡面,有提到 state transition table 有一維或二維的表示方式,二維表示法還可以選擇將狀態跟改變條件寫在列或table cell 的位置。

以分析的直覺性來看,圖形的方式比較適合人眼觀看與分析,而table 的方式,看起來就比較接近撰寫程式碼的演算法。

目前\下一個 狀態 Red Green Yellow
Red X 20s X
Green X X 30s
Yellow 5s X X

code_lock 門鎖範例

fsm 官方文件 是以一個門鎖的例子,來說明如何使用 gen_gsm。

門鎖的狀態變化規則如下:

  1. 初始啟動有限狀態機時會設置鎖的密碼,然後進入 locked 狀態等待用戶按鍵輸入密碼
  2. 使用者呼叫 code_lock:button/1 輸入密碼,在輸入的過程中會記錄當前為止鍵入的資料。如果密碼錯誤或者不完整,保持 locked 狀態。
  3. 如果密碼正確,那麼就進入 open 狀態,並執行相關操作do_unlock
  4. 當 open 狀態持續一段時間後,自動執行相關操作 do_lock,1並進入 locked 狀態

啟動 fsm

gen_fsm 是由呼叫 gen_fsm:start_link({local, code_lock}, code_lock, Code, []) 開始,Code 是初始的門鎖密碼,他會 spawns and links to a new process。

handle_event 與 handle_sync_event 的差異

handle_event 是這兩個 非同步 發送 event 的事件處理函式:

send_event(FsmRef, Event) -> ok
send_all_state_event(FsmRef, Event) -> ok

handle_sync_event 是這兩個 同步 發送 event 的事件處理函式:

sync_send_event(FsmRef, Event, Timeout) -> Reply
sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply

send_event 跟 send_all_state_event 的差異

send_all_state_event 就是所有狀態下該事件的處理方式都是一樣的,而send_event 則是在當前狀態下處理該事件。

拿銀行ATM機來舉例:取消操作是在任何狀態下都可以進程的,而且處理是一樣的,都是回到初始登錄界面,而提款操作則必須要在登錄驗證成功狀態下才能進行,在該狀態下的處理和其他狀態下的處理是不同的。

如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出,無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。

換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束。

如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束

狀態中,處理事件的函式

gen_fsm的狀態是用函數表示的

Module:StateName(Event, From, StateData) -> Result

gen_fsm 的狀態處理函數名稱是此 fsm 的狀態,第一個參數 Event 是 send_event 發送的 event 內容,後面的 StateData 是 State 變數資料。

從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,NextStateName就是下一個狀態函數的名字,返回值統一這樣:

{next_state,NextStateName,NewStateData}
{next_state,NextStateName,NewStateData,Timeout}
{next_state,NextStateName,NewStateData,hibernate}
{stop,Reason,NewStateData}

程式碼

-module(code_lock).
-behaviour(gen_fsm).

-export([start_link/1]).
-export([button/1]).

-export([init/1, locked/2, open/2]).
-export([code_change/4, handle_event/3, handle_info/3, handle_sync_event/4, terminate/3]).

%% client functions
% gen_fsm 是由呼叫 code_lock:start_link(Code) 開始,Code 是初始的門鎖密碼
% 他會 spawns and links to a new process
-spec(start_link(Code::string()) -> {ok,pid()} | ignore | {error,term()}).
start_link(Code) ->
    % {local, code_lock} 代表註冊在 local 機器,名稱為 code_lock
    % 如果寫成 {global, code_lock} 就代表是呼叫了 global:register_name/2
    % 第二個參數是所有 call back function 的 module name: code_lock
    % 第三個參數是 a list of digits,會傳送到 init
    gen_fsm:start_link({local, code_lock}, code_lock, Code, []).

-spec(button(Digit::string()) -> ok).
button(Digit) ->
    gen_fsm:send_event(code_lock, {button, Digit}).

%% gen_fsm的狀態是由函數表示的
%% Module:StateName(Event, From, StateData) -> Result
%% gen_fsm 的狀態處理函數,前面的 locked, open 是此 fsm 的兩個狀態
%% 第一個參數 {button, Digit} 是 send_event 發送的 event 內容
%% 後面的 {SoFar, Code} 是 State 變數資料
%% 
%% 從一個狀態跳到下一個狀態是通過狀態函數的返回值控制的,返回值統一這樣:
%% {next_state,NextStateName,NewStateData}
%% {next_state,NextStateName,NewStateData,Timeout}
%% {next_state,NextStateName,NewStateData,hibernate}
%% {stop,Reason,NewStateData}
%% NextStateName就是下一個狀態函數的名字 
locked({button, Digit}, {SoFar, Code}) ->
    io:format("buttion: ~p, So far: ~p, Code: ~p~n", [Digit, SoFar, Code]),
    InputDigits = lists:append(SoFar, Digit),
    case InputDigits of
        Code ->
            do_unlock(),
            %% 發送事件
            {next_state, open, {[], Code}, 10000};
        Incomplete when length(Incomplete)<length(Code) ->
            {next_state, locked, {Incomplete, Code}, 5000};
        Wrong ->
            io:format("wrong passwd: ~p~n", [Wrong]),
            {next_state, locked, {[], Code}}
    end;

%% 在 locked 狀態,timeout 時,會將目前 SoFar 中記錄的輸入密碼資料清空
locked(timeout, {_SoFar, Code}) ->
    io:format("timout when waiting button inputting, clean the input, button again plz~n"),
    {next_state, locked, {[], Code}}.

%% 這個是在 open 狀態,timeout 時,會自動呼叫此事件處理函式
open(timeout, State) ->
    do_lock(),
    {next_state, locked, State}.

%%%
%% 啟動時會被呼叫的函數,回傳值裡面的 State 會在其他函數中使用
init(LockCode) ->
    io:format("init: ~p~n", [LockCode]),
    %% 初始化 fsm 時,將 fsm 狀態設定為 locked,State 內容設定為 {[], LockCode}
    {ok, locked, {[], LockCode}}.

%% 這是 send_event(FsmRef, Event) -> ok, send_all_state_event(FsmRef, Event) -> ok
%% 這兩個 非同步 發送 event 的事件處理函式
%% 當呼叫 gen_fsm:send_event 時,不需要等待 server 回應,就可繼續處理別的事情
%% 
%% send_event 跟 send_all_state_event 的差異是
%% 如果FSM在當前狀態收到的事件是無法處理的,則整個狀態機進程會被迫退出
%% 無論FSM進程當前處於何種狀態,當gen_fsm:send_all_state_event被調用時,fsm 都會呼叫 handle_event callback函數處理。
%% 換句話說,用 send_event 發送事件時,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,此 fsm process會直接結束
%% 如果用 send_all_state_event,如果 handle_event 在目前 fsm 狀態下無法處理該事件時,不會造成 fsm process 直接結束
handle_event(Event, StateName, Data) ->
    io:format("handle_event... ~n"),
    unexpected(Event, StateName),
    {next_state, StateName, Data}.

%% 這是 sync_send_event(FsmRef, Event, Timeout) -> Reply, sync_send_all_state_event(FsmRef, Event, Timeout) -> Reply
%% 這兩個 同步 發送 event 的事件處理函式
%% 因為同步的關係,所以發送 event 可設定 Timeout,以避免 fsm 太久沒有回應
handle_sync_event(Event, From, StateName, Data) ->
    io:format("handle_sync_event, for process: ~p... ~n", [From]),
    unexpected(Event, StateName),
    {next_state, StateName, Data}.

%% handle_info:與gen_server類似,處理所有直接發給 FSM process 的訊息
handle_info(Info, StateName, Data) ->
    io:format("handle_info...~n"),
    unexpected(Info, StateName),
    {next_state, StateName, Data}.

terminate(normal, _StateName, _Data) ->
    io:format("terminate...~n"),
    ok.

code_change(_OldVsn, StateName, Data, _Extra) ->
    {ok, StateName, Data}.

%% Unexpected allows to log unexpected messages
unexpected(Msg, State) ->
    io:format("~p RECEIVED UNKNOWN EVENT: ~p, while FSM process in state: ~p~n",
              [self(), Msg, State]).

%% actions 
do_unlock() ->
    io:format("passwd is right, open the DOOR.~n").

do_lock() ->
    io:format("over, close the DOOR.~n").

測試

1> code_lock:start_link("test123").
init: "test123"
{ok,<0.33.0>}

%% 直接發送訊息,是由 handle_info 處理
2> pid(0,33,0)! hello.
handle_info...
<0.33.0> RECEIVED UNKNOWN EVENT: hello, while FSM process in state: locked
hello

3> code_lock:button("ab").
buttion: "ab", So far: [], Code: "test123"
ok

%% 輸入密碼逾時,SoFar 會被清空
4> timout when waiting button inputting, clean the input, button again plz
4> code_lock:button("test").
buttion: "test", So far: [], Code: "test123"
ok

%% 密碼吻合,開門了
5> code_lock:button("123").
buttion: "123", So far: "test", Code: "test123"
passwd is right, open the DOOR.
ok

%% 自動關門
6> close the DOOR.

%% 以 send_all_state_event 發送無法處理的事件 fooo
6> gen_fsm:send_all_state_event(code_lock, fooo).
handle_event...
<0.33.0> RECEIVED UNKNOWN EVENT: fooo, while FSM process in state: locked
ok

%% 以 send_event 發送無法處理的事件 fooo,會造成 fsm process 當機
7> gen_fsm:send_event(code_lock, fooo).

=ERROR REPORT==== 5-Mar-2014::16:00:49 ===
** State machine code_lock terminating
** Last event in was fooo
** When State == locked
**      Data  == {[],"test123"}
** Reason for termination =
** {function_clause,
       [{code_lock,terminate,
            [{function_clause,
                 [{code_lock,locked,
                      [fooo,{[],"test123"}],
                      [{file,
                           "d:/projectcase/erlang/erlangotp/src/code_lock.erl"},

                       {line,37}]},
                  {gen_fsm,handle_msg,7,[{file,"gen_fsm.erl"},{line,505}]},
                  {proc_lib,init_p_do_apply,3,
                      [{file,"proc_lib.erl"},{line,239}]}]},
             locked,
             {[],"test123"}],
            [{file,"d:/projectcase/erlang/erlangotp/src/code_lock.erl"},
             {line,96}]},
        {gen_fsm,terminate,7,[{file,"gen_fsm.erl"},{line,597}]},
        {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,239}]}]}
** exception exit: function_clause
     in function  code_lock:terminate/3
        called as code_lock:terminate({function_clause,
                                       [{code_lock,locked,
                                         [fooo,{[],"test123"}],
                                         [{file,
                                           "d:/projectcase/erlang/erlangotp/src/
code_lock.erl"},
                                          {line,37}]},
                                        {gen_fsm,handle_msg,7,
                                         [{file,"gen_fsm.erl"},{line,505}]},
                                        {proc_lib,init_p_do_apply,3,
                                         [{file,"proc_lib.erl"},{line,239}]}]},
                                      locked,
                                      {[],"test123"})
     in call from gen_fsm:terminate/7 (gen_fsm.erl, line 597)
     in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 239)

%% fsm process 確實已經被終止了
8> erlang:is_process_alive(pid(0,33,0)).
false
9>

結語

一般在討論 otp 時,只會談到 gen_server、gen_event 與 supervisor,常常會漏掉 gen_fsm,fsm 在可以根據變化的條件來決定並限制某個物件的狀態,這可以幫助我們在預期的情況下,得到物件的狀態變化,而不會因為缺少了狀態變化的判斷,直接設定物件的狀態值,導致該物件的狀態進入了意料之外的情況。

2014/05/05

erlang otp

範例內容包含了建立伺服器、監督伺服器、登錄錯誤到日誌、偵測警報這些功能,最後再將整個程式碼包裝成一個獨立的OTP應用。

建立兩個 otp server,一個用來產生質數,一個計算面積,系統錯誤會造成當機,需要一個偵測機制來重新啟動 server,也就是 supervision tree,這可以產生一個監督者,負責觀察 server,當server crash就重新啟動。

OTP 錯誤登錄器可紀錄所有錯誤,產生錯誤報告,另外因為計算很大的質數可能會造成 CPU 過熱,為了避免這個問題,使用 OTP 事件處理框架,來產生警報,以便配置強力風扇。

  1. 說明通用事件處理器的機制
  2. 說明錯誤登錄器的機制
  3. 加入警報管理
  4. 寫出兩個應用伺服器
  5. 實作監督樹,並將伺服器加入到監督樹中
  6. 把整個程式包裝成一個 OTP 應用

通用事件處理器

當程式中,有值得注意的事情 event 發生了,就送出事件訊息到註冊行程,只需要送出訊息通知有事情發生了,不在意送出訊息後會發生什麼事情。

RegProcName ! {event, E}

第一個通用事件處理器

-module(event_handler).
-export([make/1, add_handler/2, event/2]).

%% make a new event handler called Name
%% the handler function is no_op -- so we do nothing with the event
%% 產生一個 event handler,但收到 event 後,不做任何事情
make(Name) ->
    register(Name, spawn(fun() -> my_handler(fun no_op/1) end)).

%% 發送 event及處理該 event 的函數 Fun
add_handler(Name, Fun) -> Name ! {add, Fun}.

%% generate an event
event(Name, X) -> Name ! {event, X}.

my_handler(Fun) ->
    receive
        {add, Fun1} ->
            my_handler(Fun1);
        {event, Any} ->
            % 一開始是使用 no_op(_),後來可透過 add_handler 動態修改事件處理函數
            (catch Fun(Any)),
            my_handler(Fun)
    end.

no_op(_) -> void.

測試

1> event_handler:make(errors).
true
2> event_handler:event(errors, hi).
{event,hi}

發送 event 及 處理 event 的函數

想讓 event handler 做些事情,必須寫一個 callback module,並安裝在事件處理器中。

-module(motor_controller).

-export([add_event_handler/0]).

add_event_handler() ->
    event_handler:add_handler(errors, fun controller/1).

controller(too_hot) ->
    io:format("Turn off the motor~n");
controller(X) ->
    io:format("~w ignored event: ~p~n",[?MODULE, X]).

測試

3> motor_controller:add_event_handler().
{add,#Fun<motor_controller.0.125151531>}
4> event_handler:event(errors, cool).
motor_controller ignored event: cool
{event,cool}
5> event_handler:event(errors, too_hot).
Turn off the motor
{event,too_hot}

very late binding

如果寫一個函數把 event_handler:event 封裝起來

too_hot() ->
    event_handler:event(errors, too_hot).

如果出錯就呼叫 too_hot(),這會讓事件處理的模式固定。

erlang 處理事件的方式,讓我們能解除「事件發生」和「事件處理」之間的關係,我們可以在任何時候改變處理的方式,只要發送新的事件處理函數給事件處理器,就可以動態調整事件處裡的方法。也可以利用這個機制,在不停止server的狀況下,動態升級程式。

錯誤記錄器 error logger

OTP 內建可客製化的 event logger,可從三個部份討論(1) 函數呼叫,如何登錄錯誤到日誌 (2) configuration 設定錯誤登錄資料儲存的方式 (3) 錯誤的分析報告

登錄錯誤到日誌

@spec error_logger:error_msg(String) -> ok
    發送錯誤訊息到 error logger

@spec error_logger:error_msg(Format, Data) -> ok
    發送錯誤訊息到 error logger,參數跟 io:format(Format, Data) 一樣

@spec error_logger:error_report(Report) -> ok
    發送錯誤報告到 error logger
    @type Report = [{Tag, Data} | term() | string() | term()]
    @type Tag = term()
    @type Data = term()

configuration

預設可以在 erlang shell 中看到所有錯誤,我們可以另外將錯誤寫到單一文字檔,或是製作一天一個檔案的日誌檔,也可以製作轉動的環狀日誌檔。

標準的錯誤登錄器

啟動 erl 時,加入 boot 參數,預設值就是 start_clean

這是「適合編程」的環境,只有一種簡單的錯誤日誌形式

> erl -boot start_clean

這是「適合執行產品系統」的環境,SASL: System Architecture Support Libraries 會負責錯誤登錄、負載保護等等

> erl -boot start_sasl

日誌檔案的組態設定,最好放在 configuration file 裡面。

如果啟用 SASL,但又沒有提供 config file,就會發生錯誤。

D:\projectcase\erlang\erlangotp\ebin>erl -boot start_sasl

=PROGRESS REPORT==== 20-Feb-2014::11:40:27 ===
          supervisor: {local,sasl_safe_sup}
             started: [{pid,<0.34.0>},
                       {name,alarm_handler},
                       {mfargs,{alarm_handler,start_link,[]}},
                       {restart_type,permanent},
                       {shutdown,2000},
                       {child_type,worker}]
.....

=PROGRESS REPORT==== 20-Feb-2014::11:40:27 ===
         application: sasl
          started_at: nonode@nohost
Eshell V5.10.4  (abort with ^G)
1>
控制日誌的內容

錯誤登錄會自動產生三種報告

  1. 監督者報告
    當 OTP 監督者開始或停止監督一個 process 時 所發出的
  2. 進度報告
    當 OTP 監督者啟動或停止時 所發出的
  3. 當機報告
    當一個 OTP 行為終止的理由不正常 或 關閉時 所發出的

另外可以主動呼叫 error_handler module 的 function,來產生幾種等級的日誌報告,error_type 的類型有 error | error_report | info_msg | info_report | warning_msg | warning_report | crash_report | supervisor_report | progress,在分析日誌時,就能使用標籤來幫助我們決定那個日誌項目需要被檢視。

範例1

這會得到錯誤報告,而不是進度或其他報告

%% elog1.config
%% no tty 
[{sasl, [
     {sasl_error_logger, false}
    ]}].

測試

>erl -boot start_sasl -config elog1
Eshell V5.10.4  (abort with ^G)
1> error_logger:error_msg("I'm Error\n").
=ERROR REPORT==== 20-Feb-2014::11:54:08 ===
I'm Error
ok
範例2

會把 shell 所有東西複製到檔案中

%% elog2.config
%% single text file - minimal tty
[{sasl, [
     %% All reports go to this file
     {sasl_error_logger, {file, "d:/temp/error_logs/logfile"}}
    ]}].

測試

>erl -boot start_sasl -config elog2
Eshell V5.10.4  (abort with ^G)
1> error_logger:error_msg("I'm Error\n").

=ERROR REPORT==== 20-Feb-2014::12:01:01 ===
I'm Error
ok

如果 d:/temp/error_logs 目錄不存在,就不會產生檔案 logfile,檔案的內容如下:

=PROGRESS REPORT==== 20-Feb-2014::12:00:59 ===
          supervisor: {local,sasl_safe_sup}
             started: [{pid,<0.35.0>},
                       {name,alarm_handler},
                       {mfargs,{alarm_handler,start_link,[]}},
                       {restart_type,permanent},
                       {shutdown,2000},
                       {child_type,worker}]
....
範例3

所有錯誤日誌都會進入 rotating log 裡面

%% rotating log and minimal tty
[{sasl, [
     {sasl_error_logger, false},    
     %% define the parameters of the rotating log
     %% the log file directory 這裡是填目錄
     {error_logger_mf_dir,"d:/temp/error_logs"},    
         %% # bytes per logfile
     {error_logger_mf_maxbytes,10485760}, % 10 MB
         %% maximum number of logfiles
     {error_logger_mf_maxfiles, 10}
    ]}].
> erl -boot start_sasl -config elog3
範例4

在正式環境,增加 {errlog_type, error} 只記錄 error

%% rotating log and errors
[{sasl, [     
     %% minimise shell error logging
     {sasl_error_logger, false},
         %% only report errors
     {errlog_type, error},
     %% define the parameters of the rotating log
     %% the log file directory
     {error_logger_mf_dir,"d:/temp/error_logs"},    
         %% # bytes per logfile
     {error_logger_mf_maxbytes,10485760}, % 10 MB
         %% maximum number of
     {error_logger_mf_maxfiles, 10}
    ]}].

分析錯誤

rb 模組負責分析錯誤。

rb:help(). 可取得 help
rb:start([{max, 20}]). 啟動報告瀏覽器,只讀取 20 筆記錄
rb:show(1). 顯示 1 號錯誤
rb:list(). 錯誤列表
rb:grep(RegExp). 找出所有符合 RegExp 正規表示式 的報告

>erl -boot start_sasl -config elog3
1> rb:start([{max, 20}]).
rb: reading report...done.
{ok,<0.42.0>}
2> rb:show(1).

PROGRESS REPORT  <0.7.0>                                    2014-02-20 15:57:19
===============================================================================
application                                                                sasl
started_at                                                        nonode@nohost

ok

警報管理

警報處理器是 OTP gen_event 行為的 callback module。

-module(my_alarm_handler).
-behaviour(gen_event).

%% gen_event callbacks
-export([init/1, handle_event/2, handle_call/2, 
         handle_info/2, terminate/2]).

%% init(Args) must return {ok, State}
init(Args) ->
    io:format("*** my_alarm_handler init:~p~n",[Args]),
    {ok, 0}.

handle_event({set_alarm, tooHot}, N) ->
    error_logger:error_msg("*** Tell the Engineer to turn on the fan~n"),
    {ok, N+1};
handle_event({clear_alarm, tooHot}, N) ->
    error_logger:error_msg("*** Danger over. Turn off the fan~n"),
    {ok, N};
handle_event(Event, N) ->
    io:format("*** unmatched event:~p~n",[Event]),
    {ok, N}.

handle_call(_Request, N) -> Reply = N, {ok, Reply,  N}.

handle_info(_Info, N)    -> {ok, N}.

terminate(_Reason, _N)   -> ok.

測試

>erl -boot start_sasl -config elog3

% 一開始的 set_alarm,什麼事都不會發生,因為預設的處理器不做任何事情
1> alarm_handler:set_alarm(tooHot).
ok

=INFO REPORT==== 20-Feb-2014::15:01:57 ===
    alarm_handler: {set,tooHot}

% 替換 alarm_handler
2> gen_event:swap_handler(alarm_handler, {alarm_handler, swap}, {my_alarm_handler, xyz}).
*** my_alarm_handler init:{xyz,{alarm_handler,[tooHot]}}
ok

% 再一次呼叫 set_alarm 就會改用自訂的 alarm handler
3> alarm_handler:set_alarm(tooHot).

=ERROR REPORT==== 20-Feb-2014::15:02:49 ===
*** Tell the Engineer to turn on the fan
ok

% 清除 tooHot 的警報
4> alarm_handler:clear_alarm(tooHot).

=ERROR REPORT==== 20-Feb-2014::15:03:09 ===
*** Danger over. Turn off the fan
ok

查看警報的日誌報告

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> rb:start([{max,20}]).
rb: reading report...done.
{ok,<0.42.0>}
2> rb:list().
  No                Type   Process       Date     Time
  ==                ====   =======       ====     ====
  14            progress  <0.30.0> 2014-02-20 15:57:19
  13            progress  <0.30.0> 2014-02-20 15:57:19
  12            progress  <0.30.0> 2014-02-20 15:57:19
  11            progress  <0.30.0> 2014-02-20 15:57:19
  10            progress  <0.24.0> 2014-02-20 15:57:19
   9            progress  <0.30.0> 2014-02-20 15:58:12
   8         info_report  <0.30.0> 2014-02-20 16:00:25
   7               error  <0.30.0> 2014-02-20 16:00:36
   6               error  <0.30.0> 2014-02-20 16:00:39
   5            progress  <0.30.0> 2014-02-20 16:02:03
   4            progress  <0.30.0> 2014-02-20 16:02:03
   3            progress  <0.30.0> 2014-02-20 16:02:03
   2            progress  <0.30.0> 2014-02-20 16:02:03
   1            progress  <0.24.0> 2014-02-20 16:02:03
ok
3> rb:show(6).

ERROR REPORT  <0.34.0>                                      2014-02-20 16:00:39
===============================================================================

*** Danger over. Turn off the fan
ok
4> rb:show(7).

ERROR REPORT  <0.34.0>                                      2014-02-20 16:00:36
===============================================================================

*** Tell the Engineer to turn on the fan
ok
4> rb:stop().
ok

應用伺服器

有兩個伺服器

質數

-module(prime_server).
-behaviour(gen_server).

-export([new_prime/1, start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

%% client functions
start_link() ->
    %% 呼叫 gen_server:start_link(Name, CallBackMod, StartArgs, Opts) 啟動 server
    %% 第一個呼叫的是 Mod:init(StartArgs)
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

new_prime(N) ->
    %% 20000 is a timeout (ms)
    gen_server:call(?MODULE, {prime, N}, 20000).


%% 6 server callback functions

%% 啟動時會被呼叫的函數,回傳值 {ok, State} 裡面的 State 會在其他函數中使用
init([]) ->
    %% Note we must set trap_exit = true if we 
    %% want terminate/2 to be called when the application is stopped
    %% 一定要設定 trap_exit 為 true,這樣在  app 停止時,才會呼叫 terminate/2
    process_flag(trap_exit, true),
    io:format("~p starting~n",[?MODULE]),
    {ok, 0}.

handle_call({prime, K}, _From, N) -> 
    {reply, make_new_prime(K), N+1}.

handle_cast(_Msg, N)  -> {noreply, N}.

handle_info(_Info, N)  -> {noreply, N}.

terminate(_Reason, _N) -> 
    io:format("~p stopping~n",[?MODULE]),
    ok.

code_change(_OldVsn, N, _Extra) -> {ok, N}.

% private function
make_new_prime(K) ->
    if
    K > 100 ->
        alarm_handler:set_alarm(tooHot),
        N = lib_primes:make_prime(K),
        alarm_handler:clear_alarm(tooHot),
        N;
    true ->
        lib_primes:make_prime(K)
    end.

面積

-module(area_server).
-behaviour(gen_server).

-export([area/1, start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

%% client functions
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

area(Thing) ->
    gen_server:call(?MODULE, {area, Thing}).


%% 6 server callback functions
%% 啟動時會被呼叫的函數,回傳值 {ok, State} 裡面的 State 會在其他函數中使用
init([]) ->
    %% Note we must set trap_exit = true if we 
    %% want terminate/2 to be called when the application
    %% is stopped
    %% 一定要設定 trap_exit 為 true,這樣在  app 停止時,才會呼叫 terminate/2
    process_flag(trap_exit, true),
    io:format("~p starting~n",[?MODULE]),
    {ok, 0}.

handle_call({area, Thing}, _From, N) -> {reply, compute_area(Thing), N+1}.

handle_cast(_Msg, N)  -> {noreply, N}.

handle_info(_Info, N)  -> {noreply, N}.

terminate(_Reason, _N) -> 
    io:format("~p stopping~n",[?MODULE]),
    ok.

code_change(_OldVsn, N, _Extra) -> {ok, N}.

%% private function
compute_area({square, X})       -> X*X;
compute_area({rectongle, X, Y}) -> X*Y.

監督樹

所有監督樹都是行程樹,樹上方的行程(監督者)會監控下面的行程(工作者),如果工作者發生問題,監督者就可以重新啟動它。監督樹有兩種:

  1. 一對一監督樹
    如果工作者失敗,就會被監督者重新啟動。監督者會同時監督多個行程,一對一就是針對每一個行程,都會單獨地進行重新啟動的機制。
  2. 一對多監督樹
    如果任一工作者失敗,監督者監控的所有工作者都會被 kill 掉,然後重新啟動所有工作行程。

監督樹的指定是透過下面這樣的函數

init(...) ->
    {ok, {RestartStrategy, MaxRestarts, Time},
        [Worker1, Worker2, ...]}.

RestartStrategy 有兩種 (1) one_for_one (2) all_for_one
MaxRestarts 與 Time 是設定一個 「重新啟動的頻率」,如果監督者在 Time 秒內重新啟動工作者超過 MaxRestarts 次,此監督者就會終止所有工作行程,並終止自己。這個機制是為了避免發生「行程當機->重新啟動->行程當機」 的無窮迴圈。

Worker1, Worker2,... 都是 tuple,描述如何重新啟動每一個工作者行程。

Worker
    {Tag, {Mod, Fun, ArgList},
        Restart,
        Shutdown,
        Type,
        [Mod1]}

Tag
    atom,用來稱呼該工作者行程
{Mod, Fun, ArgList}
    定義了監督者會用到的函數,當作 apply(Mod, Fun, ArgList) 的參數
Restart = permanent | transient | temporary
    permanent 行程一定會被重新啟動
    transient 只有在「因非正常結束碼而停止」之後,才會重新啟動
    temporary 不會被重新啟動
Shutdown
    停止 server 時所能耗用的最大時間,超過時間,就會被直接 kill
Type = worker | supervisor
    被監督行程的型別,我們可以在工作者行程的位置改放監督者行程,建構出監督者的樹
[Mod1]
    如果子行程是監督者或 gen_server 行為callback module,這就是 callback module 的名稱

範例

-module(sellaprime_supervisor).
-behaviour(supervisor).

-export([start/0, start_in_shell_for_testing/0, start_link/1, init/1]).

start() ->
    spawn(fun() ->
                  supervisor:start_link({local,?MODULE}, ?MODULE, _Arg = [])
          end).

start_in_shell_for_testing() ->
    {ok, Pid} = supervisor:start_link({local,?MODULE}, ?MODULE, _Arg = []),
    unlink(Pid).

start_link(Args) ->
    supervisor:start_link({local,?MODULE}, ?MODULE, Args).

init([]) ->
    %% Install my personal error handler
    %  替換自訂的 error handler
    gen_event:swap_handler(alarm_handler, 
                           {alarm_handler, swap},
                           {my_alarm_handler, xyz}),

    {ok, {{one_for_one, 3, 10},
          [{tag1, 
            {area_server, start_link, []},
            permanent, 
            10000, 
            worker, 
            [area_server]},
           {tag2, 
            {prime_server, start_link, []},
            permanent, 
            10000, 
            worker, 
            [prime_server]}
          ]}}.

啟動系統

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> sellaprime_supervisor:start_in_shell_for_testing().
*** my_alarm_handler init:{xyz,{alarm_handler,[]}}
area_server starting
prime_server starting
true
2> area_server:area({square, 10}).
100

% area_server 會停掉,並重新啟動
3> area_server:area({rectangle, 10, 20}).
area_server stopping
area_server starting
** exception exit: {{function_clause,
                        [{area_server,compute_area,
                             [{rectangle,10,20}],
                             [{file,
                                  "d:/projectcase/erlang/erlangotp/src/area_serv
er.erl"},
                              {line,50}]},
                         {area_server,handle_call,3,
                             [{file,
                                  "d:/projectcase/erlang/erlangotp/src/area_serv
er.erl"},
                              {line,37}]},
                         {gen_server,handle_msg,5,
                             [{file,"gen_server.erl"},{line,585}]},
                         {proc_lib,init_p_do_apply,3,
                             [{file,"proc_lib.erl"},{line,239}]}]},
                    {gen_server,call,[area_server,{area,{rectangle,10,20}}]}}
     in function  gen_server:call/2 (gen_server.erl, line 180)
4>
=ERROR REPORT==== 20-Feb-2014::18:08:09 ===
** Generic server area_server terminating
** Last message in was {area,{rectangle,10,20}}
** When Server state == 1
** Reason for termination ==
** {function_clause,
       [{area_server,compute_area,
            [{rectangle,10,20}],
            [{file,"d:/projectcase/erlang/erlangotp/src/area_server.erl"},
             {line,50}]},
        {area_server,handle_call,3,
            [{file,"d:/projectcase/erlang/erlangotp/src/area_server.erl"},
             {line,37}]},
        {gen_server,handle_msg,5,[{file,"gen_server.erl"},{line,585}]},
        {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,239}]}]}
4> area_server:area({square, 20}).
400
5> prime_server:new_prime(20).
Generating a 20 digit prime ..........................
11511342604390163281
6> prime_server:new_prime(120).
Generating a 120 digit prime .
=ERROR REPORT==== 20-Feb-2014::18:08:48 ===
*** Tell the Engineer to turn on the fan
................................................................................
31109310729316846838871494679201035944025034010885289775272667568919068598600821
8311374378220338982779418876998717503511

=ERROR REPORT==== 20-Feb-2014::18:08:51 ===
*** Danger over. Turn off the fan

接下來查閱報表。

7> rb:start([{max, 20}]).
rb: reading report...done.
{ok,<0.53.0>}
8> rb:list().
  No                Type      Process       Date     Time
  ==                ====      =======       ====     ====
  13            progress     <0.30.0> 2014-02-20 18:07:21
  12            progress     <0.30.0> 2014-02-20 18:07:21
  11            progress     <0.30.0> 2014-02-20 18:07:21
  10            progress     <0.30.0> 2014-02-20 18:07:21
   9            progress     <0.24.0> 2014-02-20 18:07:21
   8            progress     <0.24.0> 2014-02-20 18:07:39
   7            progress     <0.24.0> 2014-02-20 18:07:39
   6               error     <0.24.0> 2014-02-20 18:08:09
   5        crash_report  area_server 2014-02-20 18:08:09
   4   supervisor_report     <0.24.0> 2014-02-20 18:08:09
   3            progress     <0.24.0> 2014-02-20 18:08:09
   2               error     <0.30.0> 2014-02-20 18:08:48
   1               error     <0.30.0> 2014-02-20 18:08:51
ok
9> rb:show(5).

CRASH REPORT  <0.43.0>                                      2014-02-20 18:08:09
===============================================================================
Crashing process
   initial_call                              {area_server,init,['Argument__1']}
   pid                                                                 <0.43.0>
   registered_name                                                  area_server
   error_info
         {exit,
            {function_clause,
                [{area_server,compute_area,
                     [{rectangle,10,20}],
                     [{file,
                          "d:/projectcase/erlang/erlangotp/src/area_server.erl",
                      {line,50}]},
                 {area_server,handle_call,3,
                     [{file,
                          "d:/projectcase/erlang/erlangotp/src/area_server.erl",
                      {line,37}]},
                 {gen_server,handle_msg,5,
                     [{file,"gen_server.erl"},{line,585}]},
                 {proc_lib,init_p_do_apply,3,
                     [{file,"proc_lib.erl"},{line,239}]}]},
            [{gen_server,terminate,6,[{file,"gen_server.erl"},{line,744}]},
             {proc_lib,init_p_do_apply,3,
                 [{file,"proc_lib.erl"},{line,239}]}]}
   ancestors                                   [sellaprime_supervisor,<0.40.0>]
   messages                                                                  []
   links                                                             [<0.42.0>]
   dictionary                                                                []
   trap_exit                                                               true
   status                                                               running
   heap_size                                                                987
   stack_size                                                                27
   reductions                                                               200

ok

包裝應用

%% sellaprime.app
{application, sellaprime, 
 [{description, "The Prime Number Shop"},
  {vsn, "1.0"},
  {modules, [sellaprime_app, sellaprime_supervisor, area_server, 
         prime_server, lib_lin, lib_primes, my_alarm_handler]},    
  {registered,[area_server, prime_server, sellaprime_super]},
  {applications, [kernel,stdlib]},
  {mod, {sellaprime_app,[]}},
  {start_phases, []}
 ]}.

sellaprime.app 的 callback module,必須有 start/2 跟 stop/1 function

% sellaprime_app.erl
-module(sellaprime_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, StartArgs) ->
    sellaprime_supervisor:start_link(StartArgs).

stop(_State) ->
    ok.

測試
application:loaded_applications(). 取得 otp 載入的applications
application:load(sellaprime). 載入 sellaprime.app
application:start(sellaprime). 啟動 sellaprime
application:stop(sellaprime). 停止 sellaprime
application:unload(sellaprime). 卸載 sellaprime.app

>erl -boot start_sasl -config elog3
Eshell V5.10.4  (abort with ^G)
1> application:loaded_applications().
[{kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
2> application:load(sellaprime).
ok
3> application:loaded_applications().
[{sellaprime,"The Prime Number Shop","1.0"},
 {kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
4> application:start(sellaprime).
*** my_alarm_handler init:{xyz,{alarm_handler,[]}}
area_server starting
prime_server starting
ok
5> area_server:area({square, 20}).
400
6> prime_server:new_prime(20).
Generating a 20 digit prime ...................
28361723754284313301
7> application:stop(sellaprime).
prime_server stopping
area_server stopping
ok
=INFO REPORT==== 21-Feb-2014::10:29:12 ===
    application: sellaprime
    exited: stopped
    type: temporary
8> application:loaded_applications().
[{sellaprime,"The Prime Number Shop","1.0"},
 {kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
9> application:unload(sellaprime).
ok
10> application:loaded_applications().
[{kernel,"ERTS  CXC 138 10","2.16.4"},
 {sasl,"SASL  CXC 138 11","2.3.4"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"}]
11> init:stop().
ok

完整的應用包含的檔案

  1. area_server.erl
    area_server 這是 gen_server 的 callback module
  2. prime_server.erl
    prime_server 這是 gen_server 的 callback module
    2.1 lib_primes.erl, lib_lin.erl
    產生 primes 的 module
  3. sellaprime_supervisor.erl
    監督者 callback module
  4. sellaprime_app.erl
    sellaprime application callback module
  5. my_alarm_handler.erl
    gen_event 事件處理 callback module
  6. sellaprime.app
    sellaprime application
  7. elog3.config
    error logger configuration

運作流程如下

  1. 啟動系統 > erl -boot start_sasl -config elog3.config
    sellaprime.app 必須在 erlang 啟動的根目錄,或在該目錄的某個次目錄
    應用控制器會在 sellaprime.app 中取得 {mod, ...} 宣告,也就是 sellaprime_app.erl

  2. 呼叫 sellaprime_app:start/2

  3. sellaprime_app:start/2 內部呼叫 sellaprime_supervisor:start_link/2,然後啟動了 sellaprime 監督者

  4. 呼叫 sellaprime_supervisor:init/1,這會安裝一個 error logger 處理器,並回傳重新啟動的策略

  5. sellaprime 的監督者會啟動 prime_server 與 area_server

  6. 呼叫 application:stop(sellaprime) 或 init:stop() 就可以停止 sellaprime application

GUI 應用監控器

這是可以檢視 application 的 GUI 程式

appmon:start().

參考

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

2014/04/28

erlang - gen_server

OTP 函式庫中包含了完整的 web server、FTP server、CORBA ORB,OTP 的 behavior 將常用的行為模式包裝起來,可想成 「用 callback 模組進行參數化」的應用框架。

行為解決了問題的「非功能」部份, callback 解決了功能的部份
問題中的非功能部份(ex: 如何動態更新程式碼)對所有應用來說都是一樣的。

以下會

  1. 以 erlang 寫一個小 client-server 程式
  2. 將程式一般化
  3. 進入真正的程式碼

發展通用伺服器的過程

以下程式會慢慢地將程式中,非功能(一般化)與功能的部份區分開來。

server1: 基本伺服器, 用 callback module 將它參數化

server1 是一個基本的 server, 可以用 callback module 將它參數化,換句話說,就是把 callback 的程式跟 server 的程式分開。

%% server1.erl
-module(server1).
-export([start/2, rpc/2]).

start(Name, Mod) ->
    register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end)).

rpc(Name, Request) ->
    Name ! {self(), Request},
    receive
        {Name, Response} -> Response
    end.

loop(Name, Mod, State) ->
    receive
        {From, Request} ->
            {Response, State1} = Mod:handle(Request, State),
            From ! {Name, Response},
            loop(Name, Mod, State1)
    end.

以下是 name_server 的 callback module,一般 OTP 程式的習慣寫法,就是把 client 跟 server 端程式,放在同一個 module 裡面。

-module(name_server).
-export([init/0, add/2, whereis/1, handle/2]).
-import(server1, [rpc/2]).

%% client apis,這是讓 client 端呼叫的程式介面
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
whereis(Name)    -> rpc(name_server, {whereis, Name}).

%% callback routines,這是讓 server 呼叫的
init() -> dict:new().

handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({whereis, Name}, Dict)    -> {dict:find(Name, Dict), Dict}.

測試

1> server1:start(name_server, name_server).
true
2> name_server:add(tom, "home").
ok
3> name_server:whereis(tom).
{ok,"home"}

注意:name_server callback 沒有共時性、沒有生成、接收、送出、register,這表示我們可以寫 client-server module,而不需要知道下面的共時模型是什麼,這是所有伺服器的基本模型。

server2: 具有 Transaction 的伺服器

下面是一個會讓客戶當機的 server,如果查詢導致 Server 例外,客戶端就會當機。

-module(server2).
-export([start/2, rpc/2]).

start(Name, Mod) ->
    register(Name, spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

rpc(Name, Request) ->
    Name ! {self(), Request},
    receive
        %% client 收到 server 回傳的 crash,就跳出程式
        {Name, crash} -> exit(rpc);
        {Name, ok, Response} -> Response
    end.

loop(Name, Mod, OldState) ->
    receive
        {From, Request} ->
            try Mod:handle(Request, OldState) of
                {Response, NewState} ->
                    From ! {Name, ok, Response},
                    loop(Name, Mod, NewState)
            catch
                _:Why ->
                    log_the_error(Name, Request, Why),
                    %% 因為程式出錯了,發送 crash 訊息給 client
                    From ! {Name, crash},
                    %% 再次 loop 等待處理下一個訊息
                    loop(Name, Mod, OldState)
            end
    end.

log_the_error(Name, Request, Why) ->
    io:format("Server ~p request ~p ~n"
                  "caused exception ~p~n", 
                  [Name, Request, Why]).

如果例外發生在處理器函數內,它以 State 的原始值進行迴圈
,如果函數成功,它會以「處理器函數所提供的」NewState值進行迴圈,當處理器發生錯誤時,伺服器的狀態會維持不變。

name_server 要改成使用 server2 的話,只需要修改 -import 的地方

-module(name_server).
-export([init/0, add/2, whereis/1, handle/2]).
-import(server2, [rpc/2]).

%% client routines
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
whereis(Name)    -> rpc(name_server, {whereis, Name}).

%% callback routines
init() -> dict:new().

handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({whereis, Name}, Dict)    -> {dict:find(Name, Dict), Dict}.

測試

1> server2:start(name_server, name_server).
true
2> name_server:add(tom, "home").
ok
3> name_server:whereis(tom).
{ok,"home"}
4> name_server:whereis(jane).
error

server3: 具有程式碼熱抽換功能的伺服器

如果送給伺服器一個程式碼熱抽換 swap_code 的訊息,就會改變 callback module,變成呼叫新模組

-module(server3).
-export([start/2, rpc/2, swap_code/2]).

start(Name, Mod) ->
    register(Name, 
             spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name, Mod) -> rpc(Name, {swap_code, Mod}).

rpc(Name, Request) ->
    Name ! {self(), Request},
    receive
        {Name, Response} -> Response
    end.

loop(Name, Mod, OldState) ->
    receive
        {From, {swap_code, NewCallBackMod}} ->
            From ! {Name, ack},
            loop(Name, NewCallBackMod, OldState);
        {From, Request} ->
            {Response, NewState} = Mod:handle(Request, OldState),
            From ! {Name, Response},
            loop(Name, Mod, NewState)
    end.

因為 name_server 所註冊綁定的 process name 不能在程式運作過程中修改掉,但我們可以動態地把 name_server process 運作的 module 換掉,達到抽換程式碼的功能。

-module(name_server1).
-export([init/0, add/2, whereis/1, handle/2]).
-import(server3, [rpc/2]).

%% client routines
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
whereis(Name)    -> rpc(name_server, {whereis, Name}).

%% callback routines
init() -> dict:new().

handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({whereis, Name}, Dict)    -> {dict:find(Name, Dict), Dict}.

測試

1> server3:start(name_server, name_server1).
true
2> name_server1:add(tom, "home").
ok
3> name_server1:add(jane, "home").
ok

如果我們測試到一半,想要增加「找出 name_server 儲存的」所有名稱,由於目前 name_server1 並沒有這個函數,所以我們就撰寫一個新的 new_name_server,增加了 all_names 與 delete 函數。

-module(new_name_server).
-export([init/0, add/2, all_names/0, delete/1, whereis/1, handle/2]).
-import(server3, [rpc/2]).

%% interface
all_names()      -> rpc(name_server, allNames).
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
delete(Name)     -> rpc(name_server, {delete, Name}).
whereis(Name)    -> rpc(name_server, {whereis, Name}).

%% callback routines
init() -> dict:new().

handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle(allNames, Dict)           -> {dict:fetch_keys(Dict), Dict};
handle({delete, Name}, Dict)     -> {ok, dict:erase(Name, Dict)};
handle({whereis, Name}, Dict)    -> {dict:find(Name, Dict), Dict}.

在 console 就可以直接用 swap_code 把 name_server 執行的 module 換成 new_name_server

4> server3:swap_code(name_server, new_name_server).
ack
5> new_name_server:all_names().
[jane,tom]

以往認為伺服器本身有記錄狀態,發送訊息就會改變狀態,如果要抽換程式邏輯,必須要把 server 停下來,改變程式,然後重新啟動,但實際上,可以透過訊息的方式,讓 server 的迴圈執行新版本的 server module。

Server 4: 具有 Transaction 與 程式碼熱抽換 合併

-module(server4).
-export([start/2, rpc/2, swap_code/2]).

start(Name, Mod) ->
    register(Name, spawn(fun() -> loop(Name,Mod,Mod:init()) end)).

swap_code(Name, Mod) -> rpc(Name, {swap_code, Mod}).

rpc(Name, Request) ->
    Name ! {self(), Request},
    receive
        {Name, crash} -> exit(rpc);
        {Name, ok, Response} -> Response
    end.

loop(Name, Mod, OldState) ->
    receive
        {From, {swap_code, NewCallbackMod}} ->
            From ! {Name, ok, ack},
            loop(Name, NewCallbackMod, OldState);
        {From, Request} ->
            try Mod:handle(Request, OldState) of
                {Response, NewState} ->
                    From ! {Name, ok, Response},
                    loop(Name, Mod, NewState)
            catch
                _: Why ->
                    log_the_error(Name, Request, Why),
                    From ! {Name, crash},
                    loop(Name, Mod, OldState)
            end
    end.

log_the_error(Name, Request, Why) ->
    io:format("Server ~p request ~p ~n"
                  "caused exception ~p~n", 
                  [Name, Request, Why]).

server 5

此程式不做任何事,直到我們告訴它 {become, F},就會變成伺服器 F。

-module(server5).
-export([start/0, rpc/2]).

start() -> spawn(fun() -> wait() end).

wait() ->
    receive
        {become, F} -> F()
    end.

rpc(Pid, Q) ->
    Pid ! {self(), Q},
    receive
        {Pid, Reply} -> Reply
    end.

這是計算階層的 server

-module(fac_server).
-export([loop/0]).

loop() ->
    receive
    {From, {fac, N}} ->
        From ! {self(), fac(N)},
        loop();
    {become, Something} ->
        Something()
    end.

fac(0) -> 1;
fac(N) -> N * fac(N-1).

測試

1> Pid = server5:start().
<0.33.0>
2> Pid ! {become, fun fac_server:loop/0}
.
{become,#Fun<fac_server.loop.0>}
3> server5:rpc(Pid, {fac, 10}).
3628800

OTP 的 gen_server 就像是最後的 server5,是個一般化,可變成任何一種 server 的 server library,它提供了所有非功能的行為與錯誤處理。

使用 gen_server

三個步驟寫出一個 gen_server callback module

  1. 為 callback module 取名
  2. 寫 client 使用的介面函數
  3. 寫 callback module 中需要的六個 callback funciton

範例

  1. 為 callback module 取名
    my_bank
  2. 寫 client 使用的介面函數
    start() 啟動銀行
    stop() 關閉銀行
    new_account(Who) 建立新銀行帳號
    deposit(Who, Amount) 存款
    withdraw(Who, Amount) 提款

    這些都是呼叫 gen_server 的函數

     start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
     stop()  -> gen_server:call(?MODULE, stop).
    
     new_account(Who)      -> gen_server:call(?MODULE, {new, Who}).
     deposit(Who, Amount)  -> gen_server:call(?MODULE, {add, Who, Amount}).
     withdraw(Who, Amount) -> gen_server:call(?MODULE, {remove, Who, Amount}).

    2.1 gen_server:start_link({local, Name}, Mode, ...) 會啟動一個本地伺服器,如果第一個參數填為 {global,GlobalName} ,則此伺服器為 global server
    2.2 ?MODULE 會展開成 my_bank
    2.3 gen_server:call(?MODULE, Term) 用來對 server 進行遠端呼叫 rpc

  3. 寫 callback module 中需要的六個 callback funcitons

callbak module 中要撰寫六個 callback functions
init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3

以下為一個 gen_server 程式,最簡單的樣板

-module().
%% 編譯時,如果沒有定義適當的callback function,就會產生警告
-behaviour(gen_server).
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

%% 呼叫 gen_server:start_link(Name, CallBackMod, StartArgs, Opts) 啟動 server
%% 第一個呼叫的是 Mod:init(StartArgs)
start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% 啟動時會被呼叫的函數,回傳值裡面的 State 會在其他函數中使用
init([]) -> {ok, State}.

%% 這是rpc 遠端呼叫時 server 的處理函數,Reply 會被送回 client 端
handle_call(_Request, _From, State) -> {reply, Reply, State}.
handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.

terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, Extra) -> {ok, State}.

以下為 my_bank 實際上用 ets 實作的程式

-module(my_bank).

-behaviour(gen_server).
-export([start/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).
-compile(export_all).

%% client functions
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop()  -> gen_server:call(?MODULE, stop).

new_account(Who)      -> gen_server:call(?MODULE, {new, Who}).
deposit(Who, Amount)  -> gen_server:call(?MODULE, {add, Who, Amount}).
withdraw(Who, Amount) -> gen_server:call(?MODULE, {remove, Who, Amount}).

%% 6 server callback functions
init([]) -> {ok, ets:new(?MODULE,[])}.

handle_call({new,Who}, _From, Tab) ->
    %% 先找看看帳號有沒有存在,舊帳號就不能再 ets:insert
    Reply = case ets:lookup(Tab, Who) of
                []  -> ets:insert(Tab, {Who,0}), 
                       {welcome, Who};
                [_] -> {Who, you_already_are_a_customer}
            end,
    {reply, Reply, Tab};
%% Who 存錢 X,最後得到餘額
handle_call({add,Who,X}, _From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                []  -> not_a_customer;
                [{Who,Balance}] ->
                    NewBalance = Balance + X,
                    ets:insert(Tab, {Who, NewBalance}),
                    {thanks, Who, your_balance_is,  NewBalance}    
            end,
    {reply, Reply, Tab};
%% Who 提款 X,最後得到餘額
handle_call({remove,Who, X}, _From, Tab) ->
    Reply = case ets:lookup(Tab, Who) of
                []  -> not_a_customer;
                %% 扣錢之前,要先判斷一下夠不夠,餘額不能變成負值
                [{Who,Balance}] when X =< Balance ->
                    NewBalance = Balance - X,
                    ets:insert(Tab, {Who, NewBalance}),
                    {thanks, Who, your_balance_is,  NewBalance};    
                [{Who,Balance}] ->
                    {sorry,Who,you_only_have,Balance,in_the_bank}
            end,
    {reply, Reply, Tab};
handle_call(stop, _From, Tab) ->
    %% server 收到 stop,會回傳以下訊息,然後停止 server
    %% 第二個 normal 會變成 terminate 的第一個參數 _Reason
    %% 第三個參數 stopped 會變成 my_bank:stop() 的回傳值
    {stop, normal, stopped, Tab}.

handle_cast(_Msg, State) -> {noreply, State}.
handle_info(_Info, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
code_change(_OldVsn, State, Extra) -> {ok, State}.

測試

1> my_bank:start().
{ok,<0.33.0>}
2> my_bank:deposit("tom", 10).
not_a_customer
3> my_bank:new_account("tom").
{welcome,"tom"}
4> my_bank:new_account("tom").
{"tom",you_already_are_a_customer}
5> my_bank:deposit("tom", 10).
{thanks,"tom",your_balance_is,10}
6> my_bank:deposit("tom", 15).
{thanks,"tom",your_balance_is,25}
7> my_bank:withdraw("tom", 5).
{thanks,"tom",your_balance_is,20}
8> my_bank:withdraw("tom", 35).
{sorry,"tom",you_only_have,20,in_the_bank}

gen_server callback functions 的細節

  1. 啟動 server
    呼叫 gen_server:start_link(Name,Mod,InitArgs,Opts) 就會啟動 server,建立一個名稱為 Name 的 server,callback module 為 Mod,Opts 控制這個 server 的行為:可指定訊息的記錄、除錯函數等等。在呼叫 Mod:init(InitArgs) 之後,此伺服器就被啟動完成

     @spec init(Args) -> {ok, State} |
                     {ok, State, Timeout} |
                     ignore |
                     {stop, Reason}

    當 init 回傳 {ok, State},就代表啟動成功,且初始狀態為 State

  2. 呼叫 server 時,客戶端會呼叫 gen_server:call(Name, Request),進而呼叫 callback function: handle_call/3

     @spec handle_call(Request, From, State) -> {reply, Reply, State} |
                     {reply, Reply, State, Timeout} |
                     {noreply, State} |
                     {noreply, State, Timeout} |
                     {stop, Reason, Reply, State} |
                     {stop, Reason, State}

    Request 是 gen_server:call 的第二個參數
    From 是 client 端發出 Request 的 process PID
    State 是server目前的狀態

    通常會回傳 {reply, Reply, NewState} ,Reply 會回傳給 client 端,變成 gen_server:call 的回傳值,而 NewState 是 server 的下一個狀態

  3. gen_server:cast(Name,Name) 這是不具有回傳值的呼叫,單純的發送訊息給 server

     @spec handle_cast(Msg, State) -> {noreply, NewState} |
                     {noreply, NewState, Timeout} |
                     {stop, Reason, NewState}

    通常會回傳 {noreply, NewState} 改變 server 的狀態

  4. gen_server:handle_info(Info, State) 是用來處理「自發訊息」的
    例如當 server process 被連結到另一個行程,且會捕捉離開訊息,那就可能會收到 {'EXIT', Pid, What} 訊息。或是系統中任何需要知道此 server 的 PID,都可以發送訊息過來,這樣的訊息最後會變成 Info 的值。

     @spec handle_info(Info, State) -> {noreply, State} |
                     {noreply, State, Timeout} |
                     {stop, Reason, State}
  5. server 終結的理由很多,但最後都會呼叫 terminate(Reason, NewState)
    5.1 handle_call, handle_cast, handle_info 可能會傳回 {stop, Reason, NewState}
    5.2 server可能會以 {'EXIT', reason } 當機

     @spec terminate(_Reason, State) -> void()

    可以在這裡把資料放進 disk,或是產生新訊息傳給其他 processes,或是直接丟棄

  6. 程式碼更新時,會呼叫 code_change

     @spec code_change(OldVsn, State, Extra) -> {ok, NewState}

在 emacs 提供的 gen_server template

%%%-------------------------------------------------------------------
%%% File    : gen_server_template.erl
%%% Author  : my name <yourname@localhost.localdomain>
%%% Description : 
%%%
%%% Created :  1 Jan 2014 by my name <yourname@localhost.localdomain>
%%%-------------------------------------------------------------------
-module().

-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
     terminate/2, code_change/3]).

-record(state, {}).

%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%====================================================================
%% gen_server callbacks
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%%                         {ok, State, Timeout} |
%%                         ignore               |
%%                         {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
    {ok, #state{}}.

%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%%                                      {reply, Reply, State, Timeout} |
%%                                      {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, Reply, State} |
%%                                      {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%%                                      {noreply, State, Timeout} |
%%                                      {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast(_Msg, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%%                                       {noreply, State, Timeout} |
%%                                       {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
    ok.

%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------

參考

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