Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Requesthttp.Response: HTTPリクエストとレスポンスを表す構造体です。
  • Transport.CancelRequest(req *Request): このメソッドは、指定された *http.Request に関連付けられた進行中のHTTPリクエストをキャンセルするために使用されます。以前は、このメソッドはリクエストが既に接続にバインドされている場合にのみ有効でした。
  • dial 処理: ネットワーク接続を確立するプロセスを指します。これには、DNSルックアップ、TCPハンドシェイク、TLSハンドシェイクなどが含まれます。これらの処理は、ネットワークの状態によっては時間がかかり、ブロックされる可能性があります。
  • persistConn: net/http パッケージ内部で使用される構造体で、単一のHTTP/1.x接続(TCP接続)を抽象化し、その接続上でのリクエストとレスポンスの送受信を管理します。TransportpersistConn のプールを管理し、接続の再利用を可能にします。
  • Goの並行処理 (Goroutines and Channels): Go言語は、軽量なスレッドであるGoroutineと、Goroutine間の安全な通信を可能にするChannelを介して並行処理をサポートします。このコミットでは、ダイヤル処理をGoroutineで実行し、Channelを使ってその結果を待機し、同時にキャンセルシグナルも受け取れるようにすることで、非同期的なキャンセルを実現しています。
    • select ステートメント: 複数の通信操作(Channelの送受信)を同時に待機し、準備ができた最初の操作を実行するために使用されます。このコミットでは、ダイヤル処理の完了とキャンセルシグナルの両方を待機するために select が活用されています。

技術的詳細

このコミットの技術的な核心は、Transport.CancelRequest が、ネットワーク接続の確立(dial)中にブロックされているリクエストを中断できるようにするために、Transport 構造体と getConn メソッドの内部ロジックが変更された点にあります。

主な変更点は以下の通りです。

  1. Transport.reqConn から Transport.reqCanceler への変更:

    • 以前の Transport 構造体には reqConn map[*Request]*persistConn というフィールドがありました。これは、特定のリクエストがどの persistConn に関連付けられているかを追跡するためのものでした。CancelRequest はこのマップを使用して、キャンセル対象のリクエストに対応する persistConn を見つけ、その接続をクローズすることでキャンセルを実現していました。
    • このコミットでは、reqConnreqCanceler map[*Request]func() に変更されました。これは、リクエストごとに func() 型のキャンセル関数を登録できるようにするためのものです。このキャンセル関数は、リクエストがキャンセルされたときに実行されるべきロジック(例えば、ダイヤル処理を中断する、接続をクローズするなど)をカプセル化します。これにより、リクエストが persistConn にバインドされているかどうかにかかわらず、より柔軟なキャンセルメカニズムが提供されます。
  2. 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 が先にクローズされた場合(つまり、リクエストがダイヤル中にキャンセルされた場合)、selectcase <-cancelc: ブロックを実行し、"net/http: request canceled while waiting for connection" というエラーを返して、ダイヤル処理を中断します。この際、バックグラウンドで実行中のダイヤル処理が完了した際には、その接続はアイドル接続プールに返却されるように handlePendingDial() がGoroutineで実行されます。これにより、リソースリークを防ぎます。
  3. export_test.gotransport_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 に集中しています。

  1. 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
    

    reqConnreqCanceler に変更されています。

  2. 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)` が呼ばれるようになりました。
    
    
  3. 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 関数を取得し、それを呼び出すように変更されています。

  4. 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() を受け取るようになりました。

  5. 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` ステートメントが導入され、ダイヤル処理とキャンセルシグナルを同時に待機するようになりました。
    
    
  6. readLooproundTrip メソッド内の 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 という新しい関数がキャンセル関数として登録されています。

コアとなるコードの解説

