I sort my thought...

2012-01-23

○○した順に保つデータ構造

http://www.amazon.co.jp/dp/475981339X を読みつつ、もしランキング = 確率的ランキングとみなし、注文した順をランキングとして採用し、実現するとしたら、どうやって実現するだろうか、というのを考えてみた。 (実際のところ、かなりの更新頻度がないといけないであろうことや、spamなどのデメリットもあるので、現実的に使えそうなポイントはかなり考えないといけないのだろうけど。

確率的ランキング

本の中で説明されている数学的なバックグラウンドについてはちゃんとなぞっているわけでもないし、まったくしっかり理解できているわけではないです。

本のあらすじを乱暴にまとめると、Amazonのランキングを観察してみると、中間層の順位の推移を長期にわたって観察すると、大きく変動しているのが観察されて、そのデータから全体を統計的に推測すると、Amazonのランキングは、注文された商品の順番に近似している、と説明している。

ある瞬間の注文された商品の順番を切りとると、上位の商品は注文頻度は圧倒的に多いのでこのような仮定が成立する、らしい。

やりたいこと

商品数がとてもたくさんあるような場合に、注文した商品の順番をランキングとしてなるべくリアルタイムに反映したい

各々の商品のページには、現在その商品が何位なのか?といった情報も表示されているので、順位が下位である場合に時間のかかるような状態だと困る

私が考えたこと

まず考えたのは順番を保ちつづけないといけないので、更新のコストを下げることについて考えました。

1つ順位が入れかわるごとに、それ以降の順位情報をすべて更新しないといけないようだと、更新速度や、商品数がどんどんあがるごとに破綻していってしまう。

中間データの更新コストが低いデータ構造といえば何かな、と考えて思いついたのは、LinkedListでした。

単にLinkedListにすると、各々の順位を求めるのが、ひとつひとつのノードをたどっていかないといけないので、下位のアイテムの順位を求めるほど遅い、という欠点があります。

