erlang 組件 application

erlang 組件 application

我們日常用到的第三方的庫,組件絕大部分是application,所以理解并且掌握applicaiton的一些特性對我們來說非常重要,也非常實用。

1.什么是applicaiton?,為什么要用application

官方的解釋是這個樣子的:

When you have written code implementing some specific functionality you might want to make the code into an application, that is, a component that can be started and stopped as a unit, and which can also be reused in other systems.

http://erlang.org/doc/design_principles/applications.html

在我理解其實就2點:

  1. 為了實現(xiàn)特定功能
  2. 可以復用

在我們的日常生活中,單項目多application的場景比較少,我們平時寫的最多的是module準確的說是回調模塊Callback Module,也會有少量的進程狀態(tài)模塊Residence Module,前者只是一個回調函數(shù),從一個狀態(tài)切換到另一個狀態(tài),所以生命周期只在單process,很少與外界的process打交道,所以使用link,monitor這些進程間關系相對較少。反觀application要考慮的方面比較多,不僅要考慮狀態(tài)的正確與否,還會考慮到進程運行的異常與否,甚至會考慮到怎么來設計監(jiān)控樹來讓程序保持健壯。

2. 如何自己實現(xiàn)一個application?

2.1 目錄結構

─ ${application}
      ├── doc
      │   ├── internal
      │   ├── examples
      │   └── src
      ├── include
      ├── priv
      ├── src
      │   └── ${application}.app.src
      └── test
  • src 必須,存放源代碼(.erl)
  • priv 非必須,存放自定義的文件,比如nif的so文件等,還有資源文件
  • include 非必須,存放一些頭文件(hrl),方便別的application訪問
  • doc 非必須,存放一些文檔
  • test 非必須,測試文件,eunit common_test文件都放在這里

2.2 application 回調模塊(callback module)

默認情況下是$APP_NAME_app,當然自己也可以在${application}.app.src自行定義,定義方法如下:

{application, $APP_NAME,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib
                 ]},
   % 請在這里自定義回調模塊
   % 請在這里修改回調參數(shù)
  {mod, {$CALLBACL_MODULE, Args}},
  {env, []}
 ]}.

下面我來做一個最簡單的例子,然后來分析源代碼的流程,applicaton 的名稱是chapp

% 文件:chapp.app.src
{application, chapp,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib
                 ]},
  {mod, { chapp_app, [myargs]}},
  {env, []}
 ]}.

%文件:chapp_app,erl
-module(chapp_app).
-behaviour(application).

-export([start/2, stop/1]).
-record(state, {
  mod
}).
%% 啟動的回調函數(shù)
start(_StartType, _StartArgs) ->
  io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, {_StartType, _StartArgs}]),
  {ok, Pid} = chapp_sup:start_link(),
  {ok, Pid, #state{mod = ?MODULE}}.

%% 停止的回調函數(shù)
stop(_State) ->
  io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, _State]),
  ok.

運行結果如下:

Eshell V10.4  (abort with ^G)
1> application:start
start/1       start/2       start_boot/1  start_boot/2  start_type/0  


1> application:start(chapp).
% 成功打印出 myargs
chapp_app,start,{normal,[myargs]}
ok
2> application:stop(chapp).
% 還可以自定義pre_stop函數(shù)
chapp_app,prep_stop,{state,chapp_app}
% 成功打印出 state
chapp_app,stop,{state,chapp_app}
=INFO REPORT==== 1-Dec-2019::14:58:57.004000 ===
    application: chapp
    exited: stopped
    type: temporary
ok

2.3 $APP_NAME.app.src 文件格式

% application.erl(kernel)
start(Application, RestartType) ->
  case load(Application) of
    ok ->
      % 要先載入 .app.src文件
      Name = get_appl_name(Application),
      application_controller:start_application(Name, RestartType);
    {error, {already_loaded, Name}} ->
      application_controller:start_application(Name, RestartType);
    Error ->
      Error
  end.
  
load1(Application, DistNodes) ->
  % 載入application
  case application_controller:load_application(Application) of
    ...
    Else ->
      Else
  end.
% 我們再來看 application_controller.erl

