golang の netchan でデータが欠落することがある?
Go言語を最近触っていてちょっとした経緯で netchan を使おうと思ったのだが、どうすればいいのかよくわからなくなってきたので取り敢えずメモっておく。このコードは上手くいく。1秒ごとに画面にログが現れる。
このソースの main() にある exporter() と importer() の呼び出しのうち片方をコメントアウトし、一方だけを呼ぶバイナリを2つ作って、Exporter 側を立ち上げっぱなしの状態で Importer を立ち上げたり終了させたりすると、Importer が切断されたあとに送られたデータのひとつ目だけが抜け落ちてしまう。
Importer が終了したタイミングを拾って
少なくとも送信したデータが欠落しているので普通に考えて netchan のバグだろうとは思う。
netchan が使えると恐らく色々な処理がプロセスを跨いでいながら自然に書けるようになると思うので、もうちょっと詳しく追ってみようと思う。golang 触り始めたばかりの小童にどこまでわかるのやら。
---- 2010-11-26 04:15 追記
処理を追っていると、ある程度流れは見えてきたと思う。
エラーが起きているのは netchan の common.go 内にある (*encDec) encode 関数なのだが、ここに「TODO: tear down connection if there is an error?」というコメントアウトがあった。
そもそもこの辺の処理自体があまり作り込みされていないのかな? と最初は思った。
ところがここでエラー処理をしようにも、そもそもこれを呼んでいる export.go の (*expClient) serveRecv 関数にあるループの頭でやっている val := ech.ch.Recv() の時点でデータを読み取ってしまっているのでエラー発生後 break したら当然この分のデータは欠落する。
欠落する原因自体はわかってきたが解決の難しさも見えてきた。
chan と同じような感じで使えるのが netchan のいいところだと思う。
そのためにはチャネルに送信されたデータの順番を無視するような形で再度挿入するような処理でエラーをリカバリーするのは気持ち悪い。
かといって複数のクライアントが同時に接続に来た場合の処理を考えると、送信が終わるまで待機する構造もパフォーマンス上の問題があるので用途によっては微妙そう。
色々考えているうちに、そもそもサーバ対クライアントで1対多の関係を想定した場合、サーバからクライアントに向けて順番が重要なデータを送信しようとしていること自体が結構コスト高めな処理だよなーという気がしてきた。
そうなると、そもそもサーバで Exporter から netchan.Send を作るんじゃなくて、クライアントで Exporter の netchan.Recv を作るようにした方がいいんじゃないかっていう何も解決しないとてもつまらない話にたどり着く。
依然として「1対1でプロセス間通信したいだけ」みたいな状況においてはサーバで Exporter、クライアントで Importer の構成であるべきだろうと思うしわりと簡単に上手くいきそうな感じがするので上手く読めるよう対策をしてもよさそうな気はする。
そのぐらいなら自分にでも書けそうだが、取りあえず今日は時間も遅いのでこの辺にしておこう。
package main import ( "log" "time" "netchan" "os/signal" ) func exporter() { exp, _ := netchan.NewExporter("tcp","127.0.0.1:65432") expSendChan := make(chan int) exp.Export("chan", expSendChan, netchan.Send) go func(){ for i:=1;;i++ { time.Sleep(1e9) expSendChan <- i } }() } func importer() { imp, _ := netchan.NewImporter("tcp","127.0.0.1:65432") impRecvChan := make(chan int) imp.Import("chan", impRecvChan, netchan.Recv) go func(){ for { log.Printf("%d", <-impRecvChan) } }() } func main() { exporter() importer() <-signal.Incoming }ただ、これが Importer と Exporter を別のプロセスを分けた時などに起こるような複数回 Importer が接続に行くような状況で上手くデータが転送されないみたいだ。
このソースの main() にある exporter() と importer() の呼び出しのうち片方をコメントアウトし、一方だけを呼ぶバイナリを2つ作って、Exporter 側を立ち上げっぱなしの状態で Importer を立ち上げたり終了させたりすると、Importer が切断されたあとに送られたデータのひとつ目だけが抜け落ちてしまう。
Importer が終了したタイミングを拾って
close(expSendChan) exp.Hangup("chan") expSendChan = make(chan int) exp.Export("chan", expSendChan, netchan.Send)みたいな感じで仕切りなおしすると問題なく送信できるようにはなるのだが、Importer が接続しに来ていることを検知する方法自体が用意されていないし、内部の処理さえ上手く作れば起こらなくなる類の問題なのでユーザ側で何かやるようなコードは一切書かずにライブラリ側の修正で解決するのが望ましそう。
少なくとも送信したデータが欠落しているので普通に考えて netchan のバグだろうとは思う。
netchan が使えると恐らく色々な処理がプロセスを跨いでいながら自然に書けるようになると思うので、もうちょっと詳しく追ってみようと思う。golang 触り始めたばかりの小童にどこまでわかるのやら。
---- 2010-11-26 04:15 追記
処理を追っていると、ある程度流れは見えてきたと思う。
エラーが起きているのは netchan の common.go 内にある (*encDec) encode 関数なのだが、ここに「TODO: tear down connection if there is an error?」というコメントアウトがあった。
そもそもこの辺の処理自体があまり作り込みされていないのかな? と最初は思った。
ところがここでエラー処理をしようにも、そもそもこれを呼んでいる export.go の (*expClient) serveRecv 関数にあるループの頭でやっている val := ech.ch.Recv() の時点でデータを読み取ってしまっているのでエラー発生後 break したら当然この分のデータは欠落する。
欠落する原因自体はわかってきたが解決の難しさも見えてきた。
chan と同じような感じで使えるのが netchan のいいところだと思う。
そのためにはチャネルに送信されたデータの順番を無視するような形で再度挿入するような処理でエラーをリカバリーするのは気持ち悪い。
かといって複数のクライアントが同時に接続に来た場合の処理を考えると、送信が終わるまで待機する構造もパフォーマンス上の問題があるので用途によっては微妙そう。
色々考えているうちに、そもそもサーバ対クライアントで1対多の関係を想定した場合、サーバからクライアントに向けて順番が重要なデータを送信しようとしていること自体が結構コスト高めな処理だよなーという気がしてきた。
そうなると、そもそもサーバで Exporter から netchan.Send を作るんじゃなくて、クライアントで Exporter の netchan.Recv を作るようにした方がいいんじゃないかっていう何も解決しないとてもつまらない話にたどり着く。
依然として「1対1でプロセス間通信したいだけ」みたいな状況においてはサーバで Exporter、クライアントで Importer の構成であるべきだろうと思うしわりと簡単に上手くいきそうな感じがするので上手く読めるよう対策をしてもよさそうな気はする。
そのぐらいなら自分にでも書けそうだが、取りあえず今日は時間も遅いのでこの辺にしておこう。