そこで、2の指数の順位のランキングはどのアイテムという辞書をつくってやり、ランキングを求める際は、一番近い2の指数の順位へ飛んでからポインタをたどる、という構造にしてみました。例えば、2位、4位、8位、16位、32位、64位、128位などは辞書を一回引くだけでどの商品か判別することができます。順位が下位であるほどポインタへ飛んでから辿る回数が多くなってしまう傾向にありますが、一からたどるよりはマシでしょう。(どちらからも辿れるように双方向なリストが望ましい

また、データを更新する際には、itemのidをベースにして、探索し、更新をかけないといけないので、itemのidをもとにリンクリストのノードへ飛ぶ辞書も用意しました。

以下のアイディアを実装したのが下記です。

https://gist.github.com/1663101

module Orderd
    class Ranking
        def initialize
            @first = @last = Node.new
            @item_id_of = {}
            @marker_of = {}
            @size = 0
        end

        def top item_id
            n = @item_id_of[item_id]
            if n.nil?
                n = Node.new
                n.value = item_id
                @item_id_of[item_id] = n
                @size += 1
                update_markers
            else
                return n if n.prev.nil?

                # nをLinkListから切りはなす
                np = n.prev
                n.prev = nil
                nn = n.next
                np.next = nn
                nn.prev = np
                update_markers n
            end
            n.next = @first
            @first.prev = n
            @first = n
            n
        end

        # markerの位置を調整してやる
        def update_markers node=nil
            marker_of = {}
            @marker_of.each do |k, v|
                marker_of[v] = k
            end
            start_marker = 0
            unless node.nil?
                n = node
                counter = 1
                n = node
                until n.next.nil?
                    if marker_of[n]
                        start_marker = marker_of[n]
                        break
                    end
                    n = n.next
                    counter += 1
                end
            end
            get_markers(0, @size).each do |i|
                if @marker_of[i]
                    if i < start_marker
                        # top内から呼ばれることを想定しているので、データを抜いたところより前のランキングは1ずつずらす
                        @marker_of[i] = @marker_of[i].prev
                    else
                        if node.nil?
                            # nodeがなかった場合は、件数がかわってくるので順番をズラす
                            @marker_of[i] = @marker_of[i].prev
                        end
                    end
                else
                    counter = 1
                    n = @last
                    until n.prev.nil?
                        rank = @size - counter
                        if rank == i
                            @marker_of[i] = n
                        end
                        counter += 1
                        n = n.prev
                    end
                end
            end
        end

        # 与えられた範囲内で
        # 2のn乗の配列を返す
        def get_markers start, last
            markers = []
            num = 2
            while num <= last
                if num >= start
                    markers.push num
                end
                num *= 2
            end
            return markers
        end

        def get_item item_id
            item = @item_id_of[item_id]
            i = 1
            node = item
            marker_of = {}
            @marker_of.each do |k, v|
                marker_of[v] = k
            end

            counter = 0
            until node.prev.nil?
                if marker_of[node]
                    return marker_of[node] + counter
                end
                counter += 1
                node = node.prev
            end
            return nil
        end

        def rank_list rank, num
            result = []
            node = @first
            start_rank = get_markers(rank, @size)
            if @marker_of[start_rank[0]]
                i = start_rank[0]
                node = @marker_of[i]
            else
                i = 1
                node = @last
            end
            until node.prev.nil?
                if i >= rank
                    if i > rank + num
                    else
                        result.unshift node
                    end
                else
                    return result
                end
                node = node.prev
                i -= 1
            end

            return result
        end

        def size
            @size
        end
    end

    class Node
        attr_accessor :prev, :next, :value
        
        def inspect
            prev_object_id = @prev.nil? ? "" : @prev.object_id
            next_object_id = @next.nil? ? "" : @next.object_id
            "<Node ##{self.object_id} value=#@value prev=#{prev_object_id} next=#{next_object_id}>"
        end
    end
end

orderd = Orderd::Ranking.new

NUM = 10000
orderd.top(15)
(1..NUM).each do |i|
    num = rand(NUM)
    p [i, num]
    orderd.top(num)
end
p orderd.get_item 15
p orderd.rank_list(100, 100).map { |n| n.value }

このような場合に、もっと良く知られたデータ構造とかあったりするんでしょうか?

2011-10-31

[]memcachedのように使えるBloomFilter

YAPCでmalaさんの話を聞いていて、memcachedのようにお手軽に使えるbloom filterがあるとひょっとすると便利かもしれない、とふと思ったのでAnyEventつかって、Bloom::Fasterのwrapperを書いてみました。

https://github.com/walf443/p5-bloomd-server

以下のようなプログラムを書いてサーバーを起動します

use strict;
use warnings;
use EV;
use AnyEvent;
use Bloomd::Server

my $cv = AnyEvent->condvar;
my $server = Bloomd::Server->new(
    capacity => 100_000_000,
    backupdir => '.',
    ulog => 'ulog',
);
$server->run
$cv->recv;

起動したらncで会話できます。

$ echo "check foo" | nc localhost 26006 
CHECK foo 0
END
$ echo "check foo barfoo" | nc localhost 26006
CHECK foo 0
CHECK barfoo 0
END
$ echo "set foo" | nc localhost 26006 
OK
$ echo "check foo barfoo" | nc localhost 26006                                                                                                                 CHECK foo 1
CHECK barfoo 0
END

statsでサーバーの統計情報を知ることができます。

$ echo "stats" | nc localhost 26006
STAT capacity 100000000
STAT cmd_check 6
STAT cmd_set 1
STAT error_rate 0.001
STAT pid 87719
STAT server_id 1
STAT server_time 132001511179202
STAT uptime 237
STAT key_count 2970
STAT vector_size 1155555563
END

backupコマンドを実行すると、backupdirで指定したディレクトリに、snapshot + timestampというファイル名でbackupを取ることができます。

snapshot.132001522548775

backupするコマンドがXSレベルでファイルの読みかきをしているので、AnyEventをブロックしてしまうという問題は、redisをパクって、forkして子プロセスでバックアップファイルを作ることで解決しました。

from_snapshotでファイル名を指定すると、バックアップを使って、起動することができます。

また、レプリケーションをすることができます。

欠点としては、あまり早くないので、基本的には他にある既存のKVSを使った方がよいケースが多いでしょう。ただ、データをどれだけsetしても使用メモリ等は増えていかないので、そういう面で運用しやすさはあるのかなーと思っています。

今自分が使えそうかなーと思っているユースケースとしては、色々なユニークユーザーの件数の集計処理をリアルタイムで実現するのに使えるかもな、という感じです。(ユニークユーザーのデータを保持しようとするとたくさんのデータを保存しないといけないが、具体的なユーザーはわからなくてよく、だいたいのユニークがわかればよい)

2011-09-26

[][]qudoで一日にjobが何件ぐらい投入されたか計測する

jobが一日あたり何件ぐらい投入されているのか、お手軽に確認できるようにするのに、MySQLのtriggerを使ってみるとどうだろ、ということでやってみた。

まず普段のqudoのschemaに加え、以下のようなテーブルを追加します。

 CREATE TABLE `job_counter` (
  `enqueued_on` date NOT NULL,
  `count` int(10) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`enqueued_on`)
) ENGINE=InnoDB

