前回: Goでメール(SMTPクライアント)はwneessen/go-mailがよさそう
wneessen/go-mail を使っているとSMTPリクエストが中断されることがたまにあって困っていたのでデバッグログで様子を見ることにした。
client.SetLogger(...)
client.SetDebugLog(true)
こんなふうにして、デバッグログを設定できるようになっている。これは標準のsmtpパッケージに手を入れて、各命令ごとにログを出すように仕込まれている。標準のsmtpパッケージはもう更新しないぞ、とあるのでそれ自体はまあいいんじゃないかな。
smtp package - net/smtp - Go Packages
そうしてログを眺めてみると、2つのリクエストが並行して起こったとき、1つめのリクエストが進行中に、2つめのEHLOを送るとバグることがわかった。
wneessen/go-mail のコード、標準のsmtpパッケージの流れを追ってみると、コネクションプールのようなものがない。そのため、クライアントを使い回した上で、リクエストを並列させると、同じコネクションで、異なるリクエストを送っていることがわかった。これによって、SMTPサーバ側が混線?して、正しい処理を行えなくなり、リクエストの処理を停止している、ような気がする。
wneessen/go-mail にコネクションプールを実装することはできそうだけど、大きく変更が必要そうなので、一旦アプリケーション側でクライアントをプールをしてみることにした。これには sql.DB のコネクションプールの実装がわかりやすかったので同じように実装した。
go/sql.go at 152ffca82fa53008bd2872f7163c7a1885da880e · golang/go
func (c *clientImpl) getFreeClient() (*gomail.Client, error) {
expire := time.Now().Add(c.timeoutForWaitFree)
for {
if clock.Now().After(expire) {
return nil, fmt.Errorf("available mail client is not found")
}
c.mux.Lock()
numFree := len(c.freeClients)
if numFree == 0 {
c.mux.Unlock()
time.Sleep(c.waitForFree)
continue
}
client := c.freeClients[0]
copy(c.freeClients, c.freeClients[1:])
c.freeClients = c.freeClients[:numFree-1]
c.mux.Unlock()
return client, nil
}
}
func (c *clientImpl) releaseClient(client *gomail.Client) {
c.freeMux.Lock()
defer c.freeMux.Unlock()
c.freeClients = append(c.freeClients, client)
}
でもって、SMTPリクエストが途切れることがなくなったのだった :ok_hand:
この記事を書いているときに思ったけれどこれならsync.Poolでもいいかも?サイズの制御ができないからコネクションが無限に増える可能性があるところが気になるくらい