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/05/02

再談Objective-C中的Block - 使用Block的注意事項

前言

簡單回顧:Block是C語言的Closure語法,Block中可以有條件地使用Block以外所定義的變數(詳情請參閱前文)。
而此篇要稍微深入探討使用Block時應注意的事項。

Block所佔用的記憶體區域被配置在堆疊上

考慮以下程式碼:
void (^block)();
if {
    block = ^{ //do something 1...  };
}
else {
    block = ^{ //do something 2...  };
}
block();
我們可能很容易因邏輯需求而寫出這樣的程式碼。
然而,由於Block所佔用的記憶體區域被配置在堆疊上;因此,Block所占用的記憶體空間只在該作用域內有效。 在C語言中,即便是在同一個函式中,一個大括號包起來的就是一個獨立的作用域[1];這表示,過了該作用域之後,Block所占用的記憶體空間可能不再被保證有效。
程式可能出錯,也可能不會;端看該Block所占用的記憶體空間是否被覆蓋掉 。
不過,一個簡單的做法就可以解決此問題,使用copy[2]:
void (^block)();
if {
    block = [^{ //do something 1...  } copy];
}
else {
    block = [^{ //do something 2...  } copy];
}
block();
如果指向該Block的變數是物件的property中,也應該使用copy:
@property (copy) void (^myblock)(void);
以copy建立副本,確保存放該Block的變數,其指向的block是有效的。

避免Block隱式保留self而造成的保留循環

我曾在前文提及,
Block會隱式地保留self:
@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    void(^myBlock)(void) = ^{
        //其實是存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
由於Block也是Objective-C物件,也會被參考計數;因此,如下寫法:
@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 

    };      
}
會使myblock保留self,而self本身又擁有myblock,兩個物件互相參考, 造成所謂的保留循環,使兩者的記憶體永遠不會被釋放。

解法1:使用weak參考的self

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    ViewController* __weak weakSelf = self;
    self.myblock = ^{
        //確認self尚未被取消配置
        if(weakSelf != nil) {
            NSLog(@"myValue in block:%i",weakSelf->myValue); 
        }                       
    };      
}
使用另一個變數指向self,並使用__weak修飾子, 如此一來,該變數(weakSelf)不會保留住self(不會增加參考計數)。
而Block使用的是weakSelf而非self本身,因此不會保留self。
唯一要注意的是,在self被取消配置時,weakSelf會被設為nil; 因此在執行前,需要檢查一下weakSelf是否為nil(雖然在正確的邏輯下不應該發生); 做好例外處理。

解法2:在block執行完後,將指向block的property設為nil

@interface ViewController : UIViewController
@property (copy) void (^myblock)(void);
@end    

@implementation ViewController {
    int myValue;
}

- (void)viewDidLoad
{
    myValue = 123;
    self.myblock = ^{
        //存取self->myValue, block隱式保留self
        NSLog(@"myValue in block:%i",myValue); 


        self->myblock = nil;            
    };      
}
更簡單的方法是在執行完block的時候,將myblock設為nil,解除互相參考的狀態。 然而,倘若block永遠都沒有執行,則此保留循環的狀況也將不會解除。 因此,務必確認block一定會被執行;否則,建議使用解法1。

結語

Block的使用一直都是把雙面刃,如果你不夠了解Block的運作機制以及其行為模式,當然很有可能造成非預期的錯誤。 Block的語法乍看之下複雜,不過其實並不難。如果你先前曾有一些Script語言的經驗,諸如: JavaScript的函式實字,或是Python的lambda表示式...等等,則應該不難理解Block的概念。
在iOS程式開發中,許多處理多執行緒的API,也漸漸地使用Block,如GCD, NSBlockOperation...等等,因此,理解並活用Block也逐漸成為新一代iOS程式設計師應當做好的基本功。

Reference


2014/05/01

How to resolve "Unable to simultaneously satisfy constraints" error in ios

在 IPhone 4S 以前,螢幕大小是等比放大的,但 IPhone 5/5S 就不是等比放大了。因此工程師們在設計 app 時,為了讓畫面在所有 device 都不跑版,就可能用 code 的方式產生畫面中所有元件,另一種方式就是使用 storyboard 搭配 auto layout,而 constraints 就是 storyboard 搭配 auto layout 時會使用到的一種技術

constraints

  • 在 storyboard 中使用 auto layout 才會出現
  • 元件對齊的條件,包含元件與父容器、元件寬高、元件間對齊等等
  • 可在 storyboard 中設定,也可用 code 設定

Unable to simultaneously satisfy constraints problem

  • 在 storyboard 中設定 constraints 後,為了畫面的顯示,又再 controller 中動態修改,導致部分 constraints 發生衝突
  • 錯誤訊息

2014-04-09 16:03:23.280 kokola[4077:60b] Unable to simultaneously satisfy constraints.
 Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x8d8f340 H:[UILabel:0x100e120(165)]>",
    "<NSLayoutConstraint:0x8d74d20 H:[UILabel:0x1006630(0)]>",
    "<NSLayoutConstraint:0x100a1c0 H:[UIView:0x101c6a0(190)]>",
    "<NSLayoutConstraint:0x8d81620 H:|-(0)-[UILabel:0x100e120]   (Names: '|':UIView:0x101c6a0 )>",
    "<NSLayoutConstraint:0x100e860 H:[UILabel:0x1006630]-(0)-|   (Names: '|':UIView:0x101c6a0 )>",
    "<NSLayoutConstraint:0x10085b0 H:[UILabel:0x100e120]-(0)-[UILabel:0x1006630]>"
)

Will attempt to recover by breaking constraint 


Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in  may also be helpful.

2014-04-14 09:58:01.131 kokola[3511:60b] Unable to simultaneously satisfy constraints.
 Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x8f604d0 V:[UIView:0x8f63e40(88)]>",
    "<NSLayoutConstraint:0x1016af0 V:[UIView:0x8f560a0(0)]>",
    "<NSLayoutConstraint:0x1016b20 V:|-(0)-[UIView:0x8f63e40]   (Names: '|':UIView:0x8f560a0 )>",
    "<NSLayoutConstraint:0x1016be0 V:[UIView:0x8f63e40]-(0)-|   (Names: '|':UIView:0x8f560a0 )>"
)

Will attempt to recover by breaking constraint 

solve

  1. 看懂錯誤訊息
    (1) 方向與 constraint value
    
    "<NSLayoutConstraint:0x8d8f340 H:[UILabel:0x100e120(165)]>"
    
    在上方錯誤訊息中," H " 代表 Horizontal,而 165 代表 constraint value,UILabel 當然就是元件啦!
    由以上訊息可以知道,在 storyboard 中有某個寬度為 165 的 UILabel
  2. 
    "<NSLayoutConstraint:0x8f604d0 V:[UIView:0x8f63e40(88)]>"
    
    看完前一個錯誤訊息後,應該不難猜到上面錯誤訊息中的 " V " 就是 Vertical,所以由這個錯誤訊息可以知道,在 storyboard 中有某個高度為 88 的 UIView

    (2) constraint 對齊
    
    "<NSLayoutConstraint:0x8d81620 H:|-(0)-[UILabel:0x100e120]   (Names: '|':UIView:0x101c6a0 )>"
    
    從前面的錯誤訊息得知 " H " 代表 Horizontal,而 " H: " 後面的 " |-(0)- " 代表對齊方式與 constraint value。其中 " | " 代表對齊的方向;" - " 代表間距;" () " 中的數值代表 constraint value,而後面的 UIView 則代表與 UILabel 對齊的元件。所以上面的錯誤訊息表示,在 storyboard 中有一個 UILabel 與左邊 UIView 間距是 0
    
    "<NSLayoutConstraint:0x1016b20 V:|-(0)-[UIView:0x8f63e40]   (Names: '|':UIView:0x8f560a0 )>"
    
    前面的解說判斷,上面的錯誤訊息代表,在 storyboard 中有一個 UIView 與上面 UIView 間距是 0
2.   根據錯誤訊息找出元件
      (1) 選擇 storyboard 並切換到 " show the version editor " 模式
      (2) 搜尋一下上述 constraint value,然後往上找一下看是哪個元件,並搭配錯誤訊息中元件種類判斷一下可能的元件,接著看看元件的其他屬性,其中 " userLabel " 就是元件在 storyboard 中的名稱

      (3) 再往上看一點,可看到綠色註解,是 xcode 產生的註解,可看出元件再哪個 ViewController


      (4) 切換回 " show the standard editor " ( 檢視 UI )


      (5) 用剛剛的線索找到元件


3.   修正 constraint value
      (1) 選擇 constraint 並切換到 " Interface Builder " 的 " connection inspector ",查看是否與程式碼連結

      (2) 切換回 controller.m,在程式碼中尋找剛剛的連結 ( 有聯結 storyboard 的宣告,前面會有黑點點 ),查詢一下看哪裡會變更這個 constraint value,確認一下計算公式是否正確,constraint value 變更後是否會與其他 constraint value 發生衝突


References