ダメ人間オンライン

あまり信用しないほうがいい技術メモとか備忘録とかその他雑記

Amon2::Plugin::Web::PageCacheというのを書いた

レスポンスのHTMLをまるっとキャッシュするplugin書いた。書いたというか別のWAFで使ってたやつをAmon2用に書き直した。キャッシュ先はmemcachedです。

dameninngenn/p5-Amon2-Plugin-Web-PageCache · GitHub

やりたかったこと

  • requestのpath毎にキャッシュするか否か設定できるようにしたい
  • requestのpath毎にキャッシュのexpire設定できるようにしたい
  • /path/to?p=1 と /path/to?p=2 は別のものとしてキャッシュしたい
    • 想定してないクエリがついてきた場合はスルーしてクエリがついてないものと同じ扱いにしたい
  • requestのpath毎にキャッシュ削除ができるようにしたい
  • HTMLだけじゃなくてJSON返してる場合もキャッシュしたい
  • そこそこお手軽に使えるようにしたい

使い方

モジュールをインストールして、pluginを読み込んで、config.plに設定書くだけです。

    package MyApp::Web;
    use Amon2::Web;

    __PACKAGE__->load_plugin('Web::PageCache');
# config.pl

'PAGE_CACHE' => +{
    enable => 1,
    memcached => +{
        servers => [
            '127.0.0.1:11211'
        ],
        namespace => 'page_cache:',
    },
    path => +{
        'MyApp::Web' => +{
            '/'                         => { expire => 60 },
            '/detail'                   => { expire => 300, query_keys => [qw/p/] },
            '/member/{id:[0-9]+}'       => { expire => 60 },
            '/list'                     => { expire => 60, query_keys => [qw/p rows/] },
        },
        'MyApp::Mobile' => +{
            '/'                         => { expire => 60 },
        },
    },
},
enable

pluginを有効にするか無効にするか指定します。1だと有効0だと無効。
開発環境とかでサッと有効無効を切り替えられるように。

memcached

memcachedの設定です。Cache::Memcached::Fastを使ってるのでそれに渡す設定を書きます。

path

path毎の設定です。
context名とpathとそれに対する設定項目を書きます。PC用とスマートフォン用とかで分けててもそれぞれ設定できるようにしてます。

pathに指定できるフォーマットは Amon2::Web::Dispatcher::RouterSimple(Router::Simple::Route) で使えるものと同じです。