で、以下のようなtriggerを追加します。

delimiter /
CREATE TRIGGER job_counter ON AFTER INSERT job 
BEGIN
  INSERT INTO job_counter (enqueued_on, count) VALUES (CURDATE(), 1) ON DUPLICATE UPDATE count=count+1;
END
/
delimiter ;

これで、jobをenqueueすると、その日のjob_counterが+1されるようになるようだ。

なければ追加しつつあるカラムをインクリメントするようなSQLは、INSERT INTO xxx () VALUES ON DUPLICATE UPDATE 〜という構文を使うらしい。

CURDATE()を使っているので、レプリケーション構成をつくるときに、Rowベースレプリケーションにとかにしてないと、Master/Slaveがズレるのかなと思いつつ、ステートメントベースの設定の状態で、mysqlbinlogでバイナリログを見てみた感じだと、トリガに関してのbinlogは吐かれていないようなので、masterでもslaveでも各々triggerが実行される、という形になるっぽいのでよろしくないのかもしれない。ちょっと自信がないな。

http://dev.mysql.com/doc/refman/5.1/ja/stored-procedure-logging.html

2011-09-20

[]KVMコマンドラインから使うの術

結構前に自宅サーバーの仮想化はXenにしていたのをこれからはKVMらしいのでKVMに変えてみるか、とやってみたときは、ホストOSにX入れないとうまく動かなかったりそういう点が嫌だなぁと思っていたが、だいぶ環境や情報が揃ってきて、Xenで作ったりするのと同程度には楽にできるようになったようだなと思ったので、部屋の模様替えして再起動したついでにえいや、とやってみた。

ちょっと覚え書き程度にまとめておく。

sudo -H virt-install -n natty -r 256 --vcpus=1 \ 
   -f /var/lib/libvirt/images/natty.img -s 5 \
   --os-type=linux --os-variant=virtio26 \
   --network bridge=br0 \ 
   --location='http://jp.archive.ubuntu.com/ubuntu/dists/natty/main/installer-amd64/' \ 
   --nographics \
   --extra-args='console=tty0 console=ttyS0,115200n8 preseed/url=http://192.168.0.101/preseed-natty.cfg preseed/url/checksum=7e717ea622372d92d0a461f07d8d4e1a language=en locale=en_US.UTF-8 hostname=natty'

起動CDのISOイメージをDownlooadしてくるのがめんどうな場合は--locationでURLを指定して直で起動できるようだ。(Debian/Ubuntu/CentOSあたりのメジャーなやつはだいたいURLがあるようだ)

--locationするURLがググっても出てこない場合は、インストールCDのisoファイルを手元にダウンロードしてきて、-cで指定する方法もあるようだ。

LinuxCLIでインストールするのに重要なのが、--nographicsと、--extra-argsの'console=ttyS0, 115200n8'で、これがないと起動しているが画面には何も表示されなかったりする。シリアルコンソール以外で遠隔インストールをするには、VNCとかをつかってやるのがメジャーなようだ。(シリアルコンソール経由でインストールできないOS(?)の場合は使う必要がありそう)