上記の変更箇所について、その目的と機能について詳しく解説します。

  1. Transport 構造体の reqConn から reqCanceler への変更:

    • 目的: 以前の reqConn は、リクエストと persistConn のマッピングを保持していました。これは、リクエストが既に確立された接続を使用している場合にのみキャンセルを可能にしていました。しかし、リクエストがまだ接続を取得しておらず、ダイヤル処理中にブロックされている場合は、このマップにはエントリがなく、キャンセルできませんでした。
    • 機能: reqCanceler map[*Request]func() に変更することで、リクエストがどの状態にあるか(接続確立済みか、ダイヤル中かなど)にかかわらず、そのリクエストをキャンセルするための具体的なアクション(関数)を登録できるようになりました。CancelRequest が呼び出されると、このマップから対応する関数を取得し、それを実行することで、リクエストの状態に応じた適切なキャンセル処理が可能になります。これにより、ダイヤル中のリクエストに対してもキャンセルシグナルを送れるようになります。
  2. RoundTrip メソッド内の getConn 呼び出しとエラーハンドリング:

    • 目的: RoundTrip はHTTPリクエストのライフサイクル全体を管理するメソッドです。getConn を呼び出す前に req を渡すことで、getConn の内部でリクエストに応じたキャンセル関数を登録できるようになります。また、getConn がエラーを返した場合に、登録されたキャンセル関数をクリーンアップ(setReqCanceler(req, nil))することで、不要なリソースの保持を防ぎます。
  3. CancelRequest メソッドのロジック変更:

    • 目的: CancelRequest の目的は、指定されたリクエストを中断することです。以前は persistConn を直接クローズしていましたが、これはリクエストが既に接続にバインドされている場合にしか機能しませんでした。
    • 機能: 新しいロジックでは、reqCanceler マップからリクエストに対応するキャンセル関数 cancel を取得し、それを実行します。この cancel 関数は、リクエストがダイヤル中であればダイヤル処理を中断するロジックを含み、リクエストが既に接続を使用していればその接続をクローズするロジックを含むことができます。これにより、CancelRequest はリクエストの状態に依存せず、常に適切なキャンセル処理を実行できるようになります。
  4. setReqConn から setReqCanceler への変更:

    • 目的: reqCanceler マップへのエントリの追加と削除を管理するためのヘルパー関数です。
    • 機能: setReqCanceler(r *Request, fn func()) は、指定されたリクエスト r に対してキャンセル関数 fn を登録します。fnnil の場合は、そのリクエストのエントリをマップから削除します。これにより、reqCanceler マップの一貫性と正確性が保たれます。
  5. 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 でキャンセルが選択された場合でも、バックグラウンドで実行中のダイヤル処理が完了した際に、その接続がアイドル接続プールに適切に返却されるようにします。これにより、リソースリークを防ぎます。
  6. readLooproundTrip メソッド内の setReqCanceler 呼び出し:

    • 目的: リクエストのライフサイクルにおける適切なタイミングでキャンセル関数を登録・解除することです。
    • 機能:
      • roundTrip メソッドの開始時に pc.t.setReqCanceler(req.Request, pc.cancelRequest) を呼び出すことで、リクエストが persistConn にバインドされたときに、その接続をクローズする pc.cancelRequest 関数がキャンセル関数として登録されます。
      • readLooproundTrip の終了時(リクエストの処理が完了したか、エラーが発生した場合)に pc.t.setReqCanceler(rc.req, nil) を呼び出すことで、そのリクエストに関連付けられたキャンセル関数が解除され、マップからエントリが削除されます。これにより、不要なエントリがマップに残るのを防ぎます。

これらの変更により、net/http.Transport は、リクエストがネットワーク接続の確立段階にある場合でも、CancelRequest を通じて適切にキャンセルできるようになり、より堅牢で応答性の高いHTTPクライアントが実現されました。

関連リンク

参考にした情報源リンク

このコミットは、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.Requesthttp.Response: HTTPリクエストとレスポンスを表す構造体です。
  • Transport.CancelRequest(req *Request): このメソッドは、指定された *http.Request に関連付けられた進行中のHTTPリクエストをキャンセルするために使用されます。以前は、このメソッドはリクエストが既に接続にバインドされている場合にのみ有効でした。
  • dial 処理: ネットワーク接続を確立するプロセスを指します。これには、DNSルックアップ、TCPハンドシェイク、TLSハンドシェイクなどが含まれます。これらの処理は、ネットワークの状態によっては時間がかかり、ブロックされる可能性があります。
  • persistConn: net/http パッケージ内部で使用される構造体で、単一のHTTP/1.x接続(TCP接続)を抽象化し、その接続上でのリクエストとレスポンスの送受信を管理します。TransportpersistConn のプールを管理し、接続の再利用を可能にします。
  • Goの並行処理 (Goroutines and Channels): Go言語は、軽量なスレッドであるGoroutineと、Goroutine間の安全な通信を可能にするChannelを介して並行処理をサポートします。このコミットでは、ダイヤル処理をGoroutineで実行し、Channelを使ってその結果を待機し、同時にキャンセルシグナルも受け取れるようにすることで、非同期的なキャンセルを実現しています。
    • select ステートメント: 複数の通信操作(Channelの送受信)を同時に待機し、準備ができた最初の操作を実行するために使用されます。このコミットでは、ダイヤル処理の完了とキャンセルシグナルの両方を待機するために select が活用されています。

技術的詳細

このコミットの技術的な核心は、Transport.CancelRequest が、ネットワーク接続の確立(dial)中にブロックされているリクエストを中断できるようにするために、Transport 構造体と getConn メソッドの内部ロジックが変更された点にあります。

