Edited at

UnixListener.Closeでソケットファイルが消えて困っている

More than 3 years have passed since last update.

具体的には https://github.com/golang/go/blob/f78a4c84ac8ed44aaf331989aa32e40081fd8f13/src/net/unixsock_posix.go#L339 の実装。

Go言語でGraceful Restartをする を参考にgoのhttpサーバをServer::Starter経由で起動させたいのだけど、この挙動のお蔭でunix domain socketを使った場合に困っている。


main.go

package main

import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"

"github.com/braintree/manners"
"github.com/lestrrat/go-server-starter/listener"
)

func main() {
go func() {
sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGTERM)
<-sigchan
manners.Close()
}()

l, err := listener.ListenAll()
if err != nil {
log.Fatalf("Failed to liten: %v", err)
}

http.Handle("/", handler)
err = manners.Serve(l[0], nil)
if err != nil {
log.Fatalf("Failed to Serve: %v", err)
}
}

var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "OK")
})


# サーバ起動

$ plenv exec carton exec -- start_server --path=/tmp/ok.sock -- main
start_server (pid:17571) starting now...
starting new worker 17611

# 起動状態の確認
$ ls -la /tmp/ok.sock
srwxrwxrwx 1 hiratara hiratara 0 11月 6 23:53 /tmp/ok.sock
$ echo -en 'GET / HTTP/1.0\r\n\r\n' | socat stdio /tmp/ok.sock
HTTP/1.0 200 OK
Date: Fri, 06 Nov 2015 14:53:45 GMT
Content-Length: 3
Content-Type: text/plain; charset=utf-8

OK

# 再起動
$ kill -HUP 17571

# サーバ側
received HUP, spawning a new worker
starting new worker 17633
new worker is now running, sending TERM to old workers:17611
killing old workers
old worker 17611 died, status:0

# サーバ状態の確認
$ ls -la /tmp/ok.sock
ls: /tmp/ok.sock にアクセスできません: そのようなファイルやディレクトリはありません
$ echo -en 'GET / HTTP/1.0\r\n\r\n' | socat stdio /tmp/ok.sock

以下のように Close を呼ぶ間だけディレクトリのパーミッションを奪うという最悪なラッパーを作って Wrap(l[0]) するくらいしか思いついていない状態。一応望む挙動にはなったけど。

type wrappedListener struct {

*net.UnixListener
}

func Wrap(l net.Listener) net.Listener {
switch ll := l.(type) {
case *net.UnixListener:
return &wrappedListener{UnixListener: ll}
default:
return ll
}
}

func (wl *wrappedListener) Close() (err error) {
sockPath := wl.Addr().String()
sockDir := path.Dir(sockPath)

info, err := os.Stat(sockDir)
if err != nil {
log.Fatalf("Failed to stat: %v", err)
}

origMode := info.Mode()
mode := (origMode & ^os.ModePerm) | (origMode & 0555)

err = os.Chmod(sockDir, mode)
if err != nil {
log.Fatalf("Failed to chmod: %v", err)
}

defer func() {
err := os.Chmod(sockDir, origMode)
if err != nil {
log.Fatalf("Failed to chmod: %v", err)
}
}()

return wl.UnixListener.Close()
}

過去に同じことを考えた人は https://github.com/coreos/go-systemd/issues/71#issuecomment-73351251 に居たっぽい。

もう一つ考えた方法はそもそも Close() 呼ばずにexitするで、go nutsではそうしろって言ってる。けど、Close()が呼べないとAccept()のループから処理が戻らないので https://github.com/braintree/manners/blob/master/server.go#L216 でブロックしたままになる。かといって、 manners のキモは https://github.com/braintree/manners/blob/master/server.go#L221 で全リクエストの処理が終わるのを待つことのはずなので、この行を抜ける前に別のところで勝手に exit するのはまずい気がする。


UnixListener.Close周りのコードの追っかけ

start_server は go のプロセスにファイルディスクリプタを渡してくるので、それをlistenする。 https://github.com/lestrrat/go-server-starter/blob/master/listener/listener.go#L82

おっかけていくと UnixListener 型を作っていることがわかる。 https://github.com/golang/go/blob/f78a4c84ac8ed44aaf331989aa32e40081fd8f13/src/net/file_unix.go#L94

UnixListenerClose ではファイルを消している。 https://github.com/golang/go/blob/f78a4c84ac8ed44aaf331989aa32e40081fd8f13/src/net/unixsock_posix.go#L339

l.fd.Close() だけを呼びたいのだがそのような口がなく、 l.fd はプライベートなので引っ張り出せなくて悲しい感じ。


【追記】


@kazuho さん

その設計自体がPOSIX違反>「Close()が呼べないとAccept()のループから処理が戻らない」

@tanaka_akr さん

いくつかの OS で試した結果、DragonFly BSD では close(2) でも shutdown(2) でも accept(2) しているスレッドは起きなかったので真面目にパイプを作ったという思い出があります。(Ruby の WEBrick)


POSIX では Close() を呼んだからといって Accept() が制御を戻してくれる保証はないといことでしょうか。知りませんでした。となると、 manners側の実装方針 に問題があると言えそうです。では一般的に正しい手法はどうなのかというのは、わかってないので調べて分かったら追記します。


@kazuho さん

動けばなんでもいいなら accept fdに普通のファイルデスクリプタをdup2するという手もある


こちらもgolangでどうすればっていうのが見えてないのでわかったら追記しますが、せっかく教えていただいたので一応目立つように引用しておきます。