115200n8という謎の文字が何を意味しているのか、というと、

http://lxr.linux.no/linux+v2.6.32/Documentation/kernel-parameters.txt#L514

http://lxr.linux.no/linux+v2.6.32/Documentation/serial-console.txt#L514

ただのオプションのようだ。115200は、serial-console.txtによるとスピードの最大値らしいので、最大値を指定する、というのが慣例になっているのかもしれない。n8は8bitごとに転送する、ということなのかな。オプションはこの数字じゃなくてもいけそうな気がするな。

なお、FreeBSDの場合はまたシリアルコンソール経由での起動の仕方が違っていて、

wget ftp://ftp.freebsd.org/pub/FreeBSD/releases/amd64/ISO-IMAGES/8.2/FreeBSD-8.2-RELEASE-amd64-bootonly.iso
bsdtar -C bsdiso-serial -pxvf FreeBSD-8.2-RELEASE-amd64-bootonly.iso
sudo bash -c "echo 'console=\"comconsole\"' >> bsdiso-serial/boot/loader.conf"
sudo -H genisoimage --no-emul-boot -R -J -b boot/cdboot -v -o FreeBSD-8.2-RELEASE-amd64-bootonly-serial.iso bsdiso-serial

みたいにして、起動ディスクのISOイメージファイルを書きかえたあと、-cでISOファイルを指定しつつ起動する、という方法でなんとかなるようだ。(なお、mkisofsじゃなくgenisoimageをつかっていたり、bsdtarをつかっていたり、Debianでの実行を前提にして書いている)

preseed/url、preseed/url/checksumは、Debian/Ubuntuのインストーラーで毎回ポチポチ入力しないですむように、preseedを動作させるための指定であるが、VMの複製はvirt-cloneでやった方が早いし、楽なので、クリーンに入れなおしたいことが頻繁になければがんばって作る必要はなかったようだ。

インストールの途中で強制終了したときは、

sudo -H virsh destroy natty
sudo -H virsh undefine natty

することで、また同じ名前で作れるようになるようだ。

ということで、Xenと違ってホストOSにゲストOSが限定される、ということがないので、自宅で色々マシンを作って実験して遊んだりするのはだいぶ快適になったはず。

2011-08-28

isuconに参加してきた

isuconに参加してきた

当日の行動

ややうろ覚えですが、自分がやっていたことをざっくり書いてみます。

  • まず、実行用の環境へアクセスしようとしたら、reverse proxyのサーバーへしかsshできなかったので、とりあえずisuconユーザー以下にあるファイルをざっと眺める
  • ソースいじっりするための、reposやdeployスクリプトを作ったり
  • Devel::KYTProfをuseするようにして、ベンチマークを走らてみる

→ 即興で作ったdeployツールのバグに気づかず、Devel::KYTProfのログが出なくてあれー、ということで時間を使う。

→ そもそもClass::Data::Inheritableがうまくuseできていないという問題があったので、とりあえずsudo -H cpanmで入れるようにした。(たぶんarch用のpathもuse libするようにすればよかったはず)

  • KYTProfをonでdeployを動かす
  • MySQLクエリを解析できるように、log_slow_queryを有効にして、long_query_timeを0にして全てのクエリがslow-logへ吐かれるようにする

だいたいここまでで12時くらい

とりあえずアプリ側をおまかせして、ミドルウェア系のチューニングをやろうかなと思い、

  • メモリ等にも余裕はありそうだったので、starmanのプロセス数を増やしてみた(→ ベンチ速度にはほとんど意味なし )
  • MySQLの設定はほぼデフォルト状態だったので、innodb-buffer-poolやkey-bufferをざっくり設定. ( → データは元々の状態で、ほぼメモリに乘りきっていたので、DBの負荷は高かったがあまり効果はなかったっぽい?)

あたりをやった。

ここまでやってみてアプリ側があんまり進んでいる感じがしなかったので、手を入れはじめた。

KYTProfのログにざーっと目を通すと、

かなり目につくやつは、

