Apache の provider 機構 - 他モジュールに移譲するしくみ

2008-11-13 追記: タイトルを変更しました

用語定義をしておきます。

consumer
provider の提供する情報を取得する役割をになう
provider
consumer の要求する情報を提供する役割をになう

「情報」というのは const void * 型の値1つ,なのでなんでもよいです。文字列定数でもいいですし*1関数ポインタでもいいです。たいていは(関数ポインタを内包した)構造体を登録/取得します。

関数ポインタなり関数ポインタを内包した構造体なりを「情報」としてうけわたすことができると何がうれしいかというと,「移譲」ができることです。あるモジュールで大枠のロジックをインプリメントし,他のモジュールで具象的な関数を登録してもらう,などすると,実装を切り替えることができます。

もちろん,同一のモジュールで consumer と provider を実装しても構いません。利用する関数の決定に一手間かかってしまいますが,のちのち他のモジュールで provider を置き換えることができるなど柔軟性がまします。

Apache で使われている providers

Apache 付属のモジュールの中で,以下のモジュールが provider 機構を利用しています。

Provider Group consumer provider
authn mod_auth_basic, mod_auth_digest, mod_authn_alias mod_authn_file, mod_authn_dbm, mod_authn_ldap, mod_authn_dbd, mod_authn_anon, mod_authn_alias
cache mod_cache mod_disk_cache, mod_mem_cache
proxylbmethod mod_proxy, mod_proxy_balancer mod_proxy_balancer
dav mod_dav mod_dav_fs (, mod_dav_svn)
dav-lock (mod_dav_svn) mod_dav_lock

コードを読んでいないので憶測まじりになりますが。

authn provider は,ユーザ名とパスワードをもとに認証を行う provider です。consumer がクライアントとユーザ名を agent とやりとりし,認証自体を provider になげている恰好です。provider にはたとえば生 file や DBM から認証情報を読み取って認証するものがあります。

cache provider は,要求されたドキュメントをキャッシュから取り出す provider です。Disk や Memory から取り出す provider が用意されており,切り替えることができます。

proxylbmethod provider は proxy のロードバランスを行う provider です。独自の provider を実装することでロードバランシングのアルゴリズムなどを自作して適用することができます。詳しくは mod_proxy_balancerに独自振り分けロジックを追加できる気がする | 眠る開発屋blog に譲ります。

dav provider や dav-lock についてはコードを読むのがめんどうなので省略します。付属モジュールだけだとファイルシステム上のリソースを DAV で提供することしかできませんが,mod_dav_svn モジュールを使うとバックエンドとして Subversion レポジトリ上のリソースを DAV で提供できるようになります。この切り替えを provider-consumer アーキテクチャで実現しているのですね。

インタフェース定義

インタフェース定義は include/ap_provider.h にあります。また実装は server/provider.c にあります。たいして行数もないですし,ロジックも難しくないので読んでみるのもいいでしょう。

3つの関数のみ定義されています。

ap_register_provider()
provider の定義
ap_lookup_provider()
provider の取得
ap_list_provider_names()
provider の列挙

いちおう各関数の仕様をみていきます。

/**
 * This function is used to register a provider with the global
 * provider pool.
 * @param pool The pool to create any storage from
 * @param provider_group The group to store the provider in
 * @param provider_name The name for this provider
 * @param provider_version The version for this provider
 * @param provider Opaque structure for this provider
 * @return APR_SUCCESS if all went well
 */
AP_DECLARE(apr_status_t) ap_register_provider(apr_pool_t *pool,
                                              const char *provider_group,
                                              const char *provider_name,
                                              const char *provider_version,
                                              const void *provider);

group, name, version の3つのキーで一意となるように provider を登録します。provider を提供するモジュールで呼び出します。

基本的に group でインタフェースの「機能」を一意に定め,各モジュールがユニークな name で登録します。そして httpd.conf の設定子で name を指定する,という使われ方が多いようです。

たとえば,cache provider-consumer インタフェースだと,

  • mod_mem_cache が memfd という name の provider を登録
  • mod_disk_cache が disk という name の provider を登録