主な変更点は以下の通りです。

  1. Transport.reqConn から Transport.reqCanceler への変更:

    • 以前の Transport 構造体には reqConn map[*Request]*persistConn というフィールドがありました。これは、特定のリクエストがどの persistConn に関連付けられているかを追跡するためのものでした。CancelRequest はこのマップを使用して、キャンセル対象のリクエストに対応する persistConn を見つけ、その接続をクローズすることでキャンセルを実現していました。
    • このコミットでは、reqConnreqCanceler map[*Request]func() に変更されました。これは、リクエストごとに func() 型のキャンセル関数を登録できるようにするためのものです。このキャンセル関数は、リクエストがキャンセルされたときに実行されるべきロジック(例えば、ダイヤル処理を中断する、接続をクローズするなど)をカプセル化します。これにより、リクエストが persistConn にバインドされているかどうかにかかわらず、より柔軟なキャンセルメカニズムが提供されます。
  2. 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 が先にクローズされた場合(つまり、リクエストがダイヤル中にキャンセルされた場合)、selectcase <-cancelc: ブロックを実行し、"net/http: request canceled while waiting for connection" というエラーを返して、ダイヤル処理を中断します。この際、バックグラウンドで実行中のダイヤル処理が完了した際には、その接続はアイドル接続プールに返却されるように handlePendingDial() がGoroutineで実行されます。これにより、リソースリークを防ぎます。
  3. export_test.gotransport_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 に集中しています。

  1. 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
    

    reqConnreqCanceler に変更されています。

  2. 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)` が呼ばれるようになりました。
    
    
  3. 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 関数を取得し、それを呼び出すように変更されています。

  4. 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() を受け取るようになりました。

  5. 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` ステートメントが導入され、ダイヤル処理とキャンセルシグナルを同時に待機するようになりました。
    
    
  6. readLooproundTrip メソッド内の 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 という新しい関数がキャンセル関数として登録されています。

コアとなるコードの解説

上記の変更箇所について、その目的と機能について詳しく解説します。

  1. Transport 構造体の reqConn から reqCanceler への変更:

    • 目的: 以前の reqConn は、リクエストと persistConn のマッピングを保持していました。これは、リクエストが既に確立された接続を使用している場合にのみキャンセルを可能にしていました。しかし、リクエストがまだ接続を取得しておらず、ダイヤル処理中にブロックされている場合は、このマップにはエントリがなく、キャンセルできませんでした。
    • 機能: reqCanceler map[*Request]func() に変更することで、リクエストがどの状態にあるか(接続確立済みか、ダイヤル中かなど)にかかわらず、そのリクエストをキャンセルするための具体的なアクション(関数)を登録できるようになりました。CancelRequest が呼び出されると、このマップから対応する関数を取得し、それを実行することで、リクエストの状態に応じた適切なキャンセル処理が可能になります。これにより、ダイヤル中のリクエストに対してもキャンセルシグナルを送れるようになります。
  2. RoundTrip メソッド内の getConn 呼び出しとエラーハンドリング:

    • 目的: RoundTrip はHTTPリクエストのライフサイクル全体を管理するメソッドです。getConn を呼び出す前に req を渡すことで、getConn の内部でリクエストに応じたキャンセル関数を登録できるようになります。また、getConn がエラーを返した場合に、登録されたキャンセル関数をクリーンアップ(setReqCanceler(req, nil))することで、不要なリソースの保持を防ぎます。
  3. CancelRequest メソッドのロジック変更:

    • 目的: CancelRequest の目的は、指定されたリクエストを中断することです。以前は persistConn を直接クローズしていましたが、これはリクエストが既に接続にバインドされている場合にしか機能しませんでした。
    • 機能: 新しいロジックでは、reqCanceler マップからリクエストに対応するキャンセル関数 cancel を取得し、それを実行します。この cancel 関数は、リクエストがダイヤル中であればダイヤル処理を中断するロジックを含み、リクエストが既に接続を使用していればその接続をクローズするロジックを含むことができます。これにより、CancelRequest はリクエストの状態に依存せず、常に適切なキャンセル処理を実行できるようになります。
  4. setReqConn から setReqCanceler への変更:

    • 目的: reqCanceler マップへのエントリの追加と削除を管理するためのヘルパー関数です。
    • 機能: setReqCanceler(r *Request, fn func()) は、指定されたリクエスト r に対してキャンセル関数 fn を登録します。fnnil の場合は、そのリクエストのエントリをマップから削除します。これにより、reqCanceler マップの一貫性と正確性が保たれます。
  5. 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 でキャンセルが選択された場合でも、バックグラウンドで実行中のダイヤル処理が完了した際に、その接続がアイドル接続プールに適切に返却されるようにします。これにより、リソースリークを防ぎます。
  6. readLooproundTrip メソッド内の setReqCanceler 呼び出し:

    • 目的: リクエストのライフサイクルにおける適切なタイミングでキャンセル関数を登録・解除することです。
    • 機能:
      • roundTrip メソッドの開始時に pc.t.setReqCanceler(req.Request, pc.cancelRequest) を呼び出すことで、リクエストが persistConn にバインドされたときに、その接続をクローズする pc.cancelRequest 関数がキャンセル関数として登録されます。
      • readLooproundTrip の終了時(リクエストの処理が完了したか、エラーが発生した場合)に pc.t.setReqCanceler(rc.req, nil) を呼び出すことで、そのリクエストに関連付けられたキャンセル関数が解除され、マップからエントリが削除されます。これにより、不要なエントリがマップに残るのを防ぎます。

これらの変更により、net/http.Transport は、リクエストがネットワーク接続の確立段階にある場合でも、CancelRequest を通じて適切にキャンセルできるようになり、より堅牢で応答性の高いHTTPクライアントが実現されました。

関連リンク

参考にした情報源リンク