% application_controller.erl
load_application(Application) ->
    gen_server:call(?AC, {load_application, Application}, infinity).

make_appl(Name) when is_atom(Name) ->
   % 在 path 里尋找 $APP_NANE.app文件,這個文件是 $APP_NAME.app.src轉化而來
  FName = atom_to_list(Name) ++ ".app",
  case code:where_is_file(FName) of
    non_existing ->
      {error, {file:format_error(enoent), FName}};
    FullName ->
      case prim_consult(FullName) of
        {ok, [Application]} ->
          {ok, make_appl_i(Application)};
        {error, Reason} ->
          {error, {file:format_error(Reason), FName}};
        error ->
          {error, "bad encoding"}
      end
  end;
  
  % 這個是$APP_NAME.app.src個文件格式
  % {application, Name, Opts}
  % 其中 有幾個一定要有的,如:description,mod,env
  % 我們要特別注意mod里面的參數(shù),因為這個是我們回調的參數(shù)
  make_appl_i({application, Name, Opts}) when is_atom(Name), is_list(Opts) ->
  Descr = get_opt(description, Opts, ""),
  Id = get_opt(id, Opts, ""),
  Vsn = get_opt(vsn, Opts, ""),
  Mods = get_opt(modules, Opts, []),
  Regs = get_opt(registered, Opts, []),
  Apps = get_opt(applications, Opts, []),
  Mod =
    case get_opt(mod, Opts, []) of
      {M, _A} = MA when is_atom(M) -> MA;
      [] -> [];
      Other -> throw({error, {badstartspec, Other}})
    end,
  Phases = get_opt(start_phases, Opts, undefined),
  Env = get_opt(env, Opts, []),
  MaxP = get_opt(maxP, Opts, infinity),
  MaxT = get_opt(maxT, Opts, infinity),
  IncApps = get_opt(included_applications, Opts, []),
  {#appl_data{name = Name, regs = Regs, mod = Mod, phases = Phases,
    mods = Mods, inc_apps = IncApps, maxP = MaxP, maxT = MaxT},
    Env, IncApps, Descr, Id, Vsn, Apps};

2.4 application啟動步驟

% application_controller.erl
handle_call({start_application, AppName, RestartType}, From, S) ->
  #state{running = Running, starting = Starting, start_p_false = SPF,
    started = Started, start_req = Start_req} = S,
  %% Check if the commandline environment variables are OK.
  %% Incase of erroneous variables do not start the application,
  %% if the application is permanent crash the node.
  %% Check if the application is already starting.
  case lists:keyfind(AppName, 1, Start_req) of
    false ->
      case catch check_start_cond(AppName, RestartType, Started, Running) of
        {ok, Appl} ->
            ......
            {false, undefined} ->
              % 正常的入口
              spawn_starter(From, Appl, S, normal),
              {noreply, S#state{starting = [{AppName, RestartType, normal, From} |
                Starting],
                start_req = [{AppName, From} | Start_req]}};
            ......
        {error, _R} = Error ->
          {reply, Error, S}
      end;
    {AppName, _FromX} ->
      SS = S#state{start_req = [{AppName, From} | Start_req]},
      {noreply, SS}
  end;

start_appl(Appl, S, Type) ->
  ApplData = Appl#appl.appl_data,
  case ApplData#appl_data.mod of
    [] ->
      {ok, undefined};
    _ ->
      %% Name = ApplData#appl_data.name,
      ......
      % 交給application_master來啟動
      case application_master:start_link(ApplData, Type) of
        {ok, _Pid} = Ok ->
          Ok;
        {error, _Reason} = Error ->
          throw(Error)
      end
  end.
 
 % application_master.erl