SELECT a.id, a.title FROM comment c INNER JOIN article a ON c.article = a.id  GROUP BY a.id ORDER BY MAX(c.created_at) DESC LIMIT 10

だった。いちおうMySQLのslowlogをmysqldumpslowにかけてみて、やっぱりそこが一番遅い、というのを確認してから、作業しはじめた。

複数クエリに分割できないかなー、というのを考えていたのですが、他のチームがぐんぐんスコアあげているのに焦っていて、とりあえず少しでも早めにスコアあげたいということで、とりあえず

SELECT a.id, a.title FROM comment c INNER JOIN article a ON c.article = a.id  GROUP BY a.id ORDER BY MAX(c.id) DESC LIMIT 10

して、少しはましになった。他チームの解法を聞くまで、article側にcommentの最終投稿カラムを持たせればよいだけ、というのに気づかなかったのは間抜けだった。orz ( この時点でスコアは1200程度)

なお懇親会で話を聞いてみると、最終投稿時刻カラムを持たせるかどうか、がスコア10000いくかいかないかの壁だったようだ。

このクエリを改善しても、やはり遅いので、ここはとりあえずmemcachedにのせよう、ということで、memcachedを構築して、使うようにしたりした。

投入してみると、どうやらmemcachedにデータがうまく入っていないらしい、ということで、

調査していた。結局並行で作業を色々していたうちに直ってしまったので、ちょっと気持ち悪いが、おそらくCentOSだとmemcachedの設定は/etc/sysconfig/memcached.confを変更しないといけないが、CentOSに不慣れで/etc/init.d/memcachedの中をいじってしまっていて設定の変更が効いていなくて、デフォルトの状態だとlocalhostからの接続しか受けつけない、とかそういう感じだったのではないかな、と思っているが定かではない。

その間に、memcachedのシリアライザや圧縮あたりの処理が入って無事動きだしたかと思いきや、「サイドバー更新後のコンテンツの更新がありません」チェックにひっかかるようになってしまった。

なので、更新時にキャッシュをクリアするようにしたが、まだダメで、更新時にキャッシュを作るように変更してみた。

だがまだうまくベンチマークのチェックが突破できない、ということで考えていて、Cache::Memcached::Fastの設定でnowaitを1にしていたので、post時に更新した瞬間にアクセスすると、リクエストは終わっていたが、更新されていない、という状態になったりしているのかなー、ということでオフにしてみたら、無事通るようになった。 ( この時点でスコアは5000程度 )

その後もひたすらベンチマークツールを動かしつつ、ログをみつつ、Devel::NYTProfの結果から遅いところを潰していく、という感じ。

DBIのコネクション貼ったり、とか、memcachedへの接続が遅いとか、ネットワーク周りの接続がXenだからか非常に遅かったのが気にかかったが、あまり調査している時間もなかったので、とりあえずはなるべく再接続させないようにインスタンスキャッシュしたりして接続回数を少し減らしたりした。

DBは、thread_cache_sizeやmax_connectionsを増やしてみたが、あんまり効果はなかったように思われる。(あんまり冷静に分析できてはいないけど)

あとは、コンテンツの新規追加後の編集はないので、サイドバー以外の情報はほとんどキャッシュすることができるとわかったので、通常のDBへ対するSELECT文をひたすらmemcachedへ向けていく、というオーソドックスな対応をしていて作業終了。

コンテンツ更新後にサイドバーが更新されていないとチェックが失敗するのに苦しめられつつ、最後には、結局失敗してしまった。

うちのチームがやった変更点は、https://github.com/walf443/isucon に反映されています。

反省

  • あまり人数をうまく活用できなかった
    • 事前打ちあわせとリーダーシップ重要
    • 現状の状況報告と各々がやることを被らないようにはっきりさせる
  • 最初のベンチマークツールを動かした時点で開始から1時間以上たってしまっていた
    • まずは「推測するな、計測せよ」
    • ベンチマークからプロファイルとかしてみた結果から判断しないと、限られた貴重な時間の中で、無駄なことに時間を使ってしまう

とても準備作業大変だったと思いますが、非常に楽しいイベントを用意していただいたライブドア様、ありがとうございました。

