DevDevデブ!!

プログラミングのこととか書きます。多分。。。

zapはgoroutine safeなのか??

READMEに明記されてないので調べてた

(というかzapはドキュメント薄い。。。ソース読まないと分からん)

(基本goroutine safeと明記されてないものはgoroutine safeでは無いんだけど)

github.com

事の発端

zapのリポジトリのREADMEにはgoroutine-safeの記載は無かったので、zapの中でgoroutine-safeにするためのapiとかサポートされてないのかと思ってググったら、以下のissueを見つけた。

github.com

対応するPR

github.com

zapは実際にログを書き込む際に、io.Writerをwrapしたinterfaceであるzap.WriteSyncerというinterface使うのだけど、それを更にLock対応のLockWriteSyncerというstructにwrapする形になる。

以下は現在のmasterブランチのlogger.goからの抜粋

type lockedWriteSyncer struct {
    sync.Mutex
    ws WriteSyncer
}

// Lock wraps a WriteSyncer in a mutex to make it safe for concurrent use. In
// particular, *os.Files must be locked before use.
func Lock(ws WriteSyncer) WriteSyncer {
    if _, ok := ws.(*lockedWriteSyncer); ok {
        // no need to layer on another lock
        return ws
    }
    return &lockedWriteSyncer{ws: ws}
}

func (s *lockedWriteSyncer) Write(bs []byte) (int, error) {
    s.Lock()
    n, err := s.ws.Write(bs)
    s.Unlock()
    return n, err
}

lockedWriteSyncerのWriteはWriteSyncerのwriteを叩く前にsync.Mutexでlockをかけるので、goroutine safeになっている。

lockedWriteSyncerもWriteSyncerのinterfaceを満たしているので、zapの他のAPIには影響がないというわけだ。

よって、WriteSyncerをzap.Lockに渡してwrapしてあげると、goroutine safeになる。

zapの標準プリセットはgoroutine safe?

zapには予め用意された設定(以後、便宜的に標準プリセットと呼ぶ)でロガーのインスタンスを作成する関数がいくつか用意されています。

zap.NewDevelopmentとか、zap.NewProductionとか

これらの関数で作成されるロガーは、goroutine safeなのかどうかソースを読んで確認した。

結論からいうと、goroutine safe

上述のzap.Lockの呼び出し箇所を確認したところ、Config構造体のopenSinksという関数の中で、引数に指定された複数のファイルパスからWriteSyncerを返すOpenという関数が叩かれており、その中で叩かれているCombineWriteSyncersという関数の中で、最後にzap.LockでWriteSyncerをwrapしたものをreturnしている。

なので、標準プリセットがどうというより、Config構造体のBuild関数を叩いて作成したLoggerについてはgoroutine safeとなるようだ。

ところでgodocをちゃんと読むと気になる記載が

以下、zapのlogger.goのLogger構造体の抜粋

// A Logger provides fast, leveled, structured logging. All methods are safe
// for concurrent use.
//
// The Logger is designed for contexts in which every microsecond and every
// allocation matters, so its API intentionally favors performance and type
// safety over brevity. For most applications, the SugaredLogger strikes a
// better balance between performance and ergonomics.
type Logger struct {
    core zapcore.Core

    development bool
    name        string
    errorOutput zapcore.WriteSyncer

    addCaller bool
    addStack  zapcore.LevelEnabler

    callerSkip int
}

「All methods are safe for concurrent use.」

はい??

InfoとかWarnとかのログ出力メソッドについては、zapcore.Coreの実装次第だと思うんだけども。。。?

このgodocの記載は信用していいのか。。。。?

そもそもio.Writer.writeがgoroutine safeだったりするケースありません?

zapでは(他のloggingライブラリもそうだと思うけど)最終的にio.Writerのwriteを叩くことになるので、io.Writerの実装次第では内部でlockをとっており、writeの呼び出し側ではlockしてあげる必要なかったりするのではと思って調べた。

(逆にいうと、io.Writerの実装依存なので、loggingライブラリ側ではwriteを叩く前にlockするのが妥当と言える)

とりあえず、os.Fileに関しては、writeの内部でlockがかかってるように見える

以下はfd_unix.goからの抜粋

// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
    if err := fd.writeLock(); err != nil {
        return 0, err
    }
    defer fd.writeUnlock()
    if err := fd.pd.prepareWrite(fd.isFile); err != nil {
        return 0, err
    }
    var nn int
    for {
        max := len(p)
        if fd.IsStream && max-nn > maxRW {
            max = nn + maxRW
        }
        n, err := syscall.Write(fd.Sysfd, p[nn:max])
        if n > 0 {
            nn += n
        }
        if nn == len(p) {
            return nn, err
        }
        if err == syscall.EAGAIN && fd.pd.pollable() {
            if err = fd.pd.waitWrite(fd.isFile); err == nil {
                continue
            }
        }
        if err != nil {
            return nn, err
        }
        if n == 0 {
            return nn, io.ErrUnexpectedEOF
        }
    }
}

先頭行の方で何やらlockをかけているのが分かる。

しかし、この実装はgo1.9からなんだよね。

go1.8ではfd_unix.goのパッケージ階層自体違うし、上記のようなlockはかかっていない。

そして、go1.9のリリースノートを確認したところ、以下の記載がある。

The os package now uses the internal runtime poller for file I/O. This reduces the number of threads required for read/write operations on pipes, and it eliminates races when one goroutine closes a file while another is using the file for I/O.

goroutine safeにしたとは言ってないような。。。???

(結果的にgoroutine safeになってるかもしれないけど)

これは非goroutine safeとして扱ったほうが良さそう