/blog/{year:\d{4}}
/blog/:year
/blog/*/*
normal string

/blog/:year と指定した場合 /blog/2012 と /blog/2013 は当然ですが別のものとしてキャッシュされます。
ここで指定していないpathはキャッシュの対象外になります。あと200番以外のステータスコードだった場合キャッシュしません。

expireはキャッシュが切れるまでの時間を秒で指定します。
expire => 60 と指定するとキャッシュが作られてから60秒間は同じ内容のものが返ります。

query_keysに何も指定しなければどんなクエリがついてこようとも同じ内容を返します。
query_keysに何か指定されていれば指定されたクエリについては別物として扱います。
例えば query_keys => [qw/p/] と指定した場合 /path/to?p=1 と /path/to?p=2 でそれぞれキャッシュされます。
指定したクエリ以外がついてきた場合 /path/to と /path/to?unknown=1 は区別されません。

キャッシュの削除

pluginを読み込むと delete_page_cache というメソッドが生えるのでこれを使います。
例えば

sub edit {
    my ($self, $c) = @_;

    # 何かupdateとかするやつ
    $c->nanika_update_suruyatu( $c->req );

    # 該当部分のキャッシュを削除してすぐ反映されるように
    $c->delete_page_cache({ name => 'MyApp::Web', path => '/detail' });

    $c->redirect('/dokoka');
}

こんな感じでpath毎にキャッシュを削除できます。/detail?p=2 /detail?p=3 みたいにクエリがついてる場合も上記の書き方で全部キャッシュ消してくれます。
ここらへんの仕組みは Catalyst::Plugin::PageCache を参考にしました。

Amon2標準で用意されている BEFORE_DISPATCH と AFTER_DISPATCH トリガーで動きます。なので特に書き換えない限りはキャッシュを返すだけの時も AFTER_DISPATCH のやつは動くので他になんか AFTER_DISPATCH のとこでやっててそこを通らせたくないって時はなんやかんやして下さい。

あとページキャッシュをpathによらず全消ししたい時は普通にflush_allすればいいと思ってるので全消しのメソッドは用意してません。pageキャッシュ用のmemcachedは他のと共用じゃなく別portとかで立てておけばめんどくさいこと考えなくていいんじゃないでしょうか。

モヤモヤ

あまりめんどくさいことせずに誰でもパッと使えるようにしたかったのでconfig.plに設定もろもろ書くようにしたけど、設定したいpathが増えてきた時にconfig.plの見通しが非常に悪くなるのでどうしたもんかなと。

delete_page_cacheは delete_page_cache('MyApp::Web', '/detail') にするかとか迷ったけど今の指定の仕方にした。

ドキュメントの英語がひどい。writing力低すぎて泣きそう。

Solrで「undefined field text」って言われた時の対処

Solr弱者なもんで設定でハマったのでメモ。

カラムを横断して検索するために問い合わせるクエリを q=field:value ではなく q=value で問い合わせて(dismax|edismax)で処理しようとしたら一生「undefined field text」って言われて困った。

textって名前のフィールドなんてないよ!って言われてるんだけど、schema.xmlを見てもtextなんてフィールド名は指定してない。

ググって出てきた解決方法は大概、exampleからコピーして編集したschema.xmlの中に元々サンプルで書いてあったtextとか残ってる気がするからそれを消せ!って感じだったけどschema.xmlの中にはなかった。

結論を言うとschema.xmlではなくてsolrconfig.xmlのほうに問題があった。

これもsolr/example/の下にあるやつをベースにして編集したんだけど、その中の

<requestHandler name="/select" class="solr.SearchHandler">

の中に

<str name="df">text</str>

てのがあってそれを消したら直った。

サンプルの設定でカラムの名前が"text"だとわかり辛いし、それでsolrconfig.xmlもすごく長くてどこをどう環境に合わせて変更すべきなのかがいまいちわかり辛い気がした。

東京Ruby会議10に行ってきた&黒Ruby会議で話してきた

1/13,14の2日間に渡って開催された東京Ruby会議10に行ってきました。

f:id:dameninngenn_owata:20130116020001j:plain

色々仕掛けがあってその中にノベルティのバッジをみんなと交換して全種類(全4種)コンプしよう!みたいなのがあったんですが、人気・不人気なバッジが当然あって僕は一番不人気なささたつバッジx4からのスタートでした。

交換を断られ続けるという中学生の頃のトラウマを呼び起こすかのような体験ができて懐かしかったです。バッジのデザインが何か変な浜辺に寝そべってる写真だったんですが、これ普通にささたつさんのアイコンにしとけば大人気になっていたと思うのは僕だけでしょうか?

黒Ruby会議

で、1日目の最後に黒Ruby会議というのがあったのですがそこで話してきました。LT形式の5分枠です。

黒い話か面白い話をしてくださいというざっくりとしたお題だったのですが、僕はとてもホワイトな人間なので黒い話もなかなか思いつかず年末あたりからずっとうんうん唸り続けてました。

一週間ぐらい前から謎の緊張感がすごくてゲロ吐きそうになりながら当日を迎えたわけですが、発表前に他の発表者の方々と黒い儀式を行ったことにより緊張も多少ほぐれて良い感じに。

# 黒い儀式(ゆーすけべーさん撮影)


話した内容はRubyのルの字も出てこないような内容だったのですが、暖かく見ていただけて石を投げられることなく楽しく話をすることができました。

しかし残念ながら話終えた直後に発表資料が謎の爆発に巻き込まれて消滅してしまったので公開することができなくなってしまいました。ざんねんだわー。

あと舞台袖でやんややんやしながら発表を見たりするのがだいぶ楽しかったですね。舞台袖マジックや!

なんし

2日目は特に雪のせいで運営の方々はとても大変だったかと思います。本当にお疲れ様でした。

素晴らしい地域Ruby会議をありがとうございました。とても楽しかったです!!

HTMLタグがちゃんと閉じてるかどうかだけをチェックするやつ書いた

かるぱねるらさんの「HTMLのバリデーションをよしなにやりたくてHTML::Lint::Pluggable書いた - Perl Advent Calendar Japan 2012 Hacker Track」を見て、そういえば書きかけのHTMLなんとかあったと思いだしたのでとりあえずそれっぽくした。

既存のモジュールでHTMLタグが閉じてるか開きタグがないかとかカジュアルにチェックするようなやつないかなと探しててHTML::Lintとかも見つけたんですが、マルチバイト入ってるだけでいらんエラー出したりしてカジュアルに使うには色々めんどくさそうだった。

というわけで既存モジュールでありそうだったけど見つからなかったのでHTML::Lint::ParserをみつつHTMLタグが閉じてるかどうかだけをチェックするやつ書いた。

dameninngenn/p5-HTML-Validate-Structure-Simple · GitHub

そう、名前どうしようかってのをずっと悩んでて、Lintもちょっと違う気がするしValidationっつってもなんかアレだしってことで一旦適当な感じで名前つけてそのままです。良い名前つけてくれる人いませんか。。。

使い方

use HTML::Validate::Structure::Simple;

my $v = HTML::Validate::Structure::Simple->new();
$v->parse( $html );

if( not $v->is_valid ) {
    for my $error ( $v->errors ) {
        warn $error->as_string;
    }
}

$v->clear();

parseにチェックしたいHTMLを渡すと、エラーがなければis_validは1を返してそうでなければ0が返ってきます。
で、エラーがあればerrorsがarrayでerrorオブジェクトを返してきます。

errorオブジェクトはas_stringを使うと

(265:138) span is unopened.

みたいな感じでどのタグが閉じてないか開きタグがないか返してくれます。
もしくは $error->tag, $error->type, $error->line, $error->column で個別に値もとれます。

errorsを空っぽにしたい時はclear()を使えば空っぽになります。

imgとかinputみたいな閉じタグがないやつは HTML::Tagset の emptyElement を見て判断してます。

はい

タグが閉じてるかどうかしか見てないので、hoge みたいな存在しないタグでも対応とれてさえすればエラーにはなりません。

色々ちゃんとやりたい場合はHTML::Lintとか上述したHTML::Lint::Pluggableとかがいいのではないでしょうか。

というか探しきれなかっただけで同じことする既存モジュールありそうな気がする。

TengでNULLをセットしたかった

UPDATE table_name SET hoge = NULL WHERE id = 100;

みたいにNULLを入れたい時Tengでやるには

$teng->update('table_name', {
    hoge => undef,
},{     
    id => 100,
}); 

のようにundefでいける。

追ってみたらundefはundefのままSQL実行する前のとこまで渡ってきてて、DBIのquote()でundefがNULLになってって感じだった。

NULL Values
Undefined values, or undef, are used to indicate NULL values. You can insert and update columns with a NULL value as you would a non-NULL value.

https://metacpan.org/module/DBI