D-7 <altijd in beweging>

Day to day life of a Perl/Go/C/C++/whatever hacker. May include anything from tech, food, and family.

カテゴリ:技術/Tech

ちょっと前から私が関わらせてもらっているThe Perl FoundationのGrants CommitteeはPerl関連で広い範囲において有用性が認められるプロジェクトにたいして定期的に助成金を交付しています(詳細はこちら)。基本的に誰でも応募可能です(応募方法はこちら)。

この活動ルールにいくつか変更が加えられました(原文はこちら)。なおTPF Grants Committeeには現在自分とMakoto Nozaki(筆頭メンバー)さんの二人の日本人が関わっておりますので、日本からの応募が今までのどの時点よりも通りやすい(便宜を図るという事ではなく応募に不備があった場合などの対応について日本向けの対応がちゃんとできるという意味です)ので、皆様是非ご検討ください。

なお変更内容については以下の通りです:

1. 二ヶ月おきに応募審査が行われる

これまでは四半期ごとでしたが、 もっと頻繁に審査・受付が行われるようになります。

2. 助成金の上限が大幅にあげられました

これまでは$3000が上限だったのですが、これが$10000まであげられました。原文には「上限がなくなった」ともありますがその後に書いてあるとおり諸々の理由により規則上は$10000が上限です。ただしこれは「お金があれば」なので、その時々によって交付可能な助成金の上限は変動します。

重要な事なのでもう一度書きますが、応募方法はこちらです。英語や応募方法に関するヘルプが必要な場合は@lestrratまでご一報ください。皆様のご応募をお待ちしております!また、この制度を活用できそうな人にこの情報が届くように拡散していただけると助かります。

    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

goで書いたwebappでもApache形式のログを出したい!ということでgo-apache-logformatを書きました。

最初はこのツールを使って毎回ServeHTTP()関数の中でログを出力する関数を呼び出すようにしようと思ったんだけど簡単なラッパーを用意したので多分かなり簡単にApacheログを出力できるようになるはず

まず最初にやらないといけないのがApacheLog構造体を作るところ。出力先のio.Writerと出力フォーマットを指定して作成するんだけど、いわゆるcombined logフォーマットだったらもうすでに作ってあるので、それをClone()しちゃえば簡単:

import (
"github.com/lestrrat/go-apache-logformat"
)

....
logger := apachelog.CombinedLog.Clone()
logger.SetOutput(...) // io.Writer. os.FileとかでもOK. デフォルトはos.Stderr

で、あとはそれをhttp.HandlerFuncで使用されるようにすればいい。もしServeHTTP()関数を実装した構造体があればそれをラップするだけでもOK:

http.ListenAndServe(":8080", apachelog.WrapLoggingWriter(handler.ServeHTTP, logger)

もし色々自分でやりたいなら、ServeHTTP関数の中で自分で以下のようにフックしてもいい:

func ServeHTTP(w http.ResponseWriter, r *http.Request) {
...
lw := apachelog.NewLoggingWriter(w, r, logger)
defer lw.EmitLog() ...
}

ここでひとつ気をつけないといけないのが、上記のServeHTTPの中ではw ではなく lw (LoggingWriter)のほうを使う事。そうしないと諸々必要な値が取れない。

最後にもしこれをログファイルにはき出しつつローテーションも行いたいならgo-file-rotatelogsを最初のloggerにかませばそれでおしまい:

import (
"github.com/lestrrat/go-file-rotatelogs"
"github.com/lestrrat/go-apache-logformat"
) ...
logfile := rotatelogs.NewRotateLogs("/path/to/accesslog.%Y%m%d")
logfile.LinkName = "/path/to/accesslog" logger := apachelog.CombinedLog.Clone()
logger.SetOutput(logfile)

というわけで是非お試しください。ツッコミ、PR、お待ちしております
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

Yet another Yak-shaving... 

というわけで、Perl5のFile::RotateLogsgoにさくっとポートしました。使い方はREADMEのままです!

これでちょちょっと書けばgoアプリでも勝手にログファイルがローテートされるようになります!
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

本日のYak Shavinggo-test-mysqldを書いた。

import (
    "database/sql"
    "log"
    "mysqltest"
)

mysqld, err := mysqltest.NewMysqld(nil)
if err != nil {
   log.Fatalf("Failed to start mysqld: %s", err)
}
defer mysqld.Stop()

db, err := sql.Open("mysql", mysqld.Datasource("test", "", "", 0))
// Now use db, which is connected to a mysql db


go-stf-serverのテストを書き始めるにあたってmysqldをどうしても立ち上げないといけないんだけど、これがまた自動的にやるのは面倒くさくて面倒くさくて・・・

というわけでTest::mysqldからまるまるまるっとぱくってgolangにポートしました。これでmysqldを立ち上げるのも簡単だね!

それほど障壁は高くないと思いますので、是非気軽にお使いください。問題があった場合はissueをお気軽にどうぞ! 
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

Perl版のSTFは自分が参加し始める前から、ひとつのスクリプトから複数の種類のワーカープロセスを立ち上げるように作られていたのでそれを踏襲し続けている。ひとつにはワーカーの種類が複数あるのでそれらをひとつひとつ立ち上げるのは結構面倒くさいのと、Perl版のバージョン2.00からはメインプロセスがシステム全体のワーカー数を関しつつ自分がどのワーカーを何個ずつ立ち上げるべきなのかを自律的に計算するようにしたのでこれはこれでよい。

さて、それでgo版だが、goでは言語設計的にfork()は使えないようになっている。なので通常はgoroutineをガツガツ立ち上げる事になるわけだが、この間やったgo版dispatcherのベンチマークなどから見ても、goroutineベースだとシステムリソースをよりガツガツ最後の1%まで使ってやろう感が見えるので、本能的に「これをSTFワーカーでやると問題が起こった時にやばい」と感じた。

正直ここは勘なので これが正解かどうかわかってない。間違ってるかもしれないけど全部goroutineでやるより、fork + exec とgoroutineの組み合わせで実装するほうがよいような気がしたので、それでやってみた。

スーパーデーモンにあたるものが"drone"と呼ばれる。こいつは必要なワーカー種類の分のコマンドをexec.Command経由で立ち上げる。しかしこの子プロセスがなんらかの理由で停止したら、即座に新しいプロセスを立ち上げ直す必要がある。ところがgolangにはノンブロッキングwaitpid()に相当するようなものはない。いや、厳密にはあるんだけど、syscall.Wait4()というポータブルではないところにあるのであった。

まぁそもそもfork+waitというのがUNIXなイディオムだからそりゃそうだ。ということでポータブルにするにはどうするか。ここはなんとなく無駄にも思えるのだけど、goroutineで豪勢にブロックするとよい:

func SpawnWorker(exitChan chan string, executable string) {
fullpath, err := exec.LookPath(executable)
...
cmd := exec.Command(fullpath, ...)
err := cmd.Start()
...
go func() {
cmd.Wait()
exitChan <- executable
}()
}
exitChanは別のところでselect{}経由でノンブロッキングで監視しておいて、もらった実行ファイル名で再度子プロセスをスタートさせる関数にフックしておく

func MainLoop() {
for {
select {
case executable := <-exitChan:
SpawnWorker(exitChan, executable)
...
}
}
}

これで永久機関のできあがりだ!あとはSTDOUT/STDERRをコピーしつつシグナルを受け取ったら良い感じにループを抜けたり、後にゾンビを残さないためにプロセスを止めたりすれば停止もキレイになる。

で、この子プロセスの中では特定のワーカー種別について必要な分だけのgoroutineをばかばか作って、必要な処理をする。方式はだいたいdroneと一緒でこちらではQ4Mにアタッチするgoroutineと、さらにそこからchannel経由でジョブをもらって実際の処理を行う複数のgoroutineを作成する。これがようやくいわゆるワーカーの部分。ここもそうする意味があるのかちゃんとわかってないけど、勘でそうするべきだと思ってmax-reqs-per-workerを設定して、定期的にgoroutineが終了するようにしてる


このマルチプロセス+マルチスレッド方式のほうがいいな、と思った理由はこれだと例えば特定のワーカーのgoroutineが詰まったりした時にそのプロセスだけ殺して再起動できるから。goroutineってthread idに対応するものが外部に出てないからkillしたくても簡単にできねぇんだよね!

で、ここまでできたので、次は残りのワーカー機能の作成と、自律的にワーカー数の調整を行う部分。自律調整の部分は上記のdrone部分でtime.Tickを使って定期的に行う予定。
 

と、ここまで書いたけど、この実装って本当に正しいのかわからんので相変わらずツッコミをお待ちしております。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

色々調べてたらgo でHTTPサーバーをgraceful restartする方法の記事にあたって、そのなかでシグナルを受け取るとリクエストを受け取ったらそれまでに受け付けたリクエストを処理してから停止するmannersというツールを見つけた。さらにゼロダウンタイムでサーバーを再起動するgoagainというツールも見つけたんだけど、これはサーバー内のコードをそれなりに変更しないといけないっぽくてうんざりしてしまった。

「っていうか普通にServer::Starterをかませるようにできればよくね?」と思ったので、go-server-starter-listenerを書いてみた!使い方はREADMEにあるとおり。このようなコードを書いておいてからstart_server --port XXX -- ./your-go-program って感じで動作させればServer::Starterが保持してるポートにバインドする・・・はず!(まだあんまりテストしてない)

あとはmannersとあわせれば良い感じでgraceful, zero-downtime restartができるはずー

ツッコミ歓迎です。 
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

色々Goで書いてみてやはりガードオブジェクトが欲しい、と思うことがよくある。一時的に何かの値を有効にしたり、かならず解放・リセット処理を行いたい場合のイディオムだ。これでいいのかわからないのだけど、とりあえず現時点で自分がやっていることをメモしておく。

Goにはdeferという仕組みがあって、関数の最後に必ずこれを実行してくれる。だがこれをオブジェクト構造体 につける方法はない。もちろん、構造体に対する関数呼び出しをdeferすることで同じ事ができるんだけど、そうするとどの関数を呼び出すのかをいちいち覚えていないといけない。一応runtime.SetFinalizer()というものはあるんだけど、これはいつ処理が走るのかわからないし、複数のオブジェクトやリソースが絡んでいる時にどの順番で処理が走るかもわからない。

ということはやはりdefer() を駆使するべきだろう。ということで自分は以下のような方法を使うことで今のところ落ち着いている。

func GuardLike () func() {
... 色々処理 ...
return func() {
... 解放処理 ...
}
}

func main() {
closer := GuardLike()
defer closer()
}  
これであればGuardLike()の中でどんな処理が行われていようと呼び出し側で気にすることなく関数をdeferから呼び出す、ということだけ覚えておけばよい。これでいいのかわからないので「こっちのほうがいいよ」という意見があれば教えて欲しい。

ちなみにPerl等のeval { ... }に相当するものを書きたい時も自分は関数でラップした上でdefer + recoverを使うなどしているので、goは結構このdeferの使い方をうまくすることで色々楽ができそうな気がする。

あとトランザクションのロールバックのような処理はなんか明示的に書いたほうがいいのかと思って今は明示的にTxnRollback()というラッパー関数を呼び出すようにしている。まだこのあたり流儀を統一できてない。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

まだ道半ばなんだけど、GroongaをGo+Perlなフロントエンドから使うシステムを動かしはじめている。

今回はとりあえず現時点での状態をざっくり書き出してみる。まだ本番化はしてないが、とりあえず本番環境からデータの挿入・削除ができるところまでつなぎ込みはした。最終的に全面的に本番化したらまたまとめ直します。

まずデータを突っ込む部分は慣れもあるのでより素早くデータの整形をしたりテーブルスキーマとかを変更したりするためにPerlでワーカーをさくさくっと書いた。ワーカーはQ4Mでデータを受け取り、データを整形してMroonga経由でデータを挿入。仕組みができたところでGroongaが我々が求めているデータ量をハンドリングできるかどうか検証するために全力で平均1行4KB~10KBくらいのデータをmroonga経由で○億件挿入してみた。この挿入処理中様々な地雷を踏んだので、kazeburoさんの助言などを元に挿入先のDBとテーブルをshardingしたりしてそれらを回避。

データは二つのよさげな検証用マシンにそれぞれ5000万件のところまではO(log(N))的な感じで挿入できていたが、それ以降は線形。これを書いている時点で1日約600万件の挿入ができている。5000万件の時点で関連grnファイルを全てあわせるとそれぞれのマシンに360GBずつそのあとはだいたい1日100GBずつくらい増えている。

sharding部分が落ち着いてきたら次は既存のシステムへのつなぎ込み用HTTP APIを作成。ここを今回はGoで作成。Goの部分は 練習もあったので簡単な参照部分をまずストレートなHTTP APIハンドラとして実装してみた。ステートレスなのでstructとかは基本的に作らず、golang.orgのExampleをほぼそのまま。

参照部分でどれだけ性能が出るのかどきどきだったけど、全く問題なし。Goちゃん速い。

次にデータ挿入部分はすでに生成してあるPerl製ワーカーとのつなぎこみだけなんだけど、queueへの書き込み部分はDBへの接続と使用テーブル両方ともある程度ランダムにばらしているのでその部分は全部Goで書き直し。この時点でサーバー側でステートを持ち始める必要ができてきたので、ApiServer型を作ってHTTPハンドラはクロージャで登録するような形に変更。コード的にはこれが大分おおきな変更だったけど、手を動かせばいいだけで、別にはまるところはなかった。定期的にランダムに挿入先をリフレッシュするように書いたんだけど、ここもgoroutineを使ってきれいにできた。time.Ticker便利!シグナル処理部分で一瞬迷ったけど、Perl5のSafe Signalsと同じ仕組みをchannelで実装しているだけだとわかってしまえばあとは簡単だったので、ごにょごにょしたり、一旦握りつぶしたシグナルをsyscall.Kill()でまた流したりとか色々した。

というわけで良い感じになってきた。そのうちこのシステムも皆様が触る部分にお目見えできると思っております。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

もう一つわりとまともなプログラム書いてみて、Go 1.1について所感。あくまで個人的な感触です。
  • やっぱりC書いてる人向けな言語な気がひしひしとする。
  • 例外がないならないでわりと気にならなくなってきた
  • 最初すごい使うだろうと思っていたdeferを意外と使わない
  • http.Requestとかはまだ貧弱な気がする
  • string[from:to] は結構便利
  • murmur hashの古いバージョンが必要だったのでポートしてみたけど、Cコードをそのまま置き換えるだけで楽だった。
  • ""と``は視覚的にわかりにくいと思うんだけど、まぁ使い方わかったら重宝してる。
  • 意外とほとんど組み込みライブラリだけでできる
  • slice.push()とかできないのすごく気になる。slice = append(slice, ...) は視覚的にすごい無駄な気がしてしょうがないんだけど、コンパイル時に最適化されてるんだろうな。
とりあえずGoは気軽に書けるし、結構高速だし、嫌いじゃないぜ。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

The Perl Foundation (TPF)のGrants Committeeのメンバーに選出されて就任依頼を受けたのでこれを謹んで引き受けることにしました!一応アジアからは唯一のメンバーとなっております。

Grants CommitteeはTPFから助成金の交付関連の管理をしているTPF内の委員会です。助成金を申請するにはこちらのルールに従って申請をメールするだけです。審査は年4回。現時点では助成金の上限は3000USドルと なっていますが、この上限はそのうち変更されるという噂を聞いています(そのあたりのルールまわりの審査・議論にも参加できる予定です)。

助成金の応募に関しては英語の壁だなんだはあるかとは思いますが、日本発のおもしろい技術はたくさんありますしオープンソース系の自分の仕事に対してなんらかの報酬がつくのは多少モチベーションがあがる材料にもなると思いますので、興味があれば是非応募してみてください!

ちなみに英語が問題ならば多少のお手伝いはできますので @lestrrat にお気軽にご相談ください。
 
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

goの練習でSTFのリペアをしてみた。



やってみた感想: go get便利。perl/DBIのRaiseErrorが欲しい。 perl/DBIのfetchrow_hashref()が欲しい。ビルトインでdeferついてるのすごいうれしい。

コードに対するツッコミ歓迎。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

はい、というわけで自分のトークです:



昨年12月頃から関わってるlivedoorBlogのコードを触っていた時の憤りをスライドにぶつけてみました。
追記:スライドに「ログにマーカーをつける」というのは、(コード読んでないけど)多分こちらのエントリにあるLog::Minimal::Indentとだいたい同じ感じのヤツです

ところでWeb上で見かける感想の中でこんなのがありました:
今年個人的に一番衝撃的だったのはやっぱ、livedoor blogのPlack化です。技術的な側面もさることながら、ああいう近視眼的には何のメリットもないし、逆にデメリットの方が大きそうな案件にリソースを割くジャッジができる会社としての姿勢が本当に凄いなと。


実はビジネス的にも意味はあるんだなー。

なかなか書くことができなかったんだけど、その内容というのがこちらと→ ブログのお引っ越し機能を大幅に強化しました! (この中の「記事URLの引き継ぎを可能に」というのがポイント)、あともうひとつ、こちら→ブログ内のURLを自由に設定できるようになりました です

エンジニアの方でしたらちょっと考えるとこれがどういう事なのかわかるかと思います。記事URLが自由になるってことは・・・もうパターン固定のディスパッチングができないのですね。元々のディスパッチング機構はApacheモジュールとして書かれていて、mod_perlの実装はこのApacheモジュールとべったりだった。これをPSGIにしたらApache側と分離したおかげできれいに実装できた、と。

(まぁ本当の事言うとPSGI化してたら「これならできるじゃん!」ということがわかってやったのだけど。)

ということでPSGIに変えたいためだけに変えたってわけではなくて、PSGIにすることによって格段に自由度が増す事がわかっていたのでそれをベースに新機能がリリースできた、というところです。 

実はこの機能のリリースがもう少し早かったらこれについても話せたのだけど、 ギリギリのところで含められなかったので、mod_perl → PSGIまでで話が終わった(時間もなかった)

 続きを読む
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

主に自分用メモ。例えば以下のようなコードからDBIハンドルを取得してるとする

use strict;
use DBI;

sub get_dbh {
DBI->connect_cached(....);
}

sub foo {
my $dbh = get_dbh();
$dbh->do(...);
}

sub bar {
my $dbh = get_dbh();
$dbh->do(...);
}

foo();
bar();
.... 
で、まぁSET NAMES utf8を自動的に発行したいなーと思ったので、get_dbhに手を入れる

sub get_dbh {
my $dbh = DBI->connect_cached(...);
$dbh->do("SET NAMES 'utf8'");
return $dbh;
}
そうするとget_dbh()内で DBIハンドル自体はconnect_cached()でキャッシュされた同じハンドルが返ってくるのだが、毎回SET NAMES 'utf8'が発行される。これは無駄だし、例えば一つ手前のステートメントの状態を期待するコードを書くと破綻する:

{
my $dbh = get_dbh();
$dbh->do("INSERT INTO ...");
}

{
my $dbh = get_dbh();
# get_dbh()にSET NAMES utf8がなければ、直前のステートメントはINSERT INTO
# get_dbh()にSET NAMES utf8があると、直前のステートメントは SET NAMES utf8
my $id = $dbh->{mysql_insertid}; # SET NAMES utf8の後なので、insertidは存在しない
}

なので、一回SET NAMES 'utf8'を発行したら、それを覚えていたい。だがDBIハンドルはハッシュのように扱えるものの、適当なキーを入れようとすると怒られる:
if (! $dbh->{MySuperSpecialFlag}) { # ERRRRRRORRRRRR
$dbh->{MySuperSpecialFlag} = 1; # ERRRRRRORRRRRR
$dbh->do("SET NAMES 'utf8'");
ところがprivate_で始まるキーなら使える!これで勝つる!
sub get_dbh {
my $dbh = DBI->connect_cached(...);
if (! $dbh->{private_my_super_special_flag}) {
$dbh->{private_my_super_special_flag} = 1;
$dbh->do("SET NAMES 'utf8'");
}
return $dbh;

ということで万事解決。
本件はちらっとつぶやいたら生き字引の@tokuhiromが覚えてたので助かりました。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

現時点ではあんまり具体的に何をしたか書けないけどブログにでっけぇ変更を入れた。システム全体の根本的な部分なので一発でキレイにとはいかなかったが、まぁ変更の規模に対して考えると妥当な感じだろう。本番化には1週間かかった。なので大分開放感がある。

具体的な事はもっと後になったら言えるが、とりあえず今の段階では Apacheモジュールを殺し、1000行近いRewriteRuleを殺し、PSGI側でミドルウェアを9個作成してPSGI側でApacheが今までやってたことを全て吸収した。

(追記 8/23 12:20)あとログがltsvになったよ!その先の事はモリス先生がやってるよ!このログもプロキシ側でログを取るのにアプリ側からの情報を組み込むとかしなきゃいけんかったので地味に面倒くさかった。わかってしまえば簡単だったけど。

パフォーマンスは正直言って、Apache版と比べて色々追加でやってることもあるので10%程落ちた。だがこれを持ってものすごい自由度を手に入れたし、あの目がくらむRewriteRuleの山とおさらばすることができた。さようならRewriteRule! 君のことを初めてviで開けてみたときから大嫌いだったよ!あと、Apacheモジュールもまぁ、悪くはなかったけど、手を入れるのが億劫だったのでやる気が起きなかった。近いうちにgit rmしてやる。

ちなみにパフォーマンス改善は相変わらずkazeburoさん無双。こっちもバグ修正とかあったりしてる間にアクセスは来続けるので、同時に見ててくれるのが大変助かります。今回もHTTPヘッダ一個調節するだけでLAが1/4になるとか見てて「ほほぉ」と思いながら作業できました。

あと今回も大量の(環境変数の指定で適時畳み込まれたり有効にしたりできる)デバッグログを入れて、アプリケーションの流れを完全に追える形を作ったおかげで問題が起こっても追うのは自分的にはかなり簡単でした。最後の最後で色々取りこぼしがあった際に大変助かった。

というわけでしばらくぼーっとしたい。 
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

ほぼ一年前に「STFワーカーの自律分散と適応スロットリング」という記事を書いた。

STFというAmazon S3っぽいツールの裏方のワーカーをどう「サービスの邪魔をせずに」「しかし可能な限り大量のデータを速やかに処理するか」という命題をどう解消した、と言う話。

あれからほぼ1年たって「そういえばもう大分STFなにもしてねぇな」と気づいたのでこのエントリを書き始めた。

あれ以来ワーカーの過負荷によるアラートが来たことない。あれ?と思ったらワーカーが止まっててキューがいっぱいいっぱいになっちゃった、というのもない。足りないワーカーは勝手に補充されるし、ストレージの負荷が高い時は勝手に自動的にスロットリングされる。要はもうこの1年近く俺は管理画面でフラグをちょいと変えたりグラフを見たりする意外ほぼなんにもしてない。勝手に動いている。

この協調メカニズムを作るのにMySQLだけを使って書いたのが自分的にポイントが高い。データを高度に共有するならともかく、リーダーエレクション程度ならZooKeeperなんていらねぇよ!

というわけで本当にただの自画自賛エントリでした。STFわりといいと思うんだけど、なかなか「使って!」っていう機会がないのが悲しい。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

Perl徹底攻略 (WEB+DB PRESS plus)
Perl徹底攻略 (WEB+DB PRESS plus) [大型本]

書き下ろしの章があったり、連載当時perl 5.10とかの話だったのがperl 5.18対応になってたりとわりとアップデートされています。自分が書いた物としては非同期プログラミングの章とか入ってます。あと一応この数年間監修は全部やってきて全ての章に関わってはいるのでなかなか感慨深いですね。

というわけでよろしくお願いいたします。 
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

(追記)なんか最新版ではなおってたらしいです!

現時点のLWP::UserAgentで meta nameに":"が使用されている(例:twitter:cardに対応している)サイトのコードを取得しに行くと色々妙な事が起こる。何が問題かというと、LWP::UserAgentはコンテンツのデコードを行ったりする際にHTMLの中身を解析してメタタグから情報を引っ張ったりして文字コード判定のヒントを仕入れたりしてる際、HTML::HeadParserというモジュールがそのようなメタデータをHTTP::Headerオブジェクトに格納しようとしているのが問題を起こしている。

例えば、
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
という指定があれば
my $res = $lwp->get("...");
print $res->header('Content-Type');
には上記のメタタグの値が入る。内部的には以下のようにメタタグの中身をHTTP::Headerオブジェクトに格納している
$headers->push_header("Content-Type", "...");

同様にhttp-equivでなくとも、name属性がある場合は "X-Meta-...."というヘッダにその値を格納するようになる。なので以下のようにtwitter:card用のメタタグが入っていた場合:
<meta name="twitter:card" content="summary" />
この値をヘッダに入れようとするわけだが、このname属性、コロンが入っている。ということはヘッダ名は・・・"X-Meta-Twitter:Card"。ぎゃー!これRFC違反!ヘッダの名前に":"を入れちゃいけない!そんなところでHeadParserの処理がおかしなところでとまってしまうのであります。

これを回避するには現時点ではLWP::UserAgentのparse_head属性を偽にする必要がある
my $lwp = LWP::UserAgent->new(parse_head => 0);
# もしくは
# $lwp->parse_head(0);
でもこれだとHTMLヘッドセクション内のヒントを使うな、って話になるんだけどまれに必要な時もあるし、それに段々twitter cardを実装しているサイトも増えてるし、おかげで僕は先日何時間も無駄にしたし・・・・ということでHTML::HeadParser::Liberalというモジュールを書いた。

使用方法はuseするだけ。
use HTML::HeadParser::Liberal;
これでtwitter:cardもパースして、X-Meta-Twitter-Cardというヘッダ名でHTTP::Headersオブジェクトからアクセスする事ができます。


このモジュールはHTML::HeadParserの内部にかなり強引に手を入れて、 twitter:cardのような書式のメタタグ名があったら ":"を無理矢理"-"に変えてヘッダに格納するようにする、というだけ。将来的に他にも必要な調節があったら入れるつもりだけど、とりあえずはこれだけです。

ちなみに効果はグローバルです。というわけでtwitter:cardもいいからとりあえずパースしろよ!って時にはどうぞ。 そのうちHTML::HeadParser本体に修正が入る事を祈る・・・
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

Perl5 Census Japan 2013に回答いただいた皆様、ご協力ありがとうございました!知らなかった人のために説明しておくと、私が2013年4月7日から19日までの間アンケート形式で日本でのPerlの利用状態等を知りたいと思い回答を募りました。回答数は394でした。

なるほどねー、へー、と思いつつデータを見ていました。取り急ぎ今回はシンプルな回答の集計結果をお知らせしようと思います。これからさらに面白い解析は是非このエントリの最後にあるデータを使ってみていただけると嬉しいです。

続きを読む
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

Test::Moreのsubtestのテストはどう書くのが一番きれいなのか

コードを見る限り、ガードオブジェクト使うとteardown部分は気が楽ですよ。以下のような使い方をすればガードオブジェクトはスコープを抜けた瞬間に必ず実行されるのでteardownのタイミングなんて気にする必要さえない。

use Scope::Guard;

subtest "A context" => sub {
my $subject;
my $setup = sub {
$subject = Bar->new;
return Scope::Guard->new(sub {
undef $subject;
});
};

subtest 'foo_method' => sub {
subtest 'given xxxx arguments' => sub {
my $guard = $setup->();
....
# teardownの明記はいらない
};
};
};
で、これをsubtestごとにやるってのが常態化するなら、全部t/Util.pm とかにまとめちゃえばいい(けどそのあたりはお好みで)。ちなみにScope::Guard使わなくても自分で簡単にガードオブジェクトは作れるので、依存関係を増やしたくない場合でもOK。
    このエントリーをはてなブックマークに追加 mixiチェック Share on Tumblr

このページのトップヘ