[インデックス 18683] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける Transport.CancelRequest
メソッドの機能を拡張し、ネットワーク接続の確立(ダイヤル)中にブロックされているリクエストもキャンセルできるようにするものです。これにより、リクエストがタイムアウトしたり、ユーザーによって明示的にキャンセルされたりした場合に、不要なネットワークリソースの消費やアプリケーションのハングアップを防ぎ、より堅牢なHTTPクライアントの動作を実現します。
コミット
commit dc6bf295b95f3b1141e81fea3e128d22e4282962
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Feb 27 13:32:40 2014 -0800
net/http: make Transport.CancelRequest work for requests blocked in a dial
Fixes #6951
LGTM=josharian
R=golang-codereviews, josharian
CC=golang-codereviews
https://golang.org/cl/69280043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dc6bf295b95f3b1141e81fea3e128d22e4282962
元コミット内容
net/http: make Transport.CancelRequest work for requests blocked in a dial
このコミットは、net/http
パッケージの Transport.CancelRequest
メソッドが、ネットワーク接続の確立(ダイヤル)中にブロックされているリクエストに対しても機能するように変更します。これは、Go issue #6951 で報告された問題を解決するためのものです。
変更の背景
この変更の背景には、Go言語の net/http
クライアントが、ネットワーク接続の確立(TCP接続の確立やTLSハンドシェイクなど)に時間がかかっている間に、リクエストをキャンセルできないという問題がありました。具体的には、Transport.CancelRequest
メソッドは、リクエストが既に接続プールから取得された persistConn
(永続的な接続) に関連付けられている場合にのみ機能していました。しかし、リクエストがまだ接続を取得しておらず、dial
(ダイヤル) 処理によってブロックされている間は、キャンセル要求が無視され、リクエストはダイヤル処理が完了するまで待機し続けるという挙動を示していました。
Go issue #6951 では、この問題が明確に指摘されています。ユーザーが http.Client
を使用してリクエストを送信し、そのリクエストがネットワークの遅延や到達不能なホストのためにダイヤル処理でブロックされた場合、CancelRequest
を呼び出してもリクエストが中断されず、アプリケーションがハングアップする可能性がありました。これは、特にモバイル環境や不安定なネットワーク環境において、ユーザーエクスペリエンスを著しく損なう可能性がありました。
このコミットは、CancelRequest
がダイヤル処理中のリクエストも中断できるようにすることで、この問題を解決し、net/http
クライアントの堅牢性と応答性を向上させることを目的としています。これにより、アプリケーションは不要なネットワーク操作を早期に終了させ、リソースを解放し、より迅速にエラー状態に対応できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の net/http
パッケージおよび関連する概念についての知識が必要です。
net/http.Transport
:http.Client
の基盤となる構造体で、HTTPリクエストの実際の送信、接続の管理(接続の再利用、キープアライブ)、プロキシ設定、TLS設定などを担当します。Transport
は、複数のリクエストに対して接続を再利用することで、パフォーマンスを向上させます。http.Request
とhttp.Response
: HTTPリクエストとレスポンスを表す構造体です。Transport.CancelRequest(req *Request)
: このメソッドは、指定された*http.Request
に関連付けられた進行中のHTTPリクエストをキャンセルするために使用されます。以前は、このメソッドはリクエストが既に接続にバインドされている場合にのみ有効でした。dial
処理: ネットワーク接続を確立するプロセスを指します。これには、DNSルックアップ、TCPハンドシェイク、TLSハンドシェイクなどが含まれます。これらの処理は、ネットワークの状態によっては時間がかかり、ブロックされる可能性があります。persistConn
:net/http
パッケージ内部で使用される構造体で、単一のHTTP/1.x接続(TCP接続)を抽象化し、その接続上でのリクエストとレスポンスの送受信を管理します。Transport
はpersistConn
のプールを管理し、接続の再利用を可能にします。- Goの並行処理 (Goroutines and Channels): Go言語は、軽量なスレッドであるGoroutineと、Goroutine間の安全な通信を可能にするChannelを介して並行処理をサポートします。このコミットでは、ダイヤル処理をGoroutineで実行し、Channelを使ってその結果を待機し、同時にキャンセルシグナルも受け取れるようにすることで、非同期的なキャンセルを実現しています。
select
ステートメント: 複数の通信操作(Channelの送受信)を同時に待機し、準備ができた最初の操作を実行するために使用されます。このコミットでは、ダイヤル処理の完了とキャンセルシグナルの両方を待機するためにselect
が活用されています。
技術的詳細
このコミットの技術的な核心は、Transport.CancelRequest
が、ネットワーク接続の確立(dial
)中にブロックされているリクエストを中断できるようにするために、Transport
構造体と getConn
メソッドの内部ロジックが変更された点にあります。
主な変更点は以下の通りです。
-
Transport.reqConn
からTransport.reqCanceler
への変更:- 以前の
Transport
構造体にはreqConn map[*Request]*persistConn
というフィールドがありました。これは、特定のリクエストがどのpersistConn
に関連付けられているかを追跡するためのものでした。CancelRequest
はこのマップを使用して、キャンセル対象のリクエストに対応するpersistConn
を見つけ、その接続をクローズすることでキャンセルを実現していました。 - このコミットでは、
reqConn
がreqCanceler map[*Request]func()
に変更されました。これは、リクエストごとにfunc()
型のキャンセル関数を登録できるようにするためのものです。このキャンセル関数は、リクエストがキャンセルされたときに実行されるべきロジック(例えば、ダイヤル処理を中断する、接続をクローズするなど)をカプセル化します。これにより、リクエストがpersistConn
にバインドされているかどうかにかかわらず、より柔軟なキャンセルメカニズムが提供されます。
- 以前の
-
getConn
メソッドのシグネチャ変更と内部ロジックの修正:getConn
メソッドは、HTTPリクエストを送信するためのpersistConn
を取得する役割を担っています。以前はfunc (t *Transport) getConn(cm connectMethod) (*persistConn, error)
でしたが、func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error)
に変更され、*http.Request
引数を受け取るようになりました。これにより、getConn
の内部で、現在処理中のリクエストに対してキャンセル関数を登録できるようになります。getConn
の内部では、ダイヤル処理(t.dialConn(cm)
)が新しいGoroutineで実行され、その結果はdialc
というチャネルに送信されます。- 最も重要な変更は、
cancelc := make(chan struct{})
という新しいチャネルが導入されたことです。このチャネルは、リクエストがキャンセルされたときにシグナルを受け取るために使用されます。 t.setReqCanceler(req, func() { close(cancelc) })
を呼び出すことで、現在のリクエスト (req
) に対応するキャンセル関数が登録されます。このキャンセル関数は、cancelc
チャネルをクローズするだけのシンプルなものです。CancelRequest
が呼び出されると、この登録された関数が実行され、cancelc
がクローズされます。getConn
の中で、select
ステートメントが導入されました。このselect
は、以下の2つのイベントを同時に待機します。case v := <-dialc:
: ダイヤル処理が完了し、結果がdialc
チャネルから受信された場合。case <-cancelc:
: リクエストがキャンセルされ、cancelc
チャネルがクローズされた場合。
- もし
cancelc
が先にクローズされた場合(つまり、リクエストがダイヤル中にキャンセルされた場合)、select
はcase <-cancelc:
ブロックを実行し、"net/http: request canceled while waiting for connection"
というエラーを返して、ダイヤル処理を中断します。この際、バックグラウンドで実行中のダイヤル処理が完了した際には、その接続はアイドル接続プールに返却されるようにhandlePendingDial()
がGoroutineで実行されます。これにより、リソースリークを防ぎます。
-
export_test.go
とtransport_test.go
の変更:export_test.go
では、NumPendingRequestsForTesting
メソッドがt.reqConn
の代わりにt.reqCanceler
の長さを返すように変更されました。transport_test.go
では、TestTransportCancelRequestInDial
という新しいテストケースが追加されました。このテストは、Dial
関数をブロックするように設定されたTransport
を使用し、ダイヤル中にCancelRequest
を呼び出すことで、リクエストが正しくキャンセルされることを検証します。これにより、このコミットが解決しようとしている問題が実際に解決されたことを保証します。
これらの変更により、Transport.CancelRequest
は、リクエストが接続を待機している間(特にダイヤル処理中)でも、そのリクエストを効果的に中断できるようになり、net/http
クライアントの応答性と信頼性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/net/http/transport.go
に集中しています。
-
Transport
構造体のフィールド変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -47,13 +47,13 @@ const DefaultMaxIdleConnsPerHost = 2 // https, and http proxies (for either http or https with CONNECT). // Transport can also cache connections for future re-use. type Transport struct { - idleMu sync.Mutex - idleConn map[connectMethodKey][]*persistConn - idleConnCh map[connectMethodKey]chan *persistConn - reqMu sync.Mutex - reqConn map[*Request]*persistConn - altMu sync.RWMutex - altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper + idleMu sync.Mutex + idleConn map[connectMethodKey][]*persistConn + idleConnCh map[connectMethodKey]chan *persistConn + reqMu sync.Mutex + reqCanceler map[*Request]func() + altMu sync.RWMutex + altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
reqConn
がreqCanceler
に変更されています。 -
RoundTrip
メソッド内のgetConn
呼び出しとエラーハンドリング:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -190,8 +190,9 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we\'ll be ready // to send it requests.\n-\tpconn, err := t.getConn(cm)\n+\tpconn, err := t.getConn(req, cm)\n \tif err != nil {\n+\t\tt.setReqCanceler(req, nil)\n \t\treturn nil, err\n \t}\n ``` `getConn` に `req` 引数が追加され、エラー時に `setReqCanceler(req, nil)` が呼ばれるようになりました。
-
CancelRequest
メソッドのロジック変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -243,10 +244,10 @@ func (t *Transport) CloseIdleConnections() { // connection. func (t *Transport) CancelRequest(req *Request) { t.reqMu.Lock() - pc := t.reqConn[req] + cancel := t.reqCanceler[req] t.reqMu.Unlock() - if pc != nil { - pc.conn.Close() + if cancel != nil { + cancel() } }
reqConn
からpc
を取得する代わりに、reqCanceler
からcancel
関数を取得し、それを呼び出すように変更されています。 -
setReqConn
からsetReqCanceler
への変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -417,16 +418,16 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn) { } } -func (t *Transport) setReqConn(r *Request, pc *persistConn) { +func (t *Transport) setReqCanceler(r *Request, fn func()) { t.reqMu.Lock() defer t.reqMu.Unlock() - if t.reqConn == nil { - t.reqConn = make(map[*Request]*persistConn) + if t.reqCanceler == nil { + t.reqCanceler = make(map[*Request]func()) } - if pc != nil { - t.reqConn[r] = pc + if fn != nil { + t.reqCanceler[r] = fn } else { - delete(t.reqConn, r) + delete(t.reqCanceler, r) } }
setReqConn
メソッドがsetReqCanceler
にリネームされ、*persistConn
の代わりにfunc()
を受け取るようになりました。 -
getConn
メソッドのシグネチャ変更とselect
ロジックの追加:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -441,7 +442,7 @@ func (t *Transport) dial(network, addr string) (c net.Conn, err error) { // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn\'t return an error, the persistConn // is ready to write requests to.\n-func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n+func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) {\n if pc := t.getIdleConn(cm); pc != nil {\n return pc, nil\n }\n @@ -451,6 +452,16 @@ func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n err error\n }\n dialc := make(chan dialRes)\n+\n+\thandlePendingDial := func() {\n+\t\tif v := <-dialc; v.err == nil {\n+\t\t\tt.putIdleConn(v.pc)\n+\t\t}\n+\t}\n+\n+\tcancelc := make(chan struct{})\n+\tt.setReqCanceler(req, func() { close(cancelc) })\n+\n go func() {\n pc, err := t.dialConn(cm)\n dialc <- dialRes{pc, err}\n @@ -467,12 +478,11 @@ func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n // else\'s dial that they didn\'t use.\n // But our dial is still going, so give it away\n // when it finishes:\n -\t\tgo func() {\n -\t\t\tif v := <-dialc; v.err == nil {\n -\t\t\t\tt.putIdleConn(v.pc)\n -\t\t\t}\n -\t\t}()\n +\t\tgo handlePendingDial()\n return pc, nil\n +\tcase <-cancelc:\n +\t\tgo handlePendingDial()\n +\t\treturn nil, errors.New(\"net/http: request canceled while waiting for connection\")\n }\n }\n ``` `getConn` が `req` を受け取るようになり、`cancelc` チャネルと `select` ステートメントが導入され、ダイヤル処理とキャンセルシグナルを同時に待機するようになりました。
-
readLoop
とroundTrip
メソッド内のsetReqCanceler
呼び出し:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -843,7 +857,7 @@ func (pc *persistConn) readLoop() {\n alive = <-waitForBodyRead\n }\n \n -\t\tpc.t.setReqConn(rc.req, nil)\n +\t\tpc.t.setReqCanceler(rc.req, nil)\n \n \t\tif !alive {\n \t\t\tpc.close()\n @@ -910,7 +924,7 @@ var errTimeout error = &httpError{err: \"net/http: timeout awaiting response head\n var errClosed error = &httpError{err: \"net/http: transport closed before response was received\"}\n \n func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {\n -\tpc.t.setReqConn(req.Request, pc)\n +\tpc.t.setReqCanceler(req.Request, pc.cancelRequest)\n \tpc.lk.Lock()\n \tpc.numExpectedResponses++\n \theaderFn := pc.mutateHeaderFunc\n @@ -995,7 +1009,7 @@ WaitResponse:\n \tpc.lk.Unlock()\n \n \tif re.err != nil {\n -\t\tpc.t.setReqConn(req.Request, nil)\n +\t\tpc.t.setReqCanceler(req.Request, nil)\n \t}\n \treturn re.res, re.err\n }
setReqConn
の呼び出しがsetReqCanceler
に置き換えられています。特にroundTrip
ではpc.cancelRequest
という新しい関数がキャンセル関数として登録されています。
コアとなるコードの解説
上記の変更箇所について、その目的と機能について詳しく解説します。
-
Transport
構造体のreqConn
からreqCanceler
への変更:- 目的: 以前の
reqConn
は、リクエストとpersistConn
のマッピングを保持していました。これは、リクエストが既に確立された接続を使用している場合にのみキャンセルを可能にしていました。しかし、リクエストがまだ接続を取得しておらず、ダイヤル処理中にブロックされている場合は、このマップにはエントリがなく、キャンセルできませんでした。 - 機能:
reqCanceler map[*Request]func()
に変更することで、リクエストがどの状態にあるか(接続確立済みか、ダイヤル中かなど)にかかわらず、そのリクエストをキャンセルするための具体的なアクション(関数)を登録できるようになりました。CancelRequest
が呼び出されると、このマップから対応する関数を取得し、それを実行することで、リクエストの状態に応じた適切なキャンセル処理が可能になります。これにより、ダイヤル中のリクエストに対してもキャンセルシグナルを送れるようになります。
- 目的: 以前の
-
RoundTrip
メソッド内のgetConn
呼び出しとエラーハンドリング:- 目的:
RoundTrip
はHTTPリクエストのライフサイクル全体を管理するメソッドです。getConn
を呼び出す前にreq
を渡すことで、getConn
の内部でリクエストに応じたキャンセル関数を登録できるようになります。また、getConn
がエラーを返した場合に、登録されたキャンセル関数をクリーンアップ(setReqCanceler(req, nil)
)することで、不要なリソースの保持を防ぎます。
- 目的:
-
CancelRequest
メソッドのロジック変更:- 目的:
CancelRequest
の目的は、指定されたリクエストを中断することです。以前はpersistConn
を直接クローズしていましたが、これはリクエストが既に接続にバインドされている場合にしか機能しませんでした。 - 機能: 新しいロジックでは、
reqCanceler
マップからリクエストに対応するキャンセル関数cancel
を取得し、それを実行します。このcancel
関数は、リクエストがダイヤル中であればダイヤル処理を中断するロジックを含み、リクエストが既に接続を使用していればその接続をクローズするロジックを含むことができます。これにより、CancelRequest
はリクエストの状態に依存せず、常に適切なキャンセル処理を実行できるようになります。
- 目的:
-
setReqConn
からsetReqCanceler
への変更:- 目的:
reqCanceler
マップへのエントリの追加と削除を管理するためのヘルパー関数です。 - 機能:
setReqCanceler(r *Request, fn func())
は、指定されたリクエストr
に対してキャンセル関数fn
を登録します。fn
がnil
の場合は、そのリクエストのエントリをマップから削除します。これにより、reqCanceler
マップの一貫性と正確性が保たれます。
- 目的:
-
getConn
メソッドのシグネチャ変更とselect
ロジックの追加:- 目的:
getConn
は、リクエストを送信するための接続を取得する最も重要なメソッドです。この変更の核心は、ダイヤル処理が完了するのを待つ間に、同時にキャンセルシグナルも監視できるようにすることです。 - 機能:
getConn(req *Request, cm connectMethod)
:req
引数を受け取ることで、このメソッド内で現在のリクエストに対するキャンセル関数を登録できるようになります。dialc := make(chan dialRes)
: ダイヤル処理の結果(確立された接続またはエラー)をGoroutineから受け取るためのチャネルです。cancelc := make(chan struct{})
: リクエストがキャンセルされたときにシグナルを受け取るためのチャネルです。このチャネルは、CancelRequest
が呼び出されたときにclose(cancelc)
されることでシグナルを送ります。t.setReqCanceler(req, func() { close(cancelc) })
:getConn
が開始されるとすぐに、現在のリクエストreq
に対応するキャンセル関数が登録されます。この関数は、cancelc
チャネルをクローズするだけです。select
ステートメント: これがこの変更の最も重要な部分です。case v := <-dialc:
: ダイヤル処理が成功または失敗して結果がdialc
に送信された場合、このケースが実行されます。接続が取得され、リクエストは続行されます。case <-cancelc:
:CancelRequest
が呼び出され、cancelc
チャネルがクローズされた場合、このケースが実行されます。これにより、ダイヤル処理がまだ完了していなくても、リクエストは直ちに中断され、"net/http: request canceled while waiting for connection"
というエラーが返されます。
go handlePendingDial()
:select
でキャンセルが選択された場合でも、バックグラウンドで実行中のダイヤル処理が完了した際に、その接続がアイドル接続プールに適切に返却されるようにします。これにより、リソースリークを防ぎます。
- 目的:
-
readLoop
とroundTrip
メソッド内のsetReqCanceler
呼び出し:- 目的: リクエストのライフサイクルにおける適切なタイミングでキャンセル関数を登録・解除することです。
- 機能:
roundTrip
メソッドの開始時にpc.t.setReqCanceler(req.Request, pc.cancelRequest)
を呼び出すことで、リクエストがpersistConn
にバインドされたときに、その接続をクローズするpc.cancelRequest
関数がキャンセル関数として登録されます。readLoop
とroundTrip
の終了時(リクエストの処理が完了したか、エラーが発生した場合)にpc.t.setReqCanceler(rc.req, nil)
を呼び出すことで、そのリクエストに関連付けられたキャンセル関数が解除され、マップからエントリが削除されます。これにより、不要なエントリがマップに残るのを防ぎます。
これらの変更により、net/http.Transport
は、リクエストがネットワーク接続の確立段階にある場合でも、CancelRequest
を通じて適切にキャンセルできるようになり、より堅牢で応答性の高いHTTPクライアントが実現されました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/dc6bf295b95f3b1141e81fea3e128d22e4282962
- Go Issue #6951: https://golang.org/issue/6951
- Go Code Review 69280043: https://golang.org/cl/69280043
参考にした情報源リンク
- Go issue tracker (issue 6951): https://golang.org/issue/6951
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go Concurrency Patterns (Goroutines and Channels): https://go.dev/blog/concurrency-patterns
- Go
select
statement: https://go.dev/tour/concurrency/5 - Understanding Go's net/http Transport: https://medium.com/@natefinch/understanding-go-s-net-http-transport-a720b75f020 (General understanding of Transport)
- Go's HTTP Client: https://blog.golang.org/http-client (General understanding of HTTP client)
- Go
net
package documentation: https://pkg.go.dev/net (Fornet.Conn
andDial
)# [インデックス 18683] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおける Transport.CancelRequest
メソッドの機能を拡張し、ネットワーク接続の確立(ダイヤル)中にブロックされているリクエストもキャンセルできるようにするものです。これにより、リクエストがタイムアウトしたり、ユーザーによって明示的にキャンセルされたりした場合に、不要なネットワークリソースの消費やアプリケーションのハングアップを防ぎ、より堅牢なHTTPクライアントの動作を実現します。
コミット
commit dc6bf295b95f3b1141e81fea3e128d22e4282962
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Feb 27 13:32:40 2014 -0800
net/http: make Transport.CancelRequest work for requests blocked in a dial
Fixes #6951
LGTM=josharian
R=golang-codereviews, josharian
CC=golang-codereviews
https://golang.org/cl/69280043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dc6bf295b95f3b1141e81fea3e128d22e4282962
元コミット内容
net/http: make Transport.CancelRequest work for requests blocked in a dial
このコミットは、net/http
パッケージの Transport.CancelRequest
メソッドが、ネットワーク接続の確立(ダイヤル)中にブロックされているリクエストに対しても機能するように変更します。これは、Go issue #6951 で報告された問題を解決するためのものです。
変更の背景
この変更の背景には、Go言語の net/http
クライアントが、ネットワーク接続の確立(TCP接続の確立やTLSハンドシェイクなど)に時間がかかっている間に、リクエストをキャンセルできないという問題がありました。具体的には、Transport.CancelRequest
メソッドは、リクエストが既に接続プールから取得された persistConn
(永続的な接続) に関連付けられている場合にのみ機能していました。しかし、リクエストがまだ接続を取得しておらず、dial
(ダイヤル) 処理によってブロックされている間は、キャンセル要求が無視され、リクエストはダイヤル処理が完了するまで待機し続けるという挙動を示していました。
Go issue #6951 では、この問題が明確に指摘されています。ユーザーが http.Client
を使用してリクエストを送信し、そのリクエストがネットワークの遅延や到達不能なホストのためにダイヤル処理でブロックされた場合、CancelRequest
を呼び出してもリクエストが中断されず、アプリケーションがハングアップする可能性がありました。これは、特にモバイル環境や不安定なネットワーク環境において、ユーザーエクスペリエンスを著しく損なう可能性がありました。
このコミットは、CancelRequest
がダイヤル処理中のリクエストも中断できるようにすることで、この問題を解決し、net/http
クライアントの堅牢性と応答性を向上させることを目的としています。これにより、アプリケーションは不要なネットワーク操作を早期に終了させ、リソースを解放し、より迅速にエラー状態に対応できるようになります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の net/http
パッケージおよび関連する概念についての知識が必要です。
net/http.Transport
:http.Client
の基盤となる構造体で、HTTPリクエストの実際の送信、接続の管理(接続の再利用、キープアライブ)、プロキシ設定、TLS設定などを担当します。Transport
は、複数のリクエストに対して接続を再利用することで、パフォーマンスを向上させます。http.Request
とhttp.Response
: HTTPリクエストとレスポンスを表す構造体です。Transport.CancelRequest(req *Request)
: このメソッドは、指定された*http.Request
に関連付けられた進行中のHTTPリクエストをキャンセルするために使用されます。以前は、このメソッドはリクエストが既に接続にバインドされている場合にのみ有効でした。dial
処理: ネットワーク接続を確立するプロセスを指します。これには、DNSルックアップ、TCPハンドシェイク、TLSハンドシェイクなどが含まれます。これらの処理は、ネットワークの状態によっては時間がかかり、ブロックされる可能性があります。persistConn
:net/http
パッケージ内部で使用される構造体で、単一のHTTP/1.x接続(TCP接続)を抽象化し、その接続上でのリクエストとレスポンスの送受信を管理します。Transport
はpersistConn
のプールを管理し、接続の再利用を可能にします。- Goの並行処理 (Goroutines and Channels): Go言語は、軽量なスレッドであるGoroutineと、Goroutine間の安全な通信を可能にするChannelを介して並行処理をサポートします。このコミットでは、ダイヤル処理をGoroutineで実行し、Channelを使ってその結果を待機し、同時にキャンセルシグナルも受け取れるようにすることで、非同期的なキャンセルを実現しています。
select
ステートメント: 複数の通信操作(Channelの送受信)を同時に待機し、準備ができた最初の操作を実行するために使用されます。このコミットでは、ダイヤル処理の完了とキャンセルシグナルの両方を待機するためにselect
が活用されています。
技術的詳細
このコミットの技術的な核心は、Transport.CancelRequest
が、ネットワーク接続の確立(dial
)中にブロックされているリクエストを中断できるようにするために、Transport
構造体と getConn
メソッドの内部ロジックが変更された点にあります。
主な変更点は以下の通りです。
-
Transport.reqConn
からTransport.reqCanceler
への変更:- 以前の
Transport
構造体にはreqConn map[*Request]*persistConn
というフィールドがありました。これは、特定のリクエストがどのpersistConn
に関連付けられているかを追跡するためのものでした。CancelRequest
はこのマップを使用して、キャンセル対象のリクエストに対応するpersistConn
を見つけ、その接続をクローズすることでキャンセルを実現していました。 - このコミットでは、
reqConn
がreqCanceler map[*Request]func()
に変更されました。これは、リクエストごとにfunc()
型のキャンセル関数を登録できるようにするためのものです。このキャンセル関数は、リクエストがキャンセルされたときに実行されるべきロジック(例えば、ダイヤル処理を中断する、接続をクローズするなど)をカプセル化します。これにより、リクエストがpersistConn
にバインドされているかどうかにかかわらず、より柔軟なキャンセルメカニズムが提供されます。
- 以前の
-
getConn
メソッドのシグネチャ変更と内部ロジックの修正:getConn
メソッドは、HTTPリクエストを送信するためのpersistConn
を取得する役割を担っています。以前はfunc (t *Transport) getConn(cm connectMethod) (*persistConn, error)
でしたが、func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error)
に変更され、*http.Request
引数を受け取るようになりました。これにより、getConn
の内部で、現在処理中のリクエストに対してキャンセル関数を登録できるようになります。getConn
の内部では、ダイヤル処理(t.dialConn(cm)
)が新しいGoroutineで実行され、その結果はdialc
というチャネルに送信されます。- 最も重要な変更は、
cancelc := make(chan struct{})
という新しいチャネルが導入されたことです。このチャネルは、リクエストがキャンセルされたときにシグナルを受け取るために使用されます。 t.setReqCanceler(req, func() { close(cancelc) })
を呼び出すことで、現在のリクエスト (req
) に対応するキャンセル関数が登録されます。このキャンセル関数は、cancelc
チャネルをクローズするだけのシンプルなものです。CancelRequest
が呼び出されると、この登録された関数が実行され、cancelc
がクローズされます。getConn
の中で、select
ステートメントが導入されました。このselect
は、以下の2つのイベントを同時に待機します。case v := <-dialc:
: ダイヤル処理が完了し、結果がdialc
チャネルから受信された場合。case <-cancelc:
: リクエストがキャンセルされ、cancelc
チャネルがクローズされた場合。
- もし
cancelc
が先にクローズされた場合(つまり、リクエストがダイヤル中にキャンセルされた場合)、select
はcase <-cancelc:
ブロックを実行し、"net/http: request canceled while waiting for connection"
というエラーを返して、ダイヤル処理を中断します。この際、バックグラウンドで実行中のダイヤル処理が完了した際には、その接続はアイドル接続プールに返却されるようにhandlePendingDial()
がGoroutineで実行されます。これにより、リソースリークを防ぎます。
-
export_test.go
とtransport_test.go
の変更:export_test.go
では、NumPendingRequestsForTesting
メソッドがt.reqConn
の代わりにt.reqCanceler
の長さを返すように変更されました。transport_test.go
では、TestTransportCancelRequestInDial
という新しいテストケースが追加されました。このテストは、Dial
関数をブロックするように設定されたTransport
を使用し、ダイヤル中にCancelRequest
を呼び出すことで、リクエストが正しくキャンセルされることを検証します。これにより、このコミットが解決しようとしている問題が実際に解決されたことを保証します。
これらの変更により、Transport.CancelRequest
は、リクエストが接続を待機している間(特にダイヤル処理中)でも、そのリクエストを効果的に中断できるようになり、net/http
クライアントの応答性と信頼性が大幅に向上しました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に src/pkg/net/http/transport.go
に集中しています。
-
Transport
構造体のフィールド変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -47,13 +47,13 @@ const DefaultMaxIdleConnsPerHost = 2 // https, and http proxies (for either http or https with CONNECT). // Transport can also cache connections for future re-use. type Transport struct { - idleMu sync.Mutex - idleConn map[connectMethodKey][]*persistConn - idleConnCh map[connectMethodKey]chan *persistConn - reqMu sync.Mutex - reqConn map[*Request]*persistConn - altMu sync.RWMutex - altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper + idleMu sync.Mutex + idleConn map[connectMethodKey][]*persistConn + idleConnCh map[connectMethodKey]chan *persistConn + reqMu sync.Mutex + reqCanceler map[*Request]func() + altMu sync.RWMutex + altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper
reqConn
がreqCanceler
に変更されています。 -
RoundTrip
メソッド内のgetConn
呼び出しとエラーハンドリング:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -190,8 +190,9 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) { // host (for http or https), the http proxy, or the http proxy // pre-CONNECTed to https server. In any case, we\'ll be ready // to send it requests.\n-\tpconn, err := t.getConn(cm)\n+\tpconn, err := t.getConn(req, cm)\n \tif err != nil {\n+\t\tt.setReqCanceler(req, nil)\n \t\treturn nil, err\n \t}\n ``` `getConn` に `req` 引数が追加され、エラー時に `setReqCanceler(req, nil)` が呼ばれるようになりました。
-
CancelRequest
メソッドのロジック変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -243,10 +244,10 @@ func (t *Transport) CloseIdleConnections() { // connection. func (t *Transport) CancelRequest(req *Request) { t.reqMu.Lock() - pc := t.reqConn[req] + cancel := t.reqCanceler[req] t.reqMu.Unlock() - if pc != nil { - pc.conn.Close() + if cancel != nil { + cancel() } }
reqConn
からpc
を取得する代わりに、reqCanceler
からcancel
関数を取得し、それを呼び出すように変更されています。 -
setReqConn
からsetReqCanceler
への変更:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -417,16 +418,16 @@ func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn) { } } -func (t *Transport) setReqConn(r *Request, pc *persistConn) { +func (t *Transport) setReqCanceler(r *Request, fn func()) { t.reqMu.Lock() defer t.reqMu.Unlock() - if t.reqConn == nil { - t.reqConn = make(map[*Request]*persistConn) + if t.reqCanceler == nil { + t.reqCanceler = make(map[*Request]func()) } - if pc != nil { - t.reqConn[r] = pc + if fn != nil { + t.reqCanceler[r] = fn } else { - delete(t.reqConn, r) + delete(t.reqCanceler, r) } }
setReqConn
メソッドがsetReqCanceler
にリネームされ、*persistConn
の代わりにfunc()
を受け取るようになりました。 -
getConn
メソッドのシグネチャ変更とselect
ロジックの追加:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -441,7 +442,7 @@ func (t *Transport) dial(network, addr string) (c net.Conn, err error) { // specified in the connectMethod. This includes doing a proxy CONNECT // and/or setting up TLS. If this doesn\'t return an error, the persistConn // is ready to write requests to.\n-func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n+\n+func (t *Transport) getConn(req *Request, cm connectMethod) (*persistConn, error) {\n if pc := t.getIdleConn(cm); pc != nil {\n return pc, nil\n }\n @@ -451,6 +452,16 @@ func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n err error\n }\n dialc := make(chan dialRes)\n+\n+\thandlePendingDial := func() {\n+\t\tif v := <-dialc; v.err == nil {\n+\t\t\tt.putIdleConn(v.pc)\n+\t\t}\n+\t}\n+\n+\tcancelc := make(chan struct{})\n+\tt.setReqCanceler(req, func() { close(cancelc) })\n+\n go func() {\n pc, err := t.dialConn(cm)\n dialc <- dialRes{pc, err}\n @@ -467,12 +478,11 @@ func (t *Transport) getConn(cm connectMethod) (*persistConn, error) {\n // else\'s dial that they didn\'t use.\n // But our dial is still going, so give it away\n // when it finishes:\n -\t\tgo func() {\n -\t\t\tif v := <-dialc; v.err == nil {\n -\t\t\t\tt.putIdleConn(v.pc)\n -\t\t\t}\n -\t\t}()\n +\t\tgo handlePendingDial()\n return pc, nil\n +\tcase <-cancelc:\n +\t\tgo handlePendingDial()\n +\t\treturn nil, errors.New(\"net/http: request canceled while waiting for connection\")\n }\n }\n ``` `getConn` が `req` を受け取るようになり、`cancelc` チャネルと `select` ステートメントが導入され、ダイヤル処理とキャンセルシグナルを同時に待機するようになりました。
-
readLoop
とroundTrip
メソッド内のsetReqCanceler
呼び出し:--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -843,7 +857,7 @@ func (pc *persistConn) readLoop() {\n alive = <-waitForBodyRead\n }\n \n -\t\tpc.t.setReqConn(rc.req, nil)\n +\t\tpc.t.setReqCanceler(rc.req, nil)\n \n \t\tif !alive {\n \t\t\tpc.close()\n @@ -910,7 +924,7 @@ var errTimeout error = &httpError{err: \"net/http: timeout awaiting response head\n var errClosed error = &httpError{err: \"net/http: transport closed before response was received\"}\n \n func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {\n -\tpc.t.setReqConn(req.Request, pc)\n +\tpc.t.setReqCanceler(req.Request, pc.cancelRequest)\n \tpc.lk.Lock()\n \tpc.numExpectedResponses++\n \theaderFn := pc.mutateHeaderFunc\n @@ -995,7 +1009,7 @@ WaitResponse:\n \tpc.lk.Unlock()\n \n \tif re.err != nil {\n -\t\tpc.t.setReqConn(req.Request, nil)\n +\t\tpc.t.setReqCanceler(req.Request, nil)\n \t}\n \treturn re.res, re.err\n }
setReqConn
の呼び出しがsetReqCanceler
に置き換えられています。特にroundTrip
ではpc.cancelRequest
という新しい関数がキャンセル関数として登録されています。
コアとなるコードの解説
上記の変更箇所について、その目的と機能について詳しく解説します。
-
Transport
構造体のreqConn
からreqCanceler
への変更:- 目的: 以前の
reqConn
は、リクエストとpersistConn
のマッピングを保持していました。これは、リクエストが既に確立された接続を使用している場合にのみキャンセルを可能にしていました。しかし、リクエストがまだ接続を取得しておらず、ダイヤル処理中にブロックされている場合は、このマップにはエントリがなく、キャンセルできませんでした。 - 機能:
reqCanceler map[*Request]func()
に変更することで、リクエストがどの状態にあるか(接続確立済みか、ダイヤル中かなど)にかかわらず、そのリクエストをキャンセルするための具体的なアクション(関数)を登録できるようになりました。CancelRequest
が呼び出されると、このマップから対応する関数を取得し、それを実行することで、リクエストの状態に応じた適切なキャンセル処理が可能になります。これにより、ダイヤル中のリクエストに対してもキャンセルシグナルを送れるようになります。
- 目的: 以前の
-
RoundTrip
メソッド内のgetConn
呼び出しとエラーハンドリング:- 目的:
RoundTrip
はHTTPリクエストのライフサイクル全体を管理するメソッドです。getConn
を呼び出す前にreq
を渡すことで、getConn
の内部でリクエストに応じたキャンセル関数を登録できるようになります。また、getConn
がエラーを返した場合に、登録されたキャンセル関数をクリーンアップ(setReqCanceler(req, nil)
)することで、不要なリソースの保持を防ぎます。
- 目的:
-
CancelRequest
メソッドのロジック変更:- 目的:
CancelRequest
の目的は、指定されたリクエストを中断することです。以前はpersistConn
を直接クローズしていましたが、これはリクエストが既に接続にバインドされている場合にしか機能しませんでした。 - 機能: 新しいロジックでは、
reqCanceler
マップからリクエストに対応するキャンセル関数cancel
を取得し、それを実行します。このcancel
関数は、リクエストがダイヤル中であればダイヤル処理を中断するロジックを含み、リクエストが既に接続を使用していればその接続をクローズするロジックを含むことができます。これにより、CancelRequest
はリクエストの状態に依存せず、常に適切なキャンセル処理を実行できるようになります。
- 目的:
-
setReqConn
からsetReqCanceler
への変更:- 目的:
reqCanceler
マップへのエントリの追加と削除を管理するためのヘルパー関数です。 - 機能:
setReqCanceler(r *Request, fn func())
は、指定されたリクエストr
に対してキャンセル関数fn
を登録します。fn
がnil
の場合は、そのリクエストのエントリをマップから削除します。これにより、reqCanceler
マップの一貫性と正確性が保たれます。
- 目的:
-
getConn
メソッドのシグネチャ変更とselect
ロジックの追加:- 目的:
getConn
は、リクエストを送信するための接続を取得する最も重要なメソッドです。この変更の核心は、ダイヤル処理が完了するのを待つ間に、同時にキャンセルシグナルも監視できるようにすることです。 - 機能:
getConn(req *Request, cm connectMethod)
:req
引数を受け取ることで、このメソッド内で現在のリクエストに対するキャンセル関数を登録できるようになります。dialc := make(chan dialRes)
: ダイヤル処理の結果(確立された接続またはエラー)をGoroutineから受け取るためのチャネルです。cancelc := make(chan struct{})
: リクエストがキャンセルされたときにシグナルを受け取るためのチャネルです。このチャネルは、CancelRequest
が呼び出されたときにclose(cancelc)
されることでシグナルを送ります。t.setReqCanceler(req, func() { close(cancelc) })
:getConn
が開始されるとすぐに、現在のリクエストreq
に対応するキャンセル関数が登録されます。この関数は、cancelc
チャネルをクローズするだけです。select
ステートメント: これがこの変更の最も重要な部分です。case v := <-dialc:
: ダイヤル処理が成功または失敗して結果がdialc
に送信された場合、このケースが実行されます。接続が取得され、リクエストは続行されます。case <-cancelc:
:CancelRequest
が呼び出され、cancelc
チャネルがクローズされた場合、このケースが実行されます。これにより、ダイヤル処理がまだ完了していなくても、リクエストは直ちに中断され、"net/http: request canceled while waiting for connection"
というエラーが返されます。
go handlePendingDial()
:select
でキャンセルが選択された場合でも、バックグラウンドで実行中のダイヤル処理が完了した際に、その接続がアイドル接続プールに適切に返却されるようにします。これにより、リソースリークを防ぎます。
- 目的:
-
readLoop
とroundTrip
メソッド内のsetReqCanceler
呼び出し:- 目的: リクエストのライフサイクルにおける適切なタイミングでキャンセル関数を登録・解除することです。
- 機能:
roundTrip
メソッドの開始時にpc.t.setReqCanceler(req.Request, pc.cancelRequest)
を呼び出すことで、リクエストがpersistConn
にバインドされたときに、その接続をクローズするpc.cancelRequest
関数がキャンセル関数として登録されます。readLoop
とroundTrip
の終了時(リクエストの処理が完了したか、エラーが発生した場合)にpc.t.setReqCanceler(rc.req, nil)
を呼び出すことで、そのリクエストに関連付けられたキャンセル関数が解除され、マップからエントリが削除されます。これにより、不要なエントリがマップに残るのを防ぎます。
これらの変更により、net/http.Transport
は、リクエストがネットワーク接続の確立段階にある場合でも、CancelRequest
を通じて適切にキャンセルできるようになり、より堅牢で応答性の高いHTTPクライアントが実現されました。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/dc6bf295b95f3b1141e81fea3e128d22e4282962
- Go Issue #6951: https://golang.org/issue/6951
- Go Code Review 69280043: https://golang.org/cl/69280043
参考にした情報源リンク
- Go issue tracker (issue 6951): https://golang.org/issue/6951
- Go
net/http
package documentation: https://pkg.go.dev/net/http - Go Concurrency Patterns (Goroutines and Channels): https://go.dev/blog/concurrency-patterns
- Go
select
statement: https://go.dev/tour/concurrency/5 - Understanding Go's net/http Transport: https://medium.com/@natefinch/understanding-go-s-net-http-transport-a720b75f020 (General understanding of Transport)
- Go's HTTP Client: https://blog.golang.org/http-client (General understanding of HTTP client)
- Go
net
package documentation: https://pkg.go.dev/net (Fornet.Conn
andDial
)