こういう参加者がみんな体験を共有できるようなイベントは懇親会とかでも今まで会ったことない人たちでもとても会話しやすくて非常によいですね。

参考: http://d.hatena.ne.jp/karupanerura/20110828/1314469620

2011-07-07

[]Test::Tester

以前テストライブラリのテストを書く際に、Test::Builder::Testerがつかえる、という記事を書いたが、Test::Builder::Testerは、テストの失敗の文字列を一字一句かえないように調整したりするのが、ダルいなぁ、というのがあったりして、もっとよいやつを探していたら、Test::Testerというモジュールをみつけたので、紹介。

Test::TesterのTest::Builder::Testerより良い点としては、

  • local $Test::Builder::Level = $Test::Builder::Level + 1しているかのチェックが意識せずに基本的にされる点
  • テスト用の処理がコンパクトにまとめられるインターフェース(check_test)

あたりが個人的には良いかな、と思いました。

どちらのモジュールにせよ、ややトリッキーなことをして実現している面はあるので、テストのファイルは独立させておき、Test::Moreの比較的新しい機能であるsubtest内とかではやらない方が無難なようです。

2011-07-04

[]続 Test::TypeConstraints

http://d.hatena.ne.jp/walf443/20110704/1309738408 で公開した、Test::TypeConstraintsですが、反応をもとに、微調整してCPANへあげました。

  • type_is_a_okというメソッド名をtype_isaにしました
  • Data::Validatorに依存せず、Mouse::Util::TypeConstraintsを直接使うようになりました
  • type_doesというRole名を指定できる版を追加しました。
  • coerceオプションで、coerce込みでチェックできるようにしました

[] local $var += 1の挙動

Test::TypeConstraintsのレビューをしてもらっていたときにid:gfxさんに教えてもらったのですが、

local $Test::Builder::Level += 2;

としていたのですが、これは、「元々の$Test::Builder::Levelに2を足す」、という挙動にはならないです。

そもそも、これは、テストが失敗したときの呼出し元が変にならないようにするためのおなじないで、一つのメソッドしか経由していないので、+1するべきで、+1でうまく動いていないことに疑問を持つべきでした。

以下挙動を確認するためのプログラムです。

use strict;
use warnings;
use Test::More;

our $TEST = 10;

{
    local $TEST += 1;
    is($TEST, 1, "undef + 1 = 1");
}
is($TEST, 10, "restore ok");

{
    local $TEST = $TEST + 1;
    is($TEST, 11, "10 + 1 = 1");
}
is($TEST, 10, "restore ok");

done_testing();

わりとうっかり書いてしまいそうなので、要注意なり。

[]Test::TypeConstraints

ちょっと固めに書いておきたいところで、メソッドの戻り値の型をテストしておきたくて、Smart::Argsとかを使うのに慣れてくると、ArrayRef[Int]とかでテストできると楽だなぁと思ったので書いてみました。

https://github.com/walf443/p5-test-type_constraints

内部的には、Data::Validatorを呼びだして、エラー時にメッセージをちょろっと変えているだけ。

自前のsubtypeつくったりして、そういうのをテストするときにも使えそうで、そういうときは、coerceが効いた方がよいのかな、思いつつ、どうやってそのあたりのインターフェースをかえようかな、というのは考え中です。

(Moose|Mouse)はけっこう使われてはいるはずなので、似たようなのが既にあるかもしれないですが、ぱっとみ見つからなかったです。

2011-04-09

[]Skeleton::CoC

フレームワークにそってアプリケーションを開発していると、これを追加するには、このクラスとこのクラスを作って、ここにテンプレートを追加する、といったことがよくある。

railsとかだとscript/generateとかあってテンプレートを元にファイルを自動生成できるので、似たようなのを簡単に作れると便利なこともありそうだな、ということで今さらながら書いてみた。

他にも似たようなのありそうだなぁと思いつつ、意外とそれっぽいのがなくて、わりと簡単に使えるのではないかなと思う。

もちろんrubigenでもよいのだけど、環境によってはrubyやらgemがあまり入ってない環境もあるので。

https://github.com/walf443/p5-skeleton-coc

