Ethna CacheManager キャッシュが見つからないときとか

Ethnaクラスには下記のようなエラー生成メソッドが定義されている。

  • &raiseError() - E_USER_ERROR
  • &raiseWarning() - E_USER_WARNING
  • &raiseNotice() - E_USER_NOTICE

キャッシュが見つからないとか、ライフタイムが過ぎたという状況でraiseError()を返すけど、どうなんだろう?
ロギングするときにlog_level=>warningとかだとガンガン引っかかるし、キャッシュというものの特性上、raiseWarning(), いや、raiseNotice()でもいい気がする,

Ethna CacheManager 比較

Ethnaにはデータキャッシュ用のプラグインがあります。効率よく使うとパフォーマンスを劇的に改善してくれるアレです。

  • Ethna_Plugin_Cachemanager_Memcache
  • Ethna_Plugin_Cachemanager_Localfile

がありますが、今までずっとEthna_Plugin_Cachemanager_Localfileばっかり使っていたので、memcachedを使うとどんだけ早いんだろー?と以前からワクワクしてたので、Ethna_Plugin_Cachemanager_Memcacheを使ってみました。
ちなみに、set時でなくget時にライフタイム指定ができるあたりが結構気に入ってます。

memcachedを入れてみる

面倒だから今回はrpmで突っ込んでみる。

# wget http://dag.wieers.com/rpm/packages/libevent/libevent-1.3b-1.el4.rf.i386.rpm
# wget http://dag.wieers.com/rpm/packages/memcached/memcached-1.2.1-3.el4.rf.i386.rpm

localfileのyumインストールでGPGKEYのチェックで怒られたので/etc/yum.confでgpgcheckを外しておく。(終わったら戻しておく)

gpgcheck=0

インストール

# yum localinstall libevent-1.3b-1.el4.rf.i386.rpm
# yum localinstall memcached-1.2.1-3.el4.rf.i386.rpm

PECLのmemcacheをインストール

# pecl install memcache

あとはphp.iniに

extension=memcache.so

を追加すればOK。これで使えるようになった。

Ethna上で設定

BASE/etc/appid-ini.phpの下のほう

    'memcache_host' => 'localhost',
    'memcache_port' => 11211,
    'memcache_use_connect' => false,
    'memcache_retry' => 3,
    'memcache_timeout' => 3,

etcのサンプルの記述を見るに、namespace毎に複数のmemcacheサーバを指定できるみたい。今回はlocalhost1台のみ。
てか、Ethna_Plugin_Cachemanager_Memcacheのソースを見てたら、localhost:11211なら書かなくても繋いでくれるみたい。

LocalfileとMemcacheで比較

検証のためアクションにベタ書き。
意味は無いけどガンガンキャッシュにアクセス。

require_once('Benchmark/Timer.php');
class Appid_Action_Memcache extends Ethnalab_ActionClass
{
    function perform()
    {
        $type = 'Memcache'; // or 'Localfile'
        $key_skel = 'foobar_%03d';
        $namespace = 'foo';
        $lifetime = 1000;
        $cm =& $this->backend->plugin->getPlugin('Cachemanager', $type);

        $bm =& new Benchmark_Timer();
        $bm->start();
        for ($i = 0; $i < 10; $i++) {
            for ($j = 0; $j < 1000; $j++) {
                $key = sprintf($key_skel, $j);
                $r = $cm->get($key, $lifetime, $namespace);
                if (Ethna::isError($r)) {
                    $cm->set($key, $j, null, $namespace);
                }
            }
        }
        $bm->stop();
        $bm->display();
        exit;
    }
}
結果 lifetime 1000
  • Memcache
    • 6.0970849990845
    • 6.269326210022
    • 6.3600161075592
  • Localfile
    • 5.1359589099884
    • 5.2926080226898
    • 5.2810370922089

アレ!?Localfileの方が早いよ!

キャッシュが頻繁に更新されたら違うかもしれない。$lifetimeを1に。

結果 lifetime 1
  • Memcache
    • 16.544981956482
    • 8.3540189266205
    • 7.2866940498352
  • Localfile
    • 10.868150949478
    • 10.440382957458
    • 9.8940320014954

