erlang

erlang 组件 application

2019-12-10  本文已影响0人  Alking

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. 为了实现特定功能
  2. 可以复用

在我们的日常生活中,单项目多application的场景比较少,我们平时写的最多的是module准确的说是回调模块Callback Module,也会有少量的进程状态模块Residence Module,前者只是一个回调函数,从一个状态切换到另一个状态,所以生命周期只在单process,很少与外界的process打交道,所以使用link,monitor这些进程间关系相对较少。反观application要考虑的方面比较多,不仅要考虑状态的正确与否,还会考虑到进程运行的异常与否,甚至会考虑到怎么来设计监控树来让程序保持健壮。

2. 如何自己实现一个application?

2.1 目录结构

─ ${application}
      ├── doc
      │   ├── internal
      │   ├── examples
      │   └── src
      ├── include
      ├── priv
      ├── src
      │   └── ${application}.app.src
      └── test

2.2 application 回调模块(callback module)

默认情况下是$APP_NAME_app,当然自己也可以在${application}.app.src自行定义,定义方法如下:

{application, $APP_NAME,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib
                 ]},
   % 请在这里自定义回调模块
   % 请在这里修改回调参数
  {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
}).
%% 启动的回调函数
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}}.

%% 停止的回调函数
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函数
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里面的参数,因为这个是我们回调的参数
  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} ->
      % 启动成功了,默认状态 State = []
      link(Pid),
      From ! {Tag, {ok, self()}},
      loop_it(From, Pid, M, []);
    {ok, Pid, AppState} ->
    % 启动成功了,默认状态 State = AppState
      link(Pid),
      From ! {Tag, {ok, self()}},
      % 启动成功,自己loop进入主循环
      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| 

到此,我们已经将启动的代码流程走了一遍,一些回调参数也已经很清楚。总结成以下几点

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收到消息
      % 执行退出逻辑
      NewAppState = prep_stop(Mod, AppState),
      exit(Child, Reason),
      receive
        {'EXIT', Child, Reason2} ->
          exit(Reason2)
      end,
      % stop 回调调用点
      catch Mod:stop(NewAppState);
       ......
    _ ->
      ......
  end.

至此,application的停止逻辑已经分析完成,我们通过阅读代码还能找到一个文档中没有的hook:pre_stop

3.总结

本文通过实例加阅读源代码的方式,演示了一遍application的实现,希望让读者加深对application的理解,为合理使用application打下坚实的基础。

4.参考文献:

上一篇 下一篇

猜你喜欢

热点阅读