して,[http://httpd.apache.org/docs/2.2/mod/mod_cache.html#cacheenable:title=CacheEnable] 設定子で provider を指定する形になっています。

version というとバージョンコントロール(上位バージョンは下位バージョンに対して上位互換性をもつなど)がありそうなイメージですが,とくにそのような機構は用意されていません。必要ならば consumer と provider でそのような機構をインタフェース仕様を規定・実装することになるでしょう。

version は 0 スタートの数値文字列("0" 等)を利用するのが慣例のようです。特に決まりはありませんが。

/**
 * This function is used to retrieve a provider from the global
 * provider pool.
 * @param provider_group The group to look for this provider in
 * @param provider_name The name for the provider
 * @param provider_version The version for the provider
 * @return provider pointer to provider if found, NULL otherwise
 */
AP_DECLARE(void *) ap_lookup_provider(const char *provider_group,
                                      const char *provider_name,
                                      const char *provider_version);

group, name, version の3つのキーで一意となる provider を取得します。consumer モジュールで呼び出します。

/**
 * This function is used to retrieve a list (array) of provider
 * names from the specified group with the specified version.
 * @param pool The pool to create any storage from
 * @param provider_group The group to look for this provider in
 * @param provider_version The version for the provider
 * @return pointer to array of ap_list_provider_names_t of provider names (could
 be empty)
 */

AP_DECLARE(apr_array_header_t *) ap_list_provider_names(apr_pool_t *pool,
                                              const char *provider_group,
                                              const char *provider_version);

指定した group, version にマッチする provider の name を列挙します。

Apache 付属モジュールでまともに使われている例はありませんが,Hooks 機構のように group に登録された provider をすべて呼び出すようなメカニズムを利用してみてもおもしろいかもしれませんね。

サンプルモジュール

とまあ座学だけでは退屈なので,実際に provider 機構を利用したモジュールを作ってみましょう。

  • consumer は2つの数値を「演算」する大枠のアルゴリズムを提供する
  • provider として具体的な「演算」(add, subtract など)を実装する

という役割分担です。

consumer の実装

まずは consumer(mod_consumer)の実装から。

/* 
**  mod_consumer.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "http_protocol.h"
#include "ap_config.h"
#include "ap_provider.h"
#include "apr_strings.h"

/* Provider Definitions */

typedef int (*arithmetic_method)(int arg1, int arg2);

/* Configuration */

typedef struct consumer_conf {
    const char *method_name;
} consumer_conf;

static void *
create_dir_config(apr_pool_t *p, char *dir)
{
    consumer_conf *conf
        = (consumer_conf *) apr_pcalloc(p, sizeof(consumer_conf));
    return conf;
}

static const char *
set_arithmetic_method(cmd_parms *cmd, void *mconfig, const char *arg)
{
    consumer_conf *conf = (consumer_conf *) mconfig;

    conf->method_name = apr_pstrdup(cmd->pool, arg);

    return NULL;    /* NO ERROR */
}

static const command_rec consumer_commands[] = {
    AP_INIT_TAKE1("ArithmeticMethod", set_arithmetic_method, NULL,
                  ACCESS_CONF | RSRC_CONF,
                  "Set method name for calculation"),
    { NULL }
};

/* Content Handler */

module AP_MODULE_DECLARE_DATA consumer_module;  /* declaration */

static int
consumer_handler(request_rec *r)
{
    consumer_conf       *conf;
    arithmetic_method   provider;
    int                 result;

    if (strcmp(r->handler, "consumer"))
        return DECLINED;

    conf = (consumer_conf *) ap_get_module_config(r->per_dir_config,
                                                  &consumer_module);

    if (! conf || ! conf->method_name)
        return HTTP_INTERNAL_SERVER_ERROR;

    provider = ap_lookup_provider("arithmetic", conf->method_name, "0");

    if (! provider)
        return HTTP_INTERNAL_SERVER_ERROR;

    result = (provider)(6, 2);

    r->content_type = "text/plain";      
    ap_rprintf(r, "%s(6, 2) = %d\n", conf->method_name, result);

    return OK;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_hook_handler(consumer_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA consumer_module = {
    STANDARD20_MODULE_STUFF, 
    create_dir_config,      /* create per-dir    config structures */
    NULL,                   /* merge  per-dir    config structures */
    NULL,                   /* create per-server config structures */
    NULL,                   /* merge  per-server config structures */
    consumer_commands,      /* table of config file commands       */
    register_hooks          /* register hooks                      */
};

最初にも書いたように通常 provider data としては構造体へのポインタをうけわたすことが多いのですが,今回は演算を行う関数ポインタ自身を provider data としています。

    provider = ap_lookup_provider("arithmetic", conf->method_name, "0");

のように,ArithmeticMethod 設定子で設定された name の provider を取得しています。group は arithmetic,version は 0 です。

これで provider は関数ポインタ自身ですから,

    result = (provider)(6, 2);

のようにして演算を provider におこなってもらいます。演算の引数は 6 と 2 の決め打ちw

provider の実装

まずは加算をおこなう provider,mod_arith_add の実装です。

/* 
**  mod_arith_add.c
*/ 

#include "httpd.h"
#include "http_config.h"
#include "ap_provider.h"

/* Provider Definitions */

typedef int (*arithmetic_method)(int arg1, int arg2);

/* Arithmetic Operation */

static int
arith_add(int arg1, int arg2)
{
    return arg1 + arg2;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_register_provider(p, "arithmetic", "add", "0", &arith_add);
}

module AP_MODULE_DECLARE_DATA arith_add_module = {
    STANDARD20_MODULE_STUFF, 
    NULL,                   /* create per-dir    config structures */
    NULL,                   /* merge  per-dir    config structures */
    NULL,                   /* create per-server config structures */
    NULL,                   /* merge  per-server config structures */
    NULL,                   /* table of config file commands       */
    register_hooks          /* register hooks                      */
};

先ほどの consumer の実装では,config を処理する必要があったのでどうしてもあるていどのコード量になってしまいましたが,こちらはとても短くおさめることができました。

add という name で arith_add() という関数ポインタを provider data として登録しています。

register_hooks で provider を登録するのが慣例のようです。登録された provider を管理するためにメモリプールが必要となります。このメモリプールは system wide に共通のものを利用しなくてはなりません((さもないと運が悪いと ap_lookup_provider() 時にセグフォってしまうはずです。))。ap_register_provider() ではこれを第一引数で与えているのですが,register_hooks が呼び出されたときの pool は server/main.c::main() で用意された global pool なのでたしかに適しているのでしょう。まぁ config stage でも global pool がわたされているのでそこで登録しても構いませんが。


さて同じように他の演算を行う provider を実装していきましょう。といっても実質下記の部分だけ書き換えれば OK です。

/* Arithmetic Operation */

static int
arith_sub(int arg1, int arg2)
{
    return arg1 - arg2;
}

/* Hook Registration and Module Settings */

static void
register_hooks(apr_pool_t *p)
{
    ap_register_provider(p, "arithmetic", "sub", "0", &arith_sub);
}

実行する

モジュールをビルドしてインストールしたうえで,下記のような設定*2でウェブサーバと立ち上げます。

$ cat /etc/httpd/conf/httpd.conf

...... snip snip snip ......

LoadModule consumer_module  modules/mod_consumer.so
LoadModule arith_add_module modules/mod_arith_add.so
LoadModule arith_sub_module modules/mod_arith_sub.so
LoadModule arith_mul_module modules/mod_arith_mul.so
LoadModule arith_div_module modules/mod_arith_div.so

<Location /arith>
    SetHandler consumer
    ArithmeticMethod add
</Location>

...... snip snip snip ......

これでリクエストを発行してみると……

$ wget -nv -O - http://localhost/arith 
add(6, 2) = 8
11:25:28 URL:http://localhost/arith [14/14] -> "-" [1]

おお,きちんと 6 + 2 を計算することができました。

このままだとつまらないので,httpd.conf を書き換えて,subtract 演算をおこなうようにしてみます。

$ sudo vi /etc/httpd/conf/httpd.conf

...... snip snip snip ......

<Location /arith>
    SetHandler consumer
    ArithmeticMethod sub
</Location>

...... snip snip snip ......

$ sudo /sbin/service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]

これで実行すると,

$ wget -nv -O - http://localhost/arith
sub(6, 2) = 4
11:26:07 URL:http://localhost/arith [14/14] -> "-" [1]

こんどは 6 - 2 を計算することができました。

consumer のロジックを修正することなく provider を選択することで任意の演算をおこなうことができるようになりました。

自力で APR の DSO ハンドリング関数を使ってもできるんじゃ?

できますけど,こちらのほうがより抽象化されてます(から利用が楽です)し,DSO を使っていない static linked された httpd でも利用可能なところが違うと思うです。

*1:極端な話キャストした数値定数でもいいはず。

*2:無駄に演算 provider のモジュールをわけましたが,今考えたら一つのモジュールにまとめてもよかったかもですね。