なんだかMemcacheは結果が安定しない。この後何回やってもバラつきは変わらず。
1プロセスで検証しても意味無いかと思って、複数コネクション張ってみたり、保存するデータをもう少しボリュームあるものに変更してみても結果に大差はなかった。
結果的にこれは、「やるじゃん!Localfile!」なのか「どうした!?Memcache!」なのかわかんない。

まとめ

まあ、memcachedは入れたままデフォな状態だし、(設定とかでなんとかなるのか?)キャッシュで大事なのはキャッシュ自体のスピードっていうよりは、キャッシュすることなわけなので別にいいのかも。

サーバが複数台あるような環境で考えるとmemcachedを使う利点ってのがいっぱいわかるけど、1台のサーバで使う程度の規模で単純なデータのキャッシュ程度なら無理してmemcachedを入れなくても、ファイルキャッシュで無問題なのかなぁと思った。

EthnaでUnit Test その2

SimpleTestのassert〜〜ってのは全て、独自のメッセージを渡せるようになってます。
例えば以下のように引数を渡すと

function test_foo()
{
    $num = 0;
    /*
     * 何か処理
     */
    $this->assertIdentical($num, 0, 'numの値が0であるか検証');
}
numの値が0であるか検証 at  [ファイルパス line 行]

と実行時に表示してくれます。assertが増えてきた場合に識別しやすくて便利ですね。


でも、メッセージを渡すとちょっと問題が出てきます。
例えば、$numの値が0では無かった場合に

Fail numの値が0であるか検証 at  [ファイルパス line 行]

としか出力されず、え?じゃあ、値はいくつだったの?ってことが多々あります。


デフォは比較した値を表示してくれるんですが、メッセージを渡すとそれが消えちゃうんですね。
SimpleTestのunit_tester.phpを見てもらうとわかると思いますが、assert〜〜は、

