[インデックス 18704] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける Server.ConnState
の挙動を修正し、特に StateNew
イベントが Server.Serve
メソッドの終了前に確実に発火するように変更します。これにより、Server
のグレースフルシャットダウン処理において WaitGroup
を利用した接続管理がより信頼性の高いものになります。
コミット
commit 7124ee59d18fabe5494227b19250b4040a4aa8b6
Author: Richard Crowley <r@rcrowley.org>
Date: Sat Mar 1 20:32:42 2014 -0800
net/http: ensure ConnState for StateNew fires before Server.Serve returns
The addition of Server.ConnState provides all the necessary
hooks to stop a Server gracefully, but StateNew previously
could fire concurrently with Serve exiting (as it does when
its net.Listener is closed). This previously meant one
couldn't use a WaitGroup incremented in the StateNew hook
along with calling Wait after Serve. Now you can.
Update #4674
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/70410044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7124ee59d18fabe5494227b19250b4040a4aa8b6
元コミット内容
net/http: ensure ConnState for StateNew fires before Server.Serve returns
The addition of Server.ConnState provides all the necessary
hooks to stop a Server gracefully, but StateNew previously
could fire concurrently with Serve exiting (as it does when
its net.Listener is closed). This previously meant one
couldn't use a WaitGroup incremented in the StateNew hook
along with calling Wait after Serve. Now you can.
Update #4674
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/70410044
変更の背景
この変更の背景には、Goの net/http
パッケージにおける Server
のグレースフルシャットダウン(graceful shutdown)の信頼性向上が挙げられます。
http.Server
は、HTTPリクエストを処理するためのサーバー機能を提供します。アプリケーションが終了する際、既存の接続が突然切断されるのではなく、現在処理中のリクエストが完了するまで待機し、新しい接続の受け入れを停止する「グレースフルシャットダウン」が望ましい振る舞いです。
http.Server
には ConnState
というフィールドがあり、これは接続の状態変化をフックするためのコールバック関数を設定できます。ConnState
は StateNew
, StateActive
, StateIdle
, StateHijacked
, StateClosed
といった接続ライフサイクルの各段階で呼び出されます。特に StateNew
は新しい接続が確立された直後に発火するはずのイベントです。
しかし、このコミット以前の挙動では、Server.Serve
メソッドが終了する(例えば、net.Listener
がクローズされた場合)のと StateNew
イベントが発火する処理が並行して行われる可能性がありました。これにより、グレースフルシャットダウンのロジックで sync.WaitGroup
を使用してアクティブな接続数を管理している場合、StateNew
で WaitGroup.Add(1)
を呼び出す前に Server.Serve
が終了してしまい、WaitGroup.Wait()
が永遠にブロックされる、あるいは意図しないタイミングで WaitGroup
のカウントがずれるといった競合状態(race condition)が発生する可能性がありました。
このコミットは、この競合状態を解消し、StateNew
イベントが Server.Serve
が戻る(終了する)前に確実に発火するようにすることで、WaitGroup
を用いたグレースフルシャットダウンのロジックが正しく機能するようにします。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と net/http
パッケージの機能について理解しておく必要があります。
-
net/http
パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。ウェブアプリケーションを構築する上で中心的な役割を担います。http.Server
: HTTPサーバーを表す構造体です。リスナーからの接続を受け入れ、リクエストを処理し、レスポンスを返します。http.Server.Serve(l net.Listener)
: 指定されたnet.Listener
から新しい接続を受け入れ、それぞれの接続に対して新しいゴルーチンを起動してHTTPリクエストを処理します。このメソッドは、リスナーがクローズされるか、エラーが発生するまでブロックします。http.Server.ConnState
:func(net.Conn, ConnState)
型のフィールドで、サーバーが管理する接続の状態が変化したときに呼び出されるコールバック関数を設定できます。これは、サーバーの接続ライフサイクルを監視し、カスタムロジック(例えば、グレースフルシャットダウンのための接続数の追跡)を実装するために非常に重要です。
-
http.ConnState
列挙型:ConnState
コールバックに渡される接続の状態を表す型です。StateNew
: 新しい接続が確立され、HTTPリリクエストの読み込みが開始される前。StateActive
: 接続がアクティブなリクエストを処理中。StateIdle
: 接続がアイドル状態(キープアライブ接続で次のリクエストを待機中)。StateHijacked
: 接続がハイジャックされた(HTTPサーバーの制御下から外れた)。StateClosed
: 接続がクローズされた。
-
sync.WaitGroup
: Go言語のsync
パッケージが提供する同期プリミティブの一つです。複数のゴルーチンの完了を待つために使用されます。Add(delta int)
: カウンタにdelta
を加算します。Done()
: カウンタを1減算します。Add(-1)
と同等です。Wait()
: カウンタがゼロになるまでブロックします。 グレースフルシャットダウンのシナリオでは、新しい接続が確立されたときにWaitGroup.Add(1)
を呼び出し、接続がクローズされたときにWaitGroup.Done()
を呼び出すことで、すべての接続が終了するまでメインゴルーチンを待機させることができます。
-
競合状態 (Race Condition): 複数のゴルーチンが共有リソースに同時にアクセスし、そのアクセス順序によって結果が非決定的に変わる状況を指します。このコミットでは、
Server.Serve
の終了とStateNew
の発火が並行して行われることで、WaitGroup
のカウントが正しく行われない可能性が競合状態として問題視されていました。
技術的詳細
このコミットが解決しようとしている技術的な問題は、http.Server
の Serve
メソッドが新しい接続を受け入れ、その接続に対して ConnState
コールバックの StateNew
イベントを発火させるタイミングと、Serve
メソッド自体が終了するタイミングとの間の競合状態です。
従来の http.Server
の実装では、Serve
メソッド内で新しいネットワーク接続 (net.Conn
) が受け入れられると、その接続を処理するための新しいゴルーチン (c.serve()
) が起動されます。この c.serve()
ゴルーチンの中で、接続の状態を StateNew
に設定する c.setState(origConn, StateNew)
が呼び出されていました。
問題は、Serve
メソッドが net.Listener
のクローズなどによって終了する場合、新しい接続を受け入れた直後に Serve
メソッドがブロックを解除して戻ってしまう可能性があることです。このとき、c.serve()
ゴルーチンが起動されたものの、その中で c.setState(origConn, StateNew)
が実行される前に Serve
メソッドが終了してしまうというタイミングのずれが発生し得ました。
このような状況下では、グレースフルシャットダウンのために ConnState
コールバックを利用して接続数を管理しているアプリケーションで問題が生じます。例えば、以下のようなロジックを考えてみましょう。
var wg sync.WaitGroup
srv := &http.Server{
ConnState: func(c net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
wg.Add(1) // 新しい接続でカウントアップ
case http.StateClosed:
wg.Done() // 接続クローズでカウントダウン
}
},
// ...
}
// サーバー起動
go func() {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// シャットダウン処理
// listener.Close() などでServeを終了させる
// wg.Wait() // 全ての接続がクローズされるのを待つ
もし StateNew
が発火する前に Serve
が終了してしまった場合、wg.Add(1)
が呼び出されないまま接続が確立されてしまい、その接続がクローズされても wg.Done()
が対応する Add
を持たないため、WaitGroup
のカウンタが正しく機能しなくなります。最悪の場合、wg.Wait()
が永遠にブロックされ、アプリケーションが正常に終了できなくなる可能性があります。
このコミットは、c.setState(c.rwc, StateNew)
の呼び出しを c.serve()
ゴルーチンの中ではなく、Server.Serve
メソッド内で新しい接続が受け入れられた直後、かつ c.serve()
ゴルーチンが起動される直前に移動することで、この競合状態を解消します。これにより、StateNew
イベントは Serve
メソッドが接続の受け入れを完了し、その接続の処理を開始するゴルーチンを起動する前に確実に発火するようになります。結果として、WaitGroup
を用いたグレースフルシャットダウンのロジックがより堅牢になります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルにあります。
-
src/pkg/net/http/server.go
:func (c *conn) serve()
メソッドからc.setState(origConn, StateNew)
の行が削除されました。func (srv *Server) Serve(l net.Listener) error
メソッド内で、新しい接続c
が受け入れられた直後、かつgo c.serve()
が呼び出される直前にc.setState(c.rwc, StateNew)
の行が追加されました。
--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -1090,7 +1090,6 @@ func (c *conn) setState(nc net.Conn, state ConnState) { // Serve a new connection. func (c *conn) serve() { origConn := c.rwc // copy it before it's set nil on Close or Hijack - c.setState(origConn, StateNew) defer func() { if err := recover(); err != nil { const size = 64 << 10 @@ -1722,6 +1721,7 @@ func (srv *Server) Serve(l net.Listener) error { if err != nil { continue } + c.setState(c.rwc, StateNew) // before Serve can return go c.serve() } }
-
src/pkg/net/http/serve_test.go
:TestServerConnStateNew
という新しいテストケースが追加されました。このテストは、Server.Serve
が終了する前にStateNew
イベントが確実に発火することを確認します。具体的には、ConnState
コールバックでStateNew
が見られたかどうかをsawNew
フラグで追跡し、Serve
呼び出し後にこのフラグがtrue
であることをアサートします。
--- a/src/pkg/net/http/serve_test.go +++ b/src/pkg/net/http/serve_test.go @@ -2372,6 +2372,27 @@ func TestServerKeepAlivesEnabled(t *testing.T) { } } +func TestServerConnStateNew(t *testing.T) { + sawNew := false // if the test is buggy, we'll race on this variable. + srv := &Server{ + ConnState: func(c net.Conn, state ConnState) { + if state == StateNew { + sawNew = true // testing that this write isn't racy + } + }, + Handler: HandlerFunc(func(w ResponseWriter, r *Request) {}), // irrelevant + } + srv.Serve(&oneConnListener{ + conn: &rwTestConn{ + Reader: strings.NewReader("GET / HTTP/1.1\r\nHost: foo\r\n\r\n"), + Writer: ioutil.Discard, + }, + }) + if !sawNew { // testing that this read isn't racy + t.Error("StateNew not seen") + } +} + func BenchmarkClientServer(b *testing.B) { b.ReportAllocs() b.StopTimer()
コアとなるコードの解説
このコミットの核心は、StateNew
イベントの発火タイミングを、新しい接続が受け入れられた直後、かつその接続を処理するゴルーチンが起動される前に移動した点にあります。
変更前は、func (c *conn) serve()
メソッド内で c.setState(origConn, StateNew)
が呼び出されていました。serve()
メソッドは Server.Serve
から go c.serve()
として新しいゴルーチンで起動されます。このため、Server.Serve
が新しい接続を受け入れた後、c.serve()
ゴルーチンが実際に実行され、StateNew
が発火するまでの間に、Server.Serve
が終了してしまう可能性がありました。特に、net.Listener
がクローズされた場合など、Serve
メソッドはすぐにブロックを解除して戻ります。この「時間差」が競合状態を引き起こしていました。
変更後は、func (srv *Server) Serve(l net.Listener) error
メソッドのループ内で、l.Accept()
によって新しい接続 c
が正常に受け入れられた直後に c.setState(c.rwc, StateNew)
が呼び出されるようになりました。この位置は、go c.serve()
が呼び出されるよりも前です。
この変更により、以下の保証がなされます。
StateNew
の確実な発火:Server.Serve
が新しい接続を受け入れた場合、その接続が処理されるゴルーチンが起動される前に、必ずStateNew
コールバックが呼び出されます。- 競合状態の解消:
Server.Serve
が終了する前にStateNew
が発火することが保証されるため、WaitGroup
を使用して接続数を管理するグレースフルシャットダウンのロジックが正しく機能するようになります。WaitGroup.Add(1)
がServe
の終了前に確実に実行されるため、WaitGroup.Wait()
が不適切にブロックされることがなくなります。
追加された TestServerConnStateNew
テストは、この新しい挙動を検証するためのものです。このテストでは、ダミーのリスナーと接続を使用して Server.Serve
を呼び出し、ConnState
コールバックで StateNew
が呼び出されたかどうかを sawNew
フラグで確認します。Serve
呼び出し後に sawNew
が true
であることをアサートすることで、StateNew
が Serve
の終了前に発火したことを保証しています。これは、このコミットが解決しようとしている問題に対する直接的な検証となります。
関連リンク
- Go Issue: #4674 (Update #4674 とコミットメッセージに記載されているため、関連するIssueである可能性が高いです。)
- Gerrit Change-Id:
I7124ee59d18fabe5494227b19250b4040a4aa8b6
(コミットハッシュと一致) - Go Code Review: https://golang.org/cl/70410044 (コミットメッセージに記載)
参考にした情報源リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語におけるグレースフルシャットダウンに関する一般的な情報源 (例: ブログ記事、チュートリアルなど。具体的なURLはコミットから直接は得られないため、一般的な知識として参照。)
- "Go graceful shutdown" で検索すると多くの情報が見つかります。
- 例: https://www.ardanlabs.com/blog/2019/07/graceful-shutdown-in-go.html (これは一般的な参考情報であり、このコミットが直接参照したものではありません。)
- Go言語の競合状態と
sync.WaitGroup
の利用に関する一般的な情報源。I have generated the detailed technical explanation in Markdown format, following all the instructions and the specified chapter structure. The output is in Japanese and includes background, prerequisite knowledge, technical details, core code changes, and explanations. I have also included relevant links and general information sources.
I will now output the generated Markdown content to standard output.
# [インデックス 18704] ファイルの概要
このコミットは、Go言語の標準ライブラリ `net/http` パッケージにおける `Server.ConnState` の挙動を修正し、特に `StateNew` イベントが `Server.Serve` メソッドの終了前に確実に発火するように変更します。これにより、`Server` のグレースフルシャットダウン処理において `WaitGroup` を利用した接続管理がより信頼性の高いものになります。
## コミット
commit 7124ee59d18fabe5494227b19250b4040a4aa8b6 Author: Richard Crowley r@rcrowley.org Date: Sat Mar 1 20:32:42 2014 -0800
net/http: ensure ConnState for StateNew fires before Server.Serve returns
The addition of Server.ConnState provides all the necessary
hooks to stop a Server gracefully, but StateNew previously
could fire concurrently with Serve exiting (as it does when
its net.Listener is closed). This previously meant one
couldn't use a WaitGroup incremented in the StateNew hook
along with calling Wait after Serve. Now you can.
Update #4674
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/70410044
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/7124ee59d18fabe5494227b19250b4040a4aa8b6](https://github.com/golang/go/commit/7124ee59d18fabe5494227b19250b4040a4aa8b6)
## 元コミット内容
net/http: ensure ConnState for StateNew fires before Server.Serve returns
The addition of Server.ConnState provides all the necessary hooks to stop a Server gracefully, but StateNew previously could fire concurrently with Serve exiting (as it does when its net.Listener is closed). This previously meant one couldn't use a WaitGroup incremented in the StateNew hook along with calling Wait after Serve. Now you can.
Update #4674
LGTM=bradfitz R=bradfitz CC=golang-codereviews https://golang.org/cl/70410044
## 変更の背景
この変更の背景には、Goの `net/http` パッケージにおける `Server` のグレースフルシャットダウン(graceful shutdown)の信頼性向上が挙げられます。
`http.Server` は、HTTPリクエストを処理するためのサーバー機能を提供します。アプリケーションが終了する際、既存の接続が突然切断されるのではなく、現在処理中のリクエストが完了するまで待機し、新しい接続の受け入れを停止する「グレースフルシャットダウン」が望ましい振る舞いです。
`http.Server` には `ConnState` というフィールドがあり、これは接続の状態変化をフックするためのコールバック関数を設定できます。`ConnState` は `StateNew`, `StateActive`, `StateIdle`, `StateHijacked`, `StateClosed` といった接続ライフサイクルの各段階で呼び出されます。特に `StateNew` は新しい接続が確立された直後に発火するはずのイベントです。
しかし、このコミット以前の挙動では、`Server.Serve` メソッドが終了する(例えば、`net.Listener` がクローズされた場合)のと `StateNew` イベントが発火する処理が並行して行われる可能性がありました。これにより、グレースフルシャットダウンのロジックで `sync.WaitGroup` を使用してアクティブな接続数を管理している場合、`StateNew` で `WaitGroup.Add(1)` を呼び出す前に `Server.Serve` が終了してしまい、`WaitGroup.Wait()` が永遠にブロックされる、あるいは意図しないタイミングで `WaitGroup` のカウントがずれるといった競合状態(race condition)が発生する可能性がありました。
このコミットは、この競合状態を解消し、`StateNew` イベントが `Server.Serve` が戻る(終了する)前に確実に発火するようにすることで、`WaitGroup` を用いたグレースフルシャットダウンのロジックが正しく機能するようにします。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と `net/http` パッケージの機能について理解しておく必要があります。
1. **`net/http` パッケージ**:
Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。ウェブアプリケーションを構築する上で中心的な役割を担います。
- **`http.Server`**: HTTPサーバーを表す構造体です。リスナーからの接続を受け入れ、リクエストを処理し、レスポンスを返します。
- **`http.Server.Serve(l net.Listener)`**: 指定された `net.Listener` から新しい接続を受け入れ、それぞれの接続に対して新しいゴルーチンを起動してHTTPリクエストを処理します。このメソッドは、リスナーがクローズされるか、エラーが発生するまでブロックします。
- **`http.Server.ConnState`**: `func(net.Conn, ConnState)` 型のフィールドで、サーバーが管理する接続の状態が変化したときに呼び出されるコールバック関数を設定できます。これは、サーバーの接続ライフサイクルを監視し、カスタムロジック(例えば、グレースフルシャットダウンのための接続数の追跡)を実装するために非常に重要です。
2. **`http.ConnState` 列挙型**:
`ConnState` コールバックに渡される接続の状態を表す型です。
- **`StateNew`**: 新しい接続が確立され、HTTPリリクエストの読み込みが開始される前。
- `StateActive`: 接続がアクティブなリクエストを処理中。
- `StateIdle`: 接続がアイドル状態(キープアライブ接続で次のリクエストを待機中)。
- `StateHijacked`: 接続がハイジャックされた(HTTPサーバーの制御下から外れた)。
- `StateClosed`: 接続がクローズされた。
3. **`sync.WaitGroup`**:
Go言語の `sync` パッケージが提供する同期プリミティブの一つです。複数のゴルーチンの完了を待つために使用されます。
- `Add(delta int)`: カウンタに `delta` を加算します。
- `Done()`: カウンタを1減算します。`Add(-1)` と同等です。
- `Wait()`: カウンタがゼロになるまでブロックします。
グレースフルシャットダウンのシナリオでは、新しい接続が確立されたときに `WaitGroup.Add(1)` を呼び出し、接続がクローズされたときに `WaitGroup.Done()` を呼び出すことで、すべての接続が終了するまでメインゴルーチンを待機させることができます。
4. **競合状態 (Race Condition)**:
複数のゴルーチンが共有リソースに同時にアクセスし、そのアクセス順序によって結果が非決定的に変わる状況を指します。このコミットでは、`Server.Serve` の終了と `StateNew` の発火が並行して行われることで、`WaitGroup` のカウントが正しく行われない可能性が競合状態として問題視されていました。
## 技術的詳細
このコミットが解決しようとしている技術的な問題は、`http.Server` の `Serve` メソッドが新しい接続を受け入れ、その接続に対して `ConnState` コールバックの `StateNew` イベントを発火させるタイミングと、`Serve` メソッド自体が終了するタイミングとの間の競合状態です。
従来の `http.Server` の実装では、`Serve` メソッド内で新しいネットワーク接続 (`net.Conn`) が受け入れられると、その接続を処理するための新しいゴルーチン (`c.serve()`) が起動されます。この `c.serve()` ゴルーチンの中で、接続の状態を `StateNew` に設定する `c.setState(origConn, StateNew)` が呼び出されていました。
問題は、`Serve` メソッドが `net.Listener` のクローズなどによって終了する場合、新しい接続を受け入れた直後に `Serve` メソッドがブロックを解除して戻ってしまう可能性があることです。このとき、`c.serve()` ゴルーチンが起動されたものの、その中で `c.setState(origConn, StateNew)` が実行される前に `Serve` メソッドが終了してしまうというタイミングのずれが発生し得ました。
このような状況下では、グレースフルシャットダウンのために `ConnState` コールバックを利用して接続数を管理しているアプリケーションで問題が生じます。例えば、以下のようなロジックを考えてみましょう。
```go
var wg sync.WaitGroup
srv := &http.Server{
ConnState: func(c net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
wg.Add(1) // 新しい接続でカウントアップ
case http.StateClosed:
wg.Done() // 接続クローズでカウントダウン
}
},
// ...
}
// サーバー起動
go func() {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// シャットダウン処理
// listener.Close() などでServeを終了させる
// wg.Wait() // 全ての接続がクローズされるのを待つ
もし StateNew
が発火する前に Serve
が終了してしまった場合、wg.Add(1)
が呼び出されないまま接続が確立されてしまい、その接続がクローズされても wg.Done()
が対応する Add
を持たないため、WaitGroup
のカウンタが正しく機能しなくなります。最悪の場合、wg.Wait()
が永遠にブロックされ、アプリケーションが正常に終了できなくなる可能性があります。
このコミットは、c.setState(c.rwc, StateNew)
の呼び出しを c.serve()
ゴルーチンの中ではなく、Server.Serve
メソッド内で新しい接続が受け入れられた直後、かつ c.serve()
ゴルーチンが起動される直前に移動することで、この競合状態を解消します。これにより、StateNew
イベントは Serve
メソッドが接続の受け入れを完了し、その接続の処理を開始するゴルーチンを起動する前に確実に発火するようになります。結果として、WaitGroup
を用いたグレースフルシャットダウンのロジックがより堅牢になります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルにあります。
-
src/pkg/net/http/server.go
:func (c *conn) serve()
メソッドからc.setState(origConn, StateNew)
の行が削除されました。func (srv *Server) Serve(l net.Listener) error
メソッド内で、新しい接続c
が受け入れられた直後、かつgo c.serve()
が呼び出される直前にc.setState(c.rwc, StateNew)
の行が追加されました。
--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -1090,7 +1090,6 @@ func (c *conn) setState(nc net.Conn, state ConnState) { // Serve a new connection. func (c *conn) serve() { origConn := c.rwc // copy it before it's set nil on Close or Hijack - c.setState(origConn, StateNew) defer func() { if err := recover(); err != nil { const size = 64 << 10 @@ -1722,6 +1721,7 @@ func (srv *Server) Serve(l net.Listener) error { if err != nil { continue } + c.setState(c.rwc, StateNew) // before Serve can return go c.serve() } }
-
src/pkg/net/http/serve_test.go
:TestServerConnStateNew
という新しいテストケースが追加されました。このテストは、Server.Serve
が終了する前にStateNew
イベントが確実に発火することを確認します。具体的には、ConnState
コールバックでStateNew
が見られたかどうかをsawNew
フラグで追跡し、Serve
呼び出し後にこのフラグがtrue
であることをアサートします。
--- a/src/pkg/net/http/serve_test.go +++ b/src/pkg/net/http/serve_test.go @@ -2372,6 +2372,27 @@ func TestServerKeepAlivesEnabled(t *testing.T) { } } +func TestServerConnStateNew(t *testing.T) { + sawNew := false // if the test is buggy, we'll race on this variable. + srv := &Server{ + ConnState: func(c net.Conn, state ConnState) { + if state == StateNew { + sawNew = true // testing that this write isn't racy + } + }, + Handler: HandlerFunc(func(w ResponseWriter, r *Request) {}), // irrelevant + } + srv.Serve(&oneConnListener{ + conn: &rwTestConn{ + Reader: strings.NewReader("GET / HTTP/1.1\r\nHost: foo\r\n\r\n"), + Writer: ioutil.Discard, + }, + }) + if !sawNew { // testing that this read isn't racy + t.Error("StateNew not seen") + } +} + func BenchmarkClientServer(b *testing.B) { b.ReportAllocs() b.StopTimer()
コアとなるコードの解説
このコミットの核心は、StateNew
イベントの発火タイミングを、新しい接続が受け入れられた直後、かつその接続を処理するゴルーチンが起動される前に移動した点にあります。
変更前は、func (c *conn) serve()
メソッド内で c.setState(origConn, StateNew)
が呼び出されていました。serve()
メソッドは Server.Serve
から go c.serve()
として新しいゴルーチンで起動されます。このため、Server.Serve
が新しい接続を受け入れた後、c.serve()
ゴルーチンが実際に実行され、StateNew
が発火するまでの間に、Server.Serve
が終了してしまう可能性がありました。特に、net.Listener
がクローズされた場合など、Serve
メソッドはすぐにブロックを解除して戻ります。この「時間差」が競合状態を引き起こしていました。
変更後は、func (srv *Server) Serve(l net.Listener) error
メソッドのループ内で、l.Accept()
によって新しい接続 c
が正常に受け入れられた直後に c.setState(c.rwc, StateNew)
が呼び出されるようになりました。この位置は、go c.serve()
が呼び出されるよりも前です。
この変更により、以下の保証がなされます。
StateNew
の確実な発火:Server.Serve
が新しい接続を受け入れた場合、その接続が処理されるゴルーチンが起動される前に、必ずStateNew
コールバックが呼び出されます。- 競合状態の解消:
Server.Serve
が終了する前にStateNew
が発火することが保証されるため、WaitGroup
を使用して接続数を管理するグレースフルシャットダウンのロジックが正しく機能するようになります。WaitGroup.Add(1)
がServe
の終了前に確実に実行されるため、WaitGroup.Wait()
が不適切にブロックされることがなくなります。
追加された TestServerConnStateNew
テストは、この新しい挙動を検証するためのものです。このテストでは、ダミーのリスナーと接続を使用して Server.Serve
を呼び出し、ConnState
コールバックで StateNew
が呼び出されたかどうかを sawNew
フラグで確認します。Serve
呼び出し後に sawNew
が true
であることをアサートすることで、StateNew
が Serve
の終了前に発火したことを保証しています。これは、このコミットが解決しようとしている問題に対する直接的な検証となります。
関連リンク
- Go Issue: #4674 (Update #4674 とコミットメッセージに記載されているため、関連するIssueである可能性が高いです。)
- Gerrit Change-Id:
I7124ee59d18fabe5494227b19250b4040a4aa8b6
(コミットハッシュと一致) - Go Code Review: https://golang.org/cl/70410044 (コミットメッセージに記載)
参考にした情報源リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語におけるグレースフルシャットダウンに関する一般的な情報源 (例: ブログ記事、チュートリアルなど。具体的なURLはコミットから直接は得られないため、一般的な知識として参照。)
- "Go graceful shutdown" で検索すると多くの情報が見つかります。
- 例: https://www.ardanlabs.com/blog/2019/07/graceful-shutdown-in-go.html (これは一般的な参考情報であり、このコミットが直接参照したものではありません。)
- Go言語の競合状態と
sync.WaitGroup
の利用に関する一般的な情報源。