テンプレートの種別ごとに、define_targetで依存関係と、ファイル名を指定してやり、Data::Section::Simpleで対応するテンプレートファイルを書いてやる。

テンプレートファイルの文法にはText::MicroTemplateを使っているので、ほぼperlで好きなように書ける、といった感じ。

        package YourApp::Skeleton;
        use parent(Skeleton::CoC);
        use String::CamelCase qw();

        __PACKAGE__->define_target('controller' => [], sub {
            my ($self, $pkg) = @_;
            my $path = join "/", split /::/, $pkg;
            return "lib/YourApp/C/$path.pm";
        });

        __PACKAGE__->define_target('action' => ['controller'], sub {
            my ($self, $str) = @_;
            my $path = join "/", map { String::CamelCase::decamelize($_) } split /::/, $self->controller;
            return "assets/tmpl/$path/$str.html";
        });

        __DATA__

        @controller
        ? my $self = shift; # $self is a Skeleton::CoC.
        package <? $self->project ?>::C::<?= $self->controller ?>;
        use strict;
        use warnings;
        use parent qw(YourApp::C);

        ? if ( $self->action ) {

        sub dispatch_<?= $self->action ?> {
            my ($self, ) = @_;
        }

        ? }
        1;

        @@ action
        [% INCLUDE "include/header.html" %]
        [% INCLUDE "include/footer.html" %]

で、生成させるときに呼ぶファイルはこんな風に記述しておく。

        strict;
        warnings;
        use YourApp::Skeleton;
        my $skeleton = YourApp::Skeleton->new(
            project => "YourApp",
            action => "index",
        );
        $skeleton->parse_options(@ARGV);
        $skeleton->generate;

で、こんな感じで実行してやる。


        $ ./skeleton.pl controller Foo::Bar
        # => generate lib/YourApp/C/Foo/Bar.pm

        $ ./skeleton.pl controller Foo::Bar action baz
        # => generate lib/YourApp/C/Foo/Bar.pm
        #    generate assets/tmpl/foo/bar/baz.html

個人的な要望にはわりと沿ってはいるが、まだそんなに使いこんではいないので、使いにくいケースはあるかもしれない。

もうちょい使いつつ、インターフェース等は変えるかもしれない。

2011-04-02

[][]pathogenのhelptagsしたらsubmoduleがdirtyになってめんどい

最近pathogen.vimへ移行して、だいぶvimライブラリをupdateしたりするのが楽になったのですが、

:call pathogen#helptags()

とかやると、uniteのマニュアルとかも:h uniteとかで引けるようになる、というのはとてもうれしいのですが、その一方で、git statusとかすると、submoduleがdirtyになってしまって、困っていました。

gitignoreに追加したらいいんじゃないかな、とおもってやってみたら、helptagsしてもdirtyには入らなくなりました。

レポジトリの.gitignoreに追加しても意味がなくて、core.excluesfileのgitignoreのファイルに、**/tagsおよび、**/tags-jaを追加するとよいらしいです。

2011-03-17

[]Cache::Pluggable

Cache::な名前空間を持つライブラリは、get/setなどのインターフェースがわりとそろえてあるのですが、ライブラリによって微妙に挙動が違ったりして、ちょっと別のライブラリを検証してみたり、とかが意外としづらいです。

例えば、Cache::Memcached::FastではhashrefなどをStorableでシリアライズしつつ透過的にget/setしてくれますが、Cache::KyotoTycoonではそういう機能はありません。

そこで、wrapperを書いてアプリケーションからは使うようにしたりするわけですが、毎回似たようなものを生やしたりするのは飽きたよ、ということで、Pluginを書いてやって、コアはシンプルな機能のままで、Pluginを抜き差しするだけで挙動を変えられるようにしよう、ということでCache::Pluggableというやつを書いてみました。

Plugin機能は元々あるメソッドの機能を上書きとかして挙動をかえまくりたかった関係で、Mouse::Roleをつかってみました。

同じメソッドをmethod modifiersしまくりなので、ちょっと使うPluginが増えてくると、Pluginのロード順を気にする必要があるのがよろしくないかもしれない。