function assertIdentical($first, $second, $message = '%s') {

といった感じなので、デフォルト引数の%s*1を上書きしちゃって元メッセージが表示されなくなっちゃいます。
例えば、'numの値が0であるか検証 (%s)' といった感じで渡してやれば共存できるんですが、何度もそんなことを書くのは面倒くさいと。
SimpleTestのassert〜〜は最終的にSimpleTestCaseクラスのassertにたどり着くっぽいので、こいつを無理やりオーバーライドしてやれば、自分の独自メッセージ + デフォルトメッセージを共存させることができます。

前記事で作成したAppid_UniteTestCaseに

function assert(&$expectation, $compare, $message = '%s') {
    return parent::assert($expectation, $compare, $message == '%s' ? $message : "{$message} (%s)");
}
Fail numの値が0であるか検証 (Identical expectation [Integer: 1] fails with [Integer: 0]
because [Integer: 1] differs from [Integer: 0] by 1) at [ファイルパス line 行]

といった感じで追加してやれば自分の好きなようにある程度変更できます。第3引数が気持ち悪いことになってますが…。
「もっと凝ったメッセージを渡したいんだ!」と言う方は向かないかもしれませんが、「確認できればそれでOKだYo!」みたいな人はこんな感じで問題ないかなと。

*1: 最終的にsprintfでメッセージに置き換えられる

EthnaでUnit Test その1

EthnaでのUnit Testに関する情報があまり無い気がするので、
以前まで抱えていたEthnaで作っていたプロジェクトで培ったTDDのノウハウと気をつけたい事についてちょこっと書きたいと思います。
あ。環境はPHP4で、EthnaCVS版がベースでの話です。

設定の方法等は公式を参照してください。
http://ethna.jp/ethna-document-dev_guide-misc-unittest.html

SimpeTestに関してはWEB+DB PRESSの29号にわかりやすい特集が掲載されています。
http://www.gihyo.co.jp/magazines/wdpress/archive/Vol29

はじめに

Ethnaではテストツールとして、SimpleTestというものが使われています。
CakePHPSymfonyでも採用されているみたい。

僕自身、テスト駆動開発Ethnaを使って行うのが初めてだったので、
PhpUnitやPhpUnit2といったものは使ったことがありません。
なので他のテストツールと比較はできないです。ゴメンナサイ。

いきなり拡張

SimpleTestのassert類って、ちょっとイケてないよねーって感じることがあったので独自assertを作ったほうが便利。
まず、Ethna_UnitTestCaseを継承したAppid_UnitTestCaseを作りましょう。
後々、共通したプロパティやメソッドが欲しくなることが多いです。後々書きます。

アクション、ビュー以外のテストケース

Ethnaのデフォではadd-action-test,add-view-testといったコマンドしか用意されてませんが、
AppManagerやAppObject、Pluginなんかに対してもテストを書くことができます。
個人的にそういったロジックレベルでのテストケースのほうがよく書くし、大事かなぁとか思います。

そういったのを簡単に作れるEthnaコマンドがあれば*1便利だなぁ…。

テストケースの数が増えると大変なのでappの下にtestといったディレクトリを作成し、

みたいにAppid_UnitTestCaseを継承したテストケースを置いて、

Appid_UnitTestManager.php内にテストケースを読み込むように記述すればOKです。
先ほど作成したAppid_UnitTestCase.phpもここで読み込んじゃえばOKです。

include_once 'Appid_UnitTestCase.php';

class Appid_UnitTestManager extends Ethna_UnitTestManager
{
    var $testcase = array(
        'Hoge' => 'app/test/Hoge_TestCase.php',
    );
}

テストケースのクラスの命名規則は、キー名_TestCaseとなってるようです。

メソッド

SimpeTestのassertで引っかかった点として、

assertTrue()

Trueって名前だら

if ($hoge === true)

って比較をするのかと思ったら内部では最終的に、

if ((boolean)$hoge)

みたいな比較をしやがるので、オブジェクトをassertTrue()に突っ込んでも青になっちゃいます。

僕が設計するロジックって、成功時はtrue,駄目なときは
それに見合ったErrorやWarningをraiseすることが多いのですが、
以下のような時に困ります。

Hoge_Managerでfooというメソッドがあった場合に、

/**
 * 何か処理を行う
 * 
 * @access public
 * @param int $n 処理結果に関わる何らかの数字
 * @return mixed(TRUE: 処理成功: EthnaError: 処理失敗)
 */
function foo($n)
{
	return $n === 1 ? true : Ethna::raiseError('1以外は駄目だよ');
}

Hoge_TestCaseでfooメソッドに対する以下のようなテストケースがあった場合、両方とも青になってしまいます。

function test_foo()
{
    $m =& $this->backend->getManager('Hoge');
    $this->assertTrue($m->foo(1));	
    $this->assertTrue($m->foo(2));
}

同じようにassertFalseに空のarrayや0等を突っ込んでも青になります。
SimpleTestをガンガン使い出すと結構困ります。

独自assertの作成
$this->assertTrue($m->foo(1) === true);

とかすればまあ大丈夫なのですが、毎回毎回そんなのを書くのは面倒ですね。
なので独自のassertを作成しましょう。
SimpleTestの構造上、本格的にassertを追加するのは面倒くさそうだったので必要最低限ですむようにします。

ここで、さっき作ったAppid_TestCaseにメソッドを追加します。
例として、引数がEthnaErrorなのかどうかを見極めるメソッドを作ってみます。

Appid_UnitTestCaseに

    /**
     * 引数がEthna_Errorか検証する
     * 
     * @access public
     * @param mixed $object チェックする値      
     * @param string $message メッセージ.
     * @return boolean (TRUE: Ethna_Errorオブジェクト, FALSE: Ethna_Errorオブジェクトでない)
     */
    function assertEthnaError($object, $message = '%s')
    {
        $this->assertTrue(
            Ethna::isError($object) === true,
            sprintf($message, " assertion got Ethna_Error")
        );
    }
    
    /**
     * 引数がEthna_Errorではないか検証する
     * 
     * @access public 
     * @param mixed $object チェックする値
     * @param string $message メッセージ.
     * @return boolean (TRUE: Ethna_Errorオブジェクトでない, FALSE: Ethna_Errorオブジェクト)
     */
    function assertNotEthnaError($object, $message = '%s')
    {
    	$this->assertTrue(
            Ethna::isError($object) === false,
            sprintf($message, " assertion got not Ethna_Error")
        );
    }

というのを書けば、各テストケースで下記のように記述できます。

function test_foo()
{
    $m =& $this->backend->getManager('Hoge');
    $this->assertNotEthnaError($m->foo(1));	
    $this->assertEthnaError($m->foo(2));
}

SimpleTestは型比較系に弱い気がするので、
必要に応じてassert等をどんどん作っていけば便利になりますよ。


と、今回はここまでです。間違い等あればごめんなさい。

*1:近いうちBoBppさんがリリースしてくれるような気がします。(ムチャフ