Gauche:I/Oタイムアウト
Shiro(2010/04/06 14:20:22 PDT): I/Oにタイムアウトつけられたら便利かな、という考察。
発端は、Chaton-Twitterブリッジで、たまにtwiter->chatonのスレッドが止まる、という問題。 コネクションがエラーになったらリカバリするコードは入っているのだけれど、 エラーにもならずにスレッドが永遠に受信待ち状態になってしまう。 これは「通信の相手側がコネクションの後始末をせずに消えた場合、 こちら側から何らかのアクションを起こさない限り、 相手が消えたことがわからない」という問題で、 どんなtcpアプリケーションにでも起こり得る。
(udpでももちろん起きるけど、udpの場合はもともとunreliableなこと前提で アプリを組むからなあ。tcpの場合、大抵は異常状態をトランスポート層で 検出してくれることをあてにするので、ちょっと盲点ではある)
(tcpの場合、keepaliveを使えばこの状態を検出できるけど、keepaliveの pingのデフォルト値は2時間とかだし、OSによってはこの値を簡単には変えられないので いまいち使いづらい)。
で、この事例そのものはtwitter apiを呼ぶスレッドに対してもう一つの スレッドがタイムアウトを監視してやればいいんだけれど、どんなアプリでも 起き得るならライブラリ側にタイムアウトを組み込みにしちゃったらいいんじゃないか、 と思った。
どのレイヤでやるか?
レイヤを問わずにタイムアウトかけるには、ブロックするかもしれない 処理を別スレッドで走らせてタイムアウトつきthread-join!を呼んでやれば いいんだけれど、タイムアウトした時に実行中のスレッドを止める方法が ちょっと厄介だ。thread-terminate! は有無をいわさずスレッドを殺しちゃうので、 あまり常用したくない (Gauche本体については一応cancellation safeになってるはずだけど、 外部ライブラリを呼んでたりするとその先でどうなってるかわからん)。 シグナルなら中断に対してはより安全だけれど、 シグナルハンドラの設定はグローバルに影響を与えちゃうので ライブラリとしては使いにくい。
素直な方法は、システムのファイルディスクリプタから読み出すレイヤに select()をかませることだ。ところがこのレイヤはポートの下にある。 httpなどの上層ライブラリはread-lineなどポートの上の層を呼んでるので、 ポートより下層でタイムアウト処理をやった場合、どうやって 上層と下層をコミュニケートさせるか。
下層から上層へは、タイムアウトが発生したら例外を投げるという 安直な方法がある。
- ポートはいくつもスタックされ得るが、積み重なった下層で タイムアウトした場合、上層のポートの内部状態は大丈夫か? 現在でも下層が例外を投げるケースはあるが、一応チェックが必要。
上層から下層へはちょいと難しい。引数で指定させるようとすると、 上から下まで全ての中間レイヤに引数をつけてまわらないとならない。
- ポートが上層と下層とをつなぐ共通項になっているので、 ポートの属性として持たせるか? ただ、タイムアウトは一回一回の アクションに付属するトランジェントなパラメータなので、 ポートの属性変更というパーシステントな状態変化を巻き込むのは 筋が悪い。
- トランジェントに層をまたいで影響を与える定番は動的変数だ。 例えば出力操作を動的変数で変更するのは (CLの*print-circle*とか) 普通なので、タイムアウトをこれで指定するのも悪くはないかも。 ファイルディスクリプタに対するシステムコール、という低レイヤの 動作が動的変数で変わるのはなんとなく不安に感じるんだけど 慣れの問題かもしれない。
- タイムアウトはポート毎に異なる。動的変数で指定するなら ポートとタイムアウト値とのalistにすべきか?
下層でタイムアウト処理をさせるとして、もうひとつ、 レイヤのミスマッチは「タイムアウト」の定義にも影響を与える。 つまり、どの時点をタイムアウトの起点とするか。 http-getにタイムアウトをかけるなら、http-getを呼んだ時点を 起点とするのがいいけれど、 下層のread(2)を呼んでるコードにとっちゃ上の事情はわからない。
ああ、pthread_cond_timedwait()とかが絶対時間でタイムアウトを 取るようになってるのはこのせいかもな。上層で絶対時間を指定してやれば 下層は起点を気にする必要がない。
上層のAPIとしては相対時間を受け入れるけれど、動的変数でタイムアウトを 指定する段階で絶対時間にしとけばいいか。