子プロセス制御ふたたび : PHP Advent Calendar jp 2011 Day 8

はい、7日目の @ さんのエントリ「DateTimeクラスの落とし穴と対策 : PHP Advent Calendar jp 2011 Day 7」から引き続いて、PHP Advent Calendar jp 2011の8日目なわけです。


今回は何を書こうかずいぶん悩んで、ちょうど昨晩開催されたPHP忘年会2011@関東でネタ募集したところ、@さんが口走ったphpQueryネタをパクるという案もあったのですが、やはり正攻法で手持ちのネタでいくことにしました。

子プロセスfork

このはてなダイアリーでの数少ないPHPヒットネタとして「pcntl extensionを使って一定個数の子プロセスに作業させる方法 - Blog::koyhoge」という記事がありまして、公開した2007年以来ぼちぼちとずっとアクセスを稼いでくれております。


ただこの時に書いたサンプルコードも、話を単純にするためにグローバル空間にベタ書きですし、今となってはあまり良いサンプルとは呼べません。実は今年になって、とある目的できちんとクラス化したものが作ったのでそれを紹介しようと思います。

きっかけは Amazon SQS+SDB のワーカー

その目的というのは、AMN の広告配信ログサーバを改善する際の途中成果として作成した、キューからデータを取ってきて DB に入れるワーカー処理です。11月15日に開催された「第7回 MongoDB 勉強会 in Tokyo」で発表した以下のスライドで、そこに至る背景と経緯を説明しています。

ProcessForker クラス

では早速コードを見ていきましょう。

<?php
class ProcessForker {
    // defaults
    protected $_options = array(
        'max_children' => 10,  // max number of child processes
        'process_limit' => 0,  // return when this count tasks are finished
        'loop_task' => false,  // reuse tasks
        'sleep' => 0, // microseconds
        'single_execution' => null,
        );

    protected $_idx_task = 0;

    public function __construct($options = null) {
        if (!empty($options)) {
            $this->_options = array_merge($this->_options, $options);
        }
    }

    public function fetchTask(&$tasks) {
        if ($this->_options['loop_task']) {
            $idx = $this->_idx_task;
            $task = $tasks[$idx];

            // circulation in tasks
            ++$idx;
            if ($idx >= count($tasks)) {
                $idx = 0;
            }
            $this->_idx_task = $idx;
        } else {
            $task = array_shift($tasks);
        }
        return $task;
    }

    public function run($tasks) {
        // number of current running child processes
        $nchildren = 0;
        // number of finished task
        $nfinished = 0;
        for (;;) {
            if (empty($tasks)) {
                break;
            }
            $maxproc = $this->_options['process_limit'];
            if (($maxproc > 0) && ($maxproc <= $nfinished)) {
                break;
            }
            if ($nchildren <= $this->_options['max_children']) {
                $task = $this->fetchTask($tasks);

                $pid = pcntl_fork();
                if ($pid === -1) {
                    throw new Exception('pcntl_fork faild');
                } else if ($pid) {
                    // parent process
                    ++$nchildren;
                } else {
                    $exit_code = 0;
                    // child process
                    $func = $task[0];
                    $arg = $task[1];
                    if (!is_array($arg)) {
                        $arg = array($arg);
                    }

                    try {
                        call_user_func_array($func, $arg);
                    } catch (Exception $e) {
                        $exit_code = -1;
                    }
                    // care singleExecution:
                    // child process must not unlock
                    $se = $this->_options['single_execution'];
                    if ($se !== null) {
                        $se->setDoUnlock(false);
                    }
                    exit($exit_code);
                }

                $sleep = $this->_options['sleep'];
                if ($sleep > 0) {
                    usleep($sleep);
                }
            } else {
                $pid = pcntl_waitpid(0, $status, 0);
                --$nchildren;
                ++$nfinished;
            }
        }
    }
}

前回は決め打ちだった各種パラメータや実際に実行する処理を、すべて外部から渡せるようにしてあります。

使用例