start_link(ApplData, Type) ->
  Parent = whereis(application_controller),
  proc_lib:start_link(application_master, init, [Parent, self(), ApplData, Type]).
 
 start_it_old(Tag, From, Type, ApplData) ->
  {M, A} = ApplData#appl_data.mod,
  case catch M:start(Type, A) of
    {ok, Pid} ->
      % 啟動成功了,默認狀態(tài) State = []
      link(Pid),
      From ! {Tag, {ok, self()}},
      loop_it(From, Pid, M, []);
    {ok, Pid, AppState} ->
    % 啟動成功了,默認狀態(tài) State = AppState
      link(Pid),
      From ! {Tag, {ok, self()}},
      % 啟動成功,自己loop進入主循環(huán)
      loop_it(From, Pid, M, AppState);
    {'EXIT', normal} ->
      From ! {Tag, {error, {{'EXIT', normal}, {M, start, [Type, A]}}}};
    {error, Reason} ->
      From ! {Tag, {error, {Reason, {M, start, [Type, A]}}}};
    Other ->
      From ! {Tag, {error, {bad_return, {{M, start, [Type, A]}, Other}}}}
  end.
% 另一個入口,和上面幾乎一樣的邏輯
 start_supervisor(Type, M, A) ->
  case catch M:start(Type, A) of
    {ok, Pid} ->
      {ok, Pid, []};
    {ok, Pid, AppState} ->
      {ok, Pid, AppState};
    {error, Reason} ->
      {error, {Reason, {M, start, [Type, A]}}};
    {'EXIT', normal} ->
      {error, {{'EXIT', normal}, {M, start, [Type, A]}}};
    Other ->
      {error, {bad_return, {{M, start, [Type, A]}, Other}}}
  end.

啟動之后的proc之間的拓撲圖如下:

|application_controller | --- |(application_master:main_loop)| --- | (application_master:loop_it)| --- | chapp_sup| 

到此,我們已經(jīng)將啟動的代碼流程走了一遍,一些回調參數(shù)也已經(jīng)很清楚。總結成以下幾點

  • application_controller 才是真正啟動appcationprocess,或者叫父進程(process)
  • application啟動是異步的,啟動結束才將結果castapplication_controller
  • 啟動之前要先載入(load)

2.5 application如何停止(stop)?

% application_controller.erl
handle_call({stop_application, AppName}, _From, S) ->
  #state{running = Running, started = Started} = S,
  case lists:keyfind(AppName, 1, Running) of
    {_AppName, Id} ->
      {_AppName2, Type} = lists:keyfind(AppName, 1, Started),
      stop_appl(AppName, Id, Type),
      NRunning = keydelete(AppName, 1, Running),
      NStarted = keydelete(AppName, 1, Started),
      cntrl(AppName, S, {ac_application_stopped, AppName}),
      {reply, ok, S#state{running = NRunning, started = NStarted}};
    false ->
      case lists:keymember(AppName, 1, Started) of
        true ->
          NStarted = keydelete(AppName, 1, Started),
          cntrl(AppName, S, {ac_application_stopped, AppName}),
          {reply, ok, S#state{started = NStarted}};
        false ->
          {reply, {error, {not_started, AppName}}, S}
      end
% application_master.erl
stop(AppMaster) -> call(AppMaster, stop).

main_loop(Parent, State) ->
  receive
    ......
    Other ->
      NewState = handle_msg(Other, State),
      main_loop(Parent, NewState)
  end.

handle_msg({stop, Tag, From}, State) ->
  catch terminate(normal, State),
  From ! {Tag, ok},
  % 自己主動退出
  exit(normal);
  
  loop_it(Parent, Child, Mod, AppState) ->
  receive
     ......
    {'EXIT', Parent, Reason} ->
      % 在stop之前還可以自定義pre_stop階段
      % application_master:main_loop退出了, application_master:loop_it收到消息
      % 執(zhí)行退出邏輯
      NewAppState = prep_stop(Mod, AppState),
      exit(Child, Reason),
      receive
        {'EXIT', Child, Reason2} ->
          exit(Reason2)
      end,
      % stop 回調調用點
      catch Mod:stop(NewAppState);
       ......
    _ ->
      ......
  end.

至此,application的停止邏輯已經(jīng)分析完成,我們通過閱讀代碼還能找到一個文檔中沒有的hook:pre_stop

3.總結

本文通過實例加閱讀源代碼的方式,演示了一遍application的實現(xiàn),希望讓讀者加深對application的理解,為合理使用application打下堅實的基礎。

4.參考文獻:

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容