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を使って定期的に行う予定。
 

と、ここまで書いたけど、この実装って本当に正しいのかわからんので相変わらずツッコミをお待ちしております。