この ProcessForker クラスは以下のように使用します。

<?php
require_once 'ProcessForker.php';

function hoge($str) {
    srand();
    $rand = rand(1, 5);
    echo "$rand: $str\n";
    sleep($rand);
}
$opts = array(
    'max_children' => 4,
    'process_limit' => 50,
    'loop_task' => true,
    );
$pf = new ProcessForker($opts);

$tasks = array(
    array('hoge', 'a'),
    array('hoge', 'b'),
    );
$pf->run($tasks);

まずはオプションを指定してProcessForkerオブジェクトを作ります。それぞのれオプションの意味は以下になります。

max_children 子プロセスの最大同時実行数
process_limit いくつのタスクを処理したら終了するか
loop_task 渡されたタスクを繰り返すかどうか
sleep 親プロセスが処理を行うたびにスリープする時間(us)
single_execution singleExecutionオブジェクト(後述)

この例では、

  • 最大4つの子プロセスを起動し (max_children => 4)
  • 全部で50回の処理を行い (process_limit => 50)
  • タスクを繰り返しながら (loop_task => true)

実行するという意味になります。

タスク指定

子プロセスとして処理するタスクは、配列の形で渡します。

array(<callable>, <引数>)

callablePHPis_callable() でtrueとなるもので、以下のどれかになります。

タスクへの引数は、ひとつの場合はそのままで構いませんが、複数の場合はそれも配列にして渡します。

繰り返し処理

loop_task オプションが true になっていると、渡されたタスクをすべて処理し終えてても終了せずに、またタスクリストの先頭から処理を行います。同じ処理を何度も繰り返したいときは、タスクを1つだけ用意して loop_task を true にすれば良いです。

single_execution 対応

常にプロセスが動いていて欲しい場合は cron で毎分キックするということをよくやりますが、しかし大元の親プロセスが複数立ち上がってしまうのは好ましくありません。そこで @ さんが作られた single_execution を利用して、同時起動チェックを行いました。しかし単に ProcessForker と single_execution を組み合わせるだけではどうも上手く動いてくれません。


調べたところ ProcessForker から起動された子プロセスが、プロセス終了時に singleExecution の管理しているロックファイルを削除してしまっていることが解りました。single_execution はそこから更に子プロセスが fork されることなど想定していないので当然ですね。

ということで、single_execution.php にパッチを当ててプロセス終了時のロック解除を選択できるように変更しました。ProcessForker 作成時の single_execution オプションに singleExecution オブジェクトを渡せば、子プロセスの場合はロックを解除せずに終了するようになっています。改変した single_execution.php は以下に置いてあります。

https://gist.github.com/1446922

これですでにプロセスが動作しているかどうか気にせずに、cronからスクリプトを定期的に起動するだけで、複数プロセスで動的に処理を行なってくれるようになりました。


明日の Advent Calendar は @takuya_1st さんです。

TwigからEntity::findするフィルターを書いてみた (Symfony Advent Calendar JP 2011 2日目)

初日の @brtriver さんに引き続き Symfony Advent Calendar JP 2011 の2日目です。


ここ数ヶ月 Symfony2 を触っています。実は Symfony 1.X の頃は興味ありながらもほとんど触っていなくて、2になってから触り始めたビギナー&ニューカマーなんです。諸先輩方よろしくお願いします。

Symfony Advent Calendar JP 2011ができるまで

昨年はSymfony Advent 2010をやっているのは知ってたので、今年はどうするのかなーやらないのかなー? と思って

とつぶやいたら、さっそく@ さんからリプライがあって、という流れで、何故か新人なのに取りまとめ役をやることになりました。不用意につぶやくと話が転がって面白い事になるという良い例です(笑)

Twig フィルターを書いてみた

プロパティの動的参照

さて本題です。Symfony2 の標準テンプレートエンジンの位置にある Twig ですが、開発しているのが Symfony の作者である Fabien さんだけあって、View としての役割がきっちり守られていて、余計なことはできないようになっています。


例えばオブジェクトのプロパティを動的に取得するようなことも、現在 Symfony2 にバンドルされている Twig 1.1.2 ではできません。Github Issues で話題になった時も Fabien は

This feature won't be included in Twig. Twig is a template system, not a fully-features language. Dynamic properties is clearly out of Twig scope.
訳: それは Twig の機能として含まれるべきではないよ。Twig はただのテンプレートシステムでフル機能の言語じゃないんだ。動的なプロパティ参照は Twig のスコープ外だよ。

https://github.com/fabpot/Twig/issues/41

と答えています。(その後考え直したのか Twig 1.2 から attribute 関数が導入されて、オブジェクトのプロパティを動的に取得できるようになりました。)

Entityから動的に取得する Twig フィルター

さて今回作成した Twig Filter は、Entity に対して検索をして値を取得するという、上記をさらに一歩踏み越えたある種のキワモノです。ロジックは Model や Controller でという原則からは外れるので安易に使うべきではありません。とは言えそういうキワモノを作るにはそれなりの理由もあるのです。

たとえばユーザの属性情報を考えてみましょう。ユーザはたくさんの属性情報を持っている場合があります。そのたくさんの属性のうちどれをどうやって表示するかを決めるのは View ですが、それを実際に表示するには Model や Controller 側でお膳立てをしておく必要があります。あるアイテムの所有者としてユーザ情報を表示するときに、そのユーザ情報の種類がすごく多かったり、直接 JOIN できなかったりする場合、M/C 側で全てを準備することは逆に現実的ではないと思うのです。

Symfony2 で Twig Extension

ということで、こんな Twig フィルターを作りました。

<?php
namespace Koyhoge\HogeBundle\Twig;
class HogeExtension extends \Twig_Extension {
    public function getFilters() {
        return array(
            'entity' => new \Twig_Filter_Method($this, 'entityFilter'),
            );
    }
    public function entityFilter($entity, $id, $prop, $by = null) {
        if (empty($by)) {
            $obj = $entity->find($id);
        } else {
            $objs = $entity->findBy(array($by => $id));
            $obj = $objs[0];
        }
        if ($obj == null) {
            return '';
        }
        $method = 'get' . ucfirst($prop);
        $call = array($obj, $method);
        if (!is_callable($call)) {
            throw new \Exception('Not callable:' . $method);
        }
        return call_user_func($call);
    }
    public function getName() {
        return 'hoge';
    }
}
使い方:Controller 側

Controller 側で以下のように Entity を直接 Twig に渡します。

<?php
namespace Koyhoge\HogeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class DefaultController extends Controller {
    public function indexAction() {
        $users = $this->getDoctrine()->getRepository('KoyhogeHogeBundle:User');
        $items = array(
            array('user_id' => 1),
            array('user_id' => 11),
        );
        return $this->render('KoyhogeHogeBundle:Default:index.html.twig',
                             array('users' => $users', 'items' => $items));
    }
}
使い方:Twig テンプレート側

テンプレート側では以下のように呼び出せます。

{% for item in items %}
  {{ users|entity(item.user_id, 'name') }}
{% endfor %}

{{ users|entity(item.user_id, 'name') }}

の部分で、User Entityから item.user_id で find() を行い、その結果のオブジェクトから name プロパティを取得する、という処理を行います。

他のキーで findBy()

別のユニークキーで findBy() したい場合は

{{ users|entity(item.other_id, 'name', 'other_key') }}

などのように3番目の引数に渡します。

まとめ

正直あまり実用的ではない tips ですが、View に必要なデータを Controller 側ですべてお膳立てするのはなんか違うなーと感じていたので、試しに実装して見ました。何にでも多用することは危険だと思われますが、デザインの都合によっていつ表示するか分からない基本的な情報をこのやり方で処理するのは、有効なのではないかと思っています。


明日は、今回の Advent Calendar 企画を私に振った張本人、日本の Symfony 界の重鎮 @hidenorigoto さんです。よろしくお願いします!

そろそろPHPMatsuriについて書いておくか

http://www.flickr.com/photos/zatsu/6260016709/
Photo CC by @


PHPMatsuri については「闇」で発表したスライドの記事を書いたのでそれでいいかとも思ってたんだけど、いやもっとちゃんとブログを書こうと思い直したばかりだったし、せっかくだからもう少し書いておきます。


大阪入りする前から今回の PHPMatsuri で何をするか考えていても何も浮かばなくて、実際にイベントが始まって一応とあるライブラリの extension を作ろうかと思いついたのだけど、さすがに準備が全然できてないので VirtualBoxCentOS 6.0 x86-64をインストールして環境をほげほげしてたら時間切れになってしまった。


まあそれでも、仕事で悩んでた Symfony2 の Bundle の設計について明確な回答をもらえたり、闇PHPMatsuri で Net_IPv6 に送ったパッチの発表をしたり、光画部活動でたくさん写真を撮ったりで、楽しかったし得るものもそれなりにありました。


TDD や CI など今日びの開発手法を多くの人達が使いこなしているのを見て、自分も負けてられないなぁと思いました。PHP は Web 開発業界では底辺に見られがちだけど、開発効率や信頼性を高めるべく新しい技術を貪欲に取り入れる人達もたくさんいます。PHP の言語仕様がダメダメな部分は分かった上で、素晴らしい設計をしたり素早く素晴らしいコードを書くことはできるし、それらを広めることで下らない苦労をする人が一人でも減れば PHP の可能性をもっと広げることにもなるのだと思います。


参加者はすでに日常に戻り日々のコードとの戦いに復帰しているのだと思われますが、年に一回くらいはああいう特別な場所で、熱い想いを持っている人達の空気にどっぷり浸かるのも、エンジニアとしてしての生き方には必要なのだろうと思います。


そう、終わってみて分かる。あれは「特別な場所」でした。来年の開催はまだ不明ですが、今年いろんな理由で参加を躊躇した人は一度は参加してみることを強くお薦めします。

2日目午前中の光画部活動

@ の「今年も撮影散歩行こうよー」という一言で、PHPMatsuri 会場の国際交流センターから、インテック大阪の横を通り、ATC まで撮影しながらぐるっと歩きました。


南港の辺りは埋立地ということもあって、どうしても風景がお台場とかぶりますね。今回の見た中にはすでに廃墟になりかけている建物もあったりして、10年後のお台場もひょっとしてこんな感じなのだろうかと感傷にふけれます。

http://www.flickr.com/photos/koyhoge/6249835479/in/set-72157627906825886
廃墟なりかけその1

http://www.flickr.com/photos/koyhoge/6250299669/in/set-72157627906825886
廃墟なりかけその2

その他の写真はこちらから。
20111016-osaka - an album on Flickr

帰京途中に寄った大阪駅ビルからの夜景がすごかった

http://www.flickr.com/photos/koyhoge/6253963357/in/set-72157627794038827

帰りは @ @ と一緒に帰ることになったのですが、二人とも新しい大阪駅の駅ビルをまだ見てないというので、ちょっと寄り道してノースゲートビルディングを観光してきました。
自分が前回来たときには大雨で見ることができなかった屋上農園も、今回はばっちり見学できました。

http://www.flickr.com/photos/koyhoge/6254519012/in/set-72157627794038827


前回はまだ骨組みだけだった駅北側のビル群も、今回見たらずいぶんと出来ていて、なんかもう物凄くなりそうな予感が今からプンプンします。楽しみ。


その他の写真はこちらから。
20111016-Osaka Station - an album on Flickr

闇PHPMatsuriで発表した

最初はネタ無いしなーと思ってたけど、Net_IPv6 に送ったパッチの話は Twitter にしか書いてなかったので、さくっとスライド作って発表した。

Slideshare は .key を直接アップロードできるようになったのは良いけど、font matrix がずれるみたいすね。

秋にはプログラム言語イベントの大トリに行くのだ

みなさんはもうとっくにご存知ですよね。再来週末にとてもエキサイティングなプログラム言語イベントが開催されます。

  • 2日間に渡る濃いセッションの数々
  • 国内外のハッカー達が多数集まる
  • 海外ゲストも参加

ここ数年は初夏から秋にかけて、プログラム言語系の大きなイベントがいくつか開催されていて、毎年楽しみな恒例行事となっています。LL イベントや RubyKaigi もそうですし、先日開催された PHP カンファレンスもその一つですね。PyCon JP はまだ参加したことはないですが、いつか参加したいと思ってます。そして今年もまたアレが開催されます。そう! YAPC::Asia Tokyo 2011 が!



えー、このエントリは PHPMatsuri リレーブログの4日目なので PHPMatsuri の事を書こうと思っていたのですが、同じキーワードがいくつかあったのでネタに走ってしまいました。


でも実際に YAPC::Asia Tokyo 2011 は、10/13(前夜祭), 10/14, 10/15 に開催されますし、前夜祭と1日目は私も参加するつもりで 1日券のチケットを購入済です。そして YAPC::Asia の2日目に後ろ髪をひかれながら大阪に移動して PHPMatsuri 2011 に参加する予定です。


予算的にちょっと辛いかもという話を聞いていたので、今回は我らがほげ技研もシルバースポンサーとして協賛させて頂いてます。


昨年の PHPMaturi 2010 では、自作の DB ライブラリの依存部分を整理して Keires_DB として openpear でリリースしたり、写真をたくさん撮ったり、会場近所の中央清掃工場の大煙突が激烈にかっこよくて感動したり、色んな人と話したりしました。100人近くの人が集まるハッカソンだけに、楽しみ方はひとそれぞれでそれも面白かったです。

さて今年の PHPMatsuri 2011、一泊4食で22,000円。確かに安くはないですが、もし参加しようかどうしようか迷っている人がいたら、他の技術イベントではなかなか経験できない貴重な機会がそこにあることをお伝えしておきます。合宿ハッカソンをまだ経験したことがないなら、一度は経験してみることをお勧めします。

さて、リレーブログ。明日は @tanakahisateru さんです。

「FlexでTitleWindowをresize可能に」へのパッチ

Flex でちょっと凝ったモーダルダイアログを作りたいときに便利な TitleWindow をリサイズ可能にしようと思って調べたら、以下のエントリを発見しました。

Flex で TitleWindow を resize 可能にする - Enjoi Blog

おおまさにこれだと、とても便利に使わせていただいたのですが、TitleWindow の子要素によっては意図せずしてリサイズ処理に入ってしまうことがありました。


これは子要素に対して発生したイベントが、バブリングで TitleWindow に届いたときに起こります。その時の event.localX, event.localY は子要素の座標系での値になりますので、それをリサイズ開始の判断に用いるとおかしなことになるんですね。

ということで onThis_mouseDown メソッドを以下のように修正しました。ステージ座標系の値から計算して、TitleWindow でのマウス座標を求めています。

      private function onThis_mouseDown(event:MouseEvent):void
      {
        var stageX:Number = event.stageX;
        var stageY:Number = event.stageY;
        var ctX:Number = event.currentTarget.x;
        var ctY:Number = event.currentTarget.y;

        var loc:Object = {
            x: stageX - ctX,
            y: stageY - ctY
        };

        // check Top pos
        if( loc.y < SIZE_DRAGAREA )
        {
          blDragTop = true;
          iDragPosHeight = loc.y;
        }

        // check Right pos
        if( this.width - SIZE_DRAGAREA < loc.x )
        {
          blDragRight = true;
          iDragPosWidth = this.width - loc.x;
        }

        // check Bottom pos
        if( this.height - SIZE_DRAGAREA < loc.y )
        {
          blDragBottom = true;
          iDragPosHeight = this.height - loc.y;
        }
        
        // check Left pos
        if( loc.x < SIZE_DRAGAREA )
        {
          blDragLeft = true;
          iDragPosWidth = loc.x;
        }
      }