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點:
- 為了實現(xiàn)特定功能
- 可以復用
在我們的日常生活中,單項目多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才是真正啟動appcation的process,或者叫父進程(process) -
application啟動是異步的,啟動結束才將結果cast給application_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.參考文獻:
- http://erlang.org/doc/design_principles/applications.html
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_controller.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_master.erl
- https://github.com/erlang/otp/blob/master/lib/kernel/src/application_starter.erl