External Process もっと詳しく

http://d.hatena.ne.jp/yssk22/20090615#1245080242http://d.hatena.ne.jp/yssk22/20090708/1247078669 で、CouchDBURIから外部プロセスをたたき起こすExternal Processについて調べました。最近、あらゆるデータをCouchDBへ、というのが自分の中の流行なので、いろんなバッチスクリプトを書いているわけですが、いつもcronに登録し忘れたり、ファイルシステムイベントの検知スクリプトを起動し忘れたりして、だめだめな自分に気がつきます。

こうなると、CouchDBのExternal Processと組み合わせて使いたくなる。つまり、バッチ系のスクリプト自動起動するように、external processをしこんでおいて、CouchDBへのアクセスがあったら、裏側で_external を蹴って帳尻を合わせよう、という戦略です。

  • External Process は /{db}/{proc_key} で起動でき、HTTP Request を受け取ることができる。
  • External Process は1度に1つのプロセスしか処理しない。

というのがすでにわかっています。もう少しExternal Processのプロセスモデルについて調べてみますが、これ以降は動作を調べるよりもソースを読んだ方が確実そうなので、ソースを読みます。以下 CouchDB 0.10.1 のソースから。


まずは設定を確認。

[httpd_db_handlers]
_proc1 = {couch_httpd_external, handle_external_req, <<"proc1">>}

こんな感じでプロセスを定義するので、couch_httpd_external.erl を調べることにします。

-export([handle_external_req/2, handle_external_req/3]).

ということで、handle_exernal_req を見ます。/2 は後方互換のためのものなので、ここでは/3のほうを見ます。

% handle_external_req/3
% for this type of config usage:
% _action = {couch_httpd_external, handle_external_req, <<"action">>}
% with urls like
% /db/_action/design/name
handle_external_req(HttpReq, Db, Name) ->
    process_external_req(HttpReq, Db, Name).

process_external_req/3 を見ます。すぐ下に書いてあります。

process_external_req(HttpReq, Db, Name) ->

    Response = couch_external_manager:execute(binary_to_list(Name),
        json_req_obj(HttpReq, Db)),

couch_external_manager というのを使ってプロセスを管理しているようです。json_req_obj は、単にプロセスに渡すためのJSONオブジェクトを構築する関数なので省略。couch_external_manager.erl に移動します。

execute(UrlName, JsonReq) ->
    Pid = gen_server:call(couch_external_manager, {get, UrlName}),
    case Pid of
    {error, Reason} ->
        Reason;
    _ ->
        couch_external_server:execute(Pid, JsonReq)
    end.

couch_external_manager からErlang側の管理プロセスの取得

external_manager を使ってErlangのプロセスのPidを取得して、execute させるようです。{get, UrlName} で取得しているので、handle_call({get, UrlName}, ...) となっている関数を見ます。

handle_call({get, UrlName}, _From, Handlers) ->
    case ets:lookup(Handlers, UrlName) of

まず、etsを使ったHandlersというハッシュテーブルから、UrlName にマッチするものをlookupして、

    [] ->
        case couch_config:get("external", UrlName, nil) of
        nil ->
            Msg = lists:flatten(
                io_lib:format("No server configured for ~p.", [UrlName])),
            {reply, {error, {unknown_external_server, ?l2b(Msg)}}, Handlers};
        Command ->
            {ok, NewPid} = couch_external_server:start_link(UrlName, Command),
            true = ets:insert(Handlers, {UrlName, NewPid}),
            {reply, NewPid, Handlers}
        end;

まだなければ、構成ファイルから作成して(Erlangの)プロセスを作る。couch_external_server というモジュールのプロセスを作ります。

    [{UrlName, Pid}] ->
        {reply, Pid, Handlers}
    end;

もう作られているようであれば、それを返す。ということで、External プロセスの設定キー1個につき、1つの couch_external_server (Erlangプロセス)が起動されることが保証されているようです。 couch_external_server.erl を見に行きます。

start_link(Name, Command) ->
    gen_server:start_link(couch_external_server, [Name, Command], []).

start_link は単にgen_server:start_linkのラッパーなので init([Name, Command])を見に行きます。

init([Name, Command]) ->
    ?LOG_INFO("EXTERNAL: Starting process for: ~s", [Name]),
    ?LOG_INFO("COMMAND: ~s", [Command]),
    Timeout = list_to_integer(couch_config:get("couchdb", "os_process_timeout",
        "5000")),
    {ok, Pid} = couch_os_process:start_link(Command, [{timeout, Timeout}]),
    couch_config:register(fun("couchdb", "os_process_timeout", NewTimeout) ->
        couch_os_process:set_timeout(Pid, list_to_integer(NewTimeout))
    end),
    {ok, {Name, Command, Pid}}.

タイムアウトを構成ファイルから取得して、プロセスを起動しています。couch_os_process は、View などからJavaScriptプロセスを起動するときと同じです。こんな感じで、プロセスが1個作成されることが(Erlang+Erlang/OTPで)保証されています。

Erlang側の管理プロセスとExternalプロセスのやりとり

これは簡単で、couch_os_process なので、単にJSONを標準入出力でやりとりするだけでしょう。実際、couch_external_server.erl を見ると、

execute(Pid, JsonReq) ->
    gen_server:call(Pid, {execute, JsonReq}, infinity).

となっていて、このPidはcouch_os_processのPidなので、Viewでの処理と同じです。標準入出力でJSONをやりとりする。