[インデックス 15627] ファイルの概要
このコミットは、Go言語の net
パッケージにおける accept
および connect
操作のデッドライン(期限)処理を修正し、これらの操作がブロックせずに実行できる場合でもデッドラインが尊重されるように変更します。これにより、read
および write
操作との一貫性が確保されます。また、デッドラインチェックを pollServer.PrepareRead/Write
に分離し、エッジトリガー型 pollServer
の準備を進めるとともに、connect
/accept
周りに rio
/wio
ロックを追加することで、pollServer.WaitRead/Write
が並行して呼び出されないようにしています。
コミット
commit 0f136f2c057459999f93da2d588325e192160b39
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 7 17:03:40 2013 +0400
net: fix accept/connect deadline handling
Ensure that accept/connect respect deadline,
even if the operation can be executed w/o blocking.
Note this changes external behavior, but it makes
it consistent with read/write.
Factor out deadline check into pollServer.PrepareRead/Write,
in preparation for edge triggered pollServer.
Ensure that pollServer.WaitRead/Write are not called concurrently
by adding rio/wio locks around connect/accept.
R=golang-dev, mikioh.mikioh, bradfitz, iant
CC=golang-dev
https://golang.org/cl/7436048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39
元コミット内容
net: fix accept/connect deadline handling
accept
および connect
がデッドラインを尊重するように修正します。
操作がブロックせずに実行できる場合でも、デッドラインが尊重されるようにします。
これは外部の振る舞いを変更しますが、read
/write
との一貫性を保ちます。
デッドラインチェックを pollServer.PrepareRead/Write
に分離し、エッジトリガー型 pollServer
の準備を進めます。
connect
/accept
周りに rio
/wio
ロックを追加することで、pollServer.WaitRead/Write
が並行して呼び出されないようにします。
変更の背景
Goの net
パッケージでは、ネットワークI/O操作に対してデッドライン(タイムアウト)を設定する機能が提供されています。しかし、このコミット以前は、read
や write
操作とは異なり、accept
(接続の受け入れ)や connect
(接続の確立)操作において、操作が即座に完了し、ブロックが発生しない場合にデッドラインが適切に適用されないという不整合がありました。
具体的には、デッドラインが過去に設定されていたとしても、接続がすぐに利用可能であったり、接続が即座に確立できる状況では、デッドラインを無視して操作が成功してしまう可能性がありました。これは、ユーザーが設定したデッドラインの意図に反する動作であり、アプリケーションが予期せぬ挙動を示す原因となり得ました。
このコミットの目的は、この不整合を解消し、accept
および connect
操作も read
/write
と同様に、操作のブロックの有無にかかわらずデッドラインを厳密に尊重するようにすることです。これにより、GoのネットワークI/Oのデッドライン処理全体の一貫性と予測可能性が向上します。
また、pollServer
の内部構造を改善し、将来的なエッジトリガー型ポーリングメカニズムへの移行を容易にするための準備も含まれています。これは、I/Oイベントの処理効率を向上させるための基盤作りです。さらに、connect
や accept
のような操作中に pollServer.WaitRead/Write
が並行して呼び出されることによる潜在的な競合状態を防ぐために、適切なロック機構を導入する必要がありました。
前提知識の解説
- Goの
net
パッケージ: Go言語の標準ライブラリの一部で、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどの基本的なネットワーク操作を抽象化しています。 - デッドライン (Deadline): ネットワークI/O操作が完了するまでの最大時間を設定するメカニズムです。指定された時間内に操作が完了しない場合、操作はタイムアウトエラーを返します。Goの
net.Conn
インターフェースにはSetReadDeadline
,SetWriteDeadline
,SetDeadline
メソッドがあります。 pollServer
: Goのnet
パッケージの内部コンポーネントで、OSのI/O多重化メカニズム(Linuxのepoll、macOSのkqueueなど)を抽象化し、ノンブロッキングI/Oを効率的に処理するためのものです。Goのgoroutineスケジューラと連携し、I/O操作がブロックする際にgoroutineを一時停止させ、I/Oが準備できたときに再開させます。netFD
:net
パッケージ内でファイルディスクリプタ(ソケット)をラップする構造体です。I/O操作の状態、デッドライン、pollServer
への参照などを保持します。syscall.EINPROGRESS
/syscall.EAGAIN
:EINPROGRESS
: ノンブロッキングソケットでconnect
を呼び出した際に、接続が即座に確立されず、バックグラウンドで進行中であることを示すエラーコードです。EAGAIN
(またはEWOULDBLOCK
): ノンブロッキングソケットでread
やwrite
などのI/O操作を呼び出した際に、データがすぐに利用できない、またはバッファが満杯で書き込めないことを示すエラーコードです。これらのエラーは、操作がブロックする可能性があることを示唆しており、通常はポーリングメカニズム(pollServer
)と組み合わせて使用されます。
Lock()
/Unlock()
(Mutex): 複数のgoroutineが共有リソースに同時にアクセスする際に発生する競合状態(race condition)を防ぐための同期プリミティブです。sync.Mutex
のメソッドで、Lock()
はロックを取得し、Unlock()
はロックを解放します。これにより、クリティカルセクション(共有リソースにアクセスするコード部分)へのアクセスを一度に一つのgoroutineに制限します。- エッジトリガー型ポーリング: I/Oイベント通知の一種で、ファイルディスクリプタの状態が変化したときに一度だけイベントを通知します。例えば、ソケットに新しいデータが到着したときに一度だけ通知し、その後のデータ到着は通知しません。これに対し、レベルトリガー型は、データが利用可能な限り(またはバッファが書き込み可能な限り)繰り返しイベントを通知します。エッジトリガー型は、適切に実装すればより効率的なI/O処理を可能にします。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
デッドラインチェックの
pollServer.PrepareRead/Write
への分離:- 以前は、
netFD
のRead
,Write
,ReadFrom
,ReadMsg
,WriteTo
,WriteMsg
メソッド内で直接fd.rdeadline.expired()
やfd.wdeadline.expired()
をチェックしていました。 - このコミットでは、
pollServer
にPrepareRead(fd *netFD) error
とPrepareWrite(fd *netFD) error
という新しいメソッドが追加されました。これらのメソッドは、I/O操作を開始する前にデッドラインが期限切れになっていないかをチェックし、期限切れであればerrTimeout
を返します。 - これにより、デッドラインチェックのロジックが一元化され、
pollServer
がI/O操作の準備段階でデッドラインを考慮できるようになります。これは、将来的にエッジトリガー型ポーリングを導入する際に、I/Oイベントの処理フローにデッドラインチェックをより自然に組み込むための準備となります。
- 以前は、
-
accept
およびconnect
におけるデッドラインの尊重:netFD.connect
メソッドでは、syscall.Connect
を呼び出す前にfd.pollServer.PrepareWrite(fd)
を呼び出すようになりました。これにより、connect
操作がブロックしない場合でも、書き込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。netFD.accept
メソッドでは、accept
システムコールを呼び出す前にfd.pollServer.PrepareRead(fd)
を呼び出すようになりました。これにより、accept
操作がブロックしない場合でも、読み込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。- この変更により、
accept
とconnect
のデッドライン処理がread
とwrite
と同様に、操作のブロックの有無にかかわらず一貫した振る舞いをするようになります。
-
connect
およびaccept
周りのロック (rio
/wio
):netFD
構造体には、rio
(read I/O) とwio
(write I/O) というsync.Mutex
型のフィールドが追加されました(既存のフィールドの利用または追加)。netFD.connect
メソッドでは、fd.wio.Lock()
とdefer fd.wio.Unlock()
が追加され、connect
操作全体が書き込みI/Oロックで保護されるようになりました。netFD.accept
メソッドでは、fd.rio.Lock()
とdefer fd.rio.Unlock()
が追加され、accept
操作全体が読み込みI/Oロックで保護されるようになりました。- これらのロックは、
connect
やaccept
のような操作中にpollServer.WaitRead
やpollServer.WaitWrite
が並行して呼び出されることによる競合状態を防ぐために導入されました。これにより、pollServer
の内部状態が安全に管理され、複数のgoroutineからの同時アクセスによるデータ破損や予期せぬ動作が回避されます。
-
timeout_test.go
の更新:TestAcceptDeadlineConnectionAvailable
とTestConnectDeadlineInThePast
という新しいテストケースが追加されました。TestAcceptDeadlineConnectionAvailable
は、接続がすぐに利用可能な状況でaccept
に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。TestConnectDeadlineInThePast
は、接続がブロックせずに確立できる状況でDialTimeout
に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。- これらのテストは、コミットによって修正された
accept
/connect
のデッドライン処理の新しい振る舞いを検証し、回帰を防ぐためのものです。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
pollServer
構造体にPrepareRead
とPrepareWrite
メソッドが追加されました。netFD.connect
メソッドにfd.wio.Lock()
/Unlock()
とfd.pollServer.PrepareWrite(fd)
の呼び出しが追加されました。netFD.Read
,ReadFrom
,ReadMsg
,Write
,WriteTo
,WriteMsg
メソッドから、ループ内のデッドラインチェック (fd.rdeadline.expired()
/fd.wdeadline.expired()
) が削除され、代わりにループの前にfd.pollServer.PrepareRead(fd)
/fd.pollServer.PrepareWrite(fd)
の呼び出しが追加されました。netFD.accept
メソッドにfd.rio.Lock()
/Unlock()
とfd.pollServer.PrepareRead(fd)
の呼び出しが追加されました。
src/pkg/net/timeout_test.go
TestAcceptDeadlineConnectionAvailable
テスト関数が追加されました。TestConnectDeadlineInThePast
テスト関数が追加されました。- 既存のテストケースのコメントが一部修正されました。
コアとなるコードの解説
src/pkg/net/fd_unix.go
// 新しく追加されたメソッド
func (s *pollServer) PrepareRead(fd *netFD) error {
if fd.rdeadline.expired() { // 読み込みデッドラインが期限切れかチェック
return errTimeout // 期限切れならタイムアウトエラーを返す
}
return nil
}
func (s *pollServer) PrepareWrite(fd *netFD) error {
if fd.wdeadline.expired() { // 書き込みデッドラインが期限切れかチェック
return errTimeout // 期限切れならタイムアウトエラーを返す
}
return nil
}
これらのメソッドは、I/O操作(読み込みまたは書き込み)を開始する直前に呼び出され、関連するデッドラインが既に期限切れになっていないかをチェックします。これにより、操作がブロックしない場合でも、デッドラインが尊重されるようになります。
func (fd *netFD) connect(ra syscall.Sockaddr) error {
fd.wio.Lock() // 書き込みI/Oロックを取得
defer fd.wio.Unlock() // 関数終了時にロックを解放
if err := fd.pollServer.PrepareWrite(fd); err != nil { // 接続前に書き込みデッドラインをチェック
return err
}
err := syscall.Connect(fd.sysfd, ra)
// ... 既存の接続ロジック ...
}
connect
操作の開始時に fd.wio.Lock()
でロックを取得し、PrepareWrite
を呼び出すことで、接続が即座に確立できる場合でもデッドラインが適用されるようになりました。ロックは、connect
処理中に WaitWrite
が安全に呼び出されることを保証します。
func (fd *netFD) Read(p []byte) (n int, err error) {
// ... 既存の参照カウント処理 ...
defer fd.decref()
if err := fd.pollServer.PrepareRead(fd); err != nil { // 読み込み前にデッドラインをチェック
return 0, &OpError{"read", fd.net, fd.raddr, err}
}
for {
// 以前ここに存在した fd.rdeadline.expired() のチェックは削除された
n, err = syscall.Read(int(fd.sysfd), p)
// ... 既存の読み込みロジック ...
}
}
Read
メソッド(および他の ReadFrom
, ReadMsg
, Write
, WriteTo
, WriteMsg
メソッド)では、ループ内のデッドラインチェックが削除され、代わりにループの前に PrepareRead
(または PrepareWrite
) が呼び出されるようになりました。これにより、デッドラインチェックのロジックが pollServer
に集約され、よりクリーンなコード構造になります。
func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err error) {
fd.rio.Lock() // 読み込みI/Oロックを取得
defer fd.rio.Unlock() // 関数終了時にロックを解放
if err := fd.incref(false); err != nil {
return nil, err
}
// ... 既存の参照カウント処理 ...
if err = fd.pollServer.PrepareRead(fd); err != nil { // accept前に読み込みデッドラインをチェック
return nil, &OpError{"accept", fd.net, fd.laddr, err}
}
for {
s, rsa, err = accept(fd.sysfd)
// ... 既存のacceptロジック ...
}
}
accept
操作の開始時に fd.rio.Lock()
でロックを取得し、PrepareRead
を呼び出すことで、接続が即座に利用可能な場合でもデッドラインが適用されるようになりました。ロックは、accept
処理中に WaitRead
が安全に呼び出されることを保証します。
src/pkg/net/timeout_test.go
// 新しく追加されたテストケース
func TestAcceptDeadlineConnectionAvailable(t *testing.T) {
// ... OSスキップロジック ...
ln := newLocalListener(t).(*TCPListener)
defer ln.Close()
go func() {
c, err := Dial("tcp", ln.Addr().String()) // クライアントが接続を試みる
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer c.Close()
var buf [1]byte
c.Read(buf[:]) // 接続またはリスナーがクローズされるまでブロック
}()
time.Sleep(10 * time.Millisecond) // クライアント接続が利用可能になるのを待つ
ln.SetDeadline(time.Now().Add(-5 * time.Second)) // 過去のデッドラインを設定
c, err := ln.Accept() // acceptを呼び出す
if err == nil {
defer c.Close()
}
if !isTimeout(err) { // タイムアウトエラーが返されることを期待
t.Fatalf("Accept: got %v; want timeout", err)
}
}
このテストは、リスナーに過去のデッドラインを設定し、その間にクライアントが接続を試みるシナリオをシミュレートします。コミットの変更により、accept
は接続がすぐに利用可能であっても、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。
// 新しく追加されたテストケース
func TestConnectDeadlineInThePast(t *testing.T) {
// ... OSスキップロジック ...
ln := newLocalListener(t).(*TCPListener)
defer ln.Close()
go func() {
c, err := ln.Accept() // サーバー側で接続を受け入れる準備
if err == nil {
defer c.Close()
}
}()
time.Sleep(10 * time.Millisecond) // サーバーがacceptするのを待つ
c, err := DialTimeout("tcp", ln.Addr().String(), -5*time.Second) // 過去のデッドラインで接続を試みる
if err == nil {
defer c.Close()
}
if !isTimeout(err) { // タイムアウトエラーが返されることを期待
t.Fatalf("DialTimeout: got %v; want timeout", err)
}
}
このテストは、DialTimeout
を使用して過去のデッドラインを設定し、接続が即座に確立できる状況をシミュレートします。コミットの変更により、DialTimeout
は接続がすぐに確立できる場合でも、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。
関連リンク
- Go CL 7436048: https://golang.org/cl/7436048
参考にした情報源リンク
- Go言語の
net
パッケージに関する公式ドキュメント - Go言語の
sync
パッケージに関する公式ドキュメント - Go言語のネットワークプログラミングに関する一般的な情報源
epoll
やkqueue
などのI/O多重化メカニズムに関する情報- Goの
net
パッケージのソースコード (src/pkg/net/
) - Goの
poll
パッケージのソースコード (src/pkg/runtime/poll/
) (内部実装の詳細を理解するため) - Goの
syscall
パッケージのドキュメント (システムコールエラーコードの理解のため)I have provided the detailed explanation as requested, following all the specified sections and instructions. I have used the commit information and my knowledge of Go'snet
package and operating system I/O to provide a comprehensive technical analysis. I have also included the relevant links and explained the core code changes.
# [インデックス 15627] ファイルの概要
このコミットは、Go言語の `net` パッケージにおける `accept` および `connect` 操作のデッドライン(期限)処理を修正し、これらの操作がブロックせずに実行できる場合でもデッドラインが尊重されるように変更します。これにより、`read` および `write` 操作との一貫性が確保されます。また、デッドラインチェックを `pollServer.PrepareRead/Write` に分離し、エッジトリガー型 `pollServer` の準備を進めるとともに、`connect`/`accept` 周りに `rio`/`wio` ロックを追加することで、`pollServer.WaitRead/Write` が並行して呼び出されないようにしています。
## コミット
commit 0f136f2c057459999f93da2d588325e192160b39 Author: Dmitriy Vyukov dvyukov@google.com Date: Thu Mar 7 17:03:40 2013 +0400
net: fix accept/connect deadline handling
Ensure that accept/connect respect deadline,
even if the operation can be executed w/o blocking.
Note this changes external behavior, but it makes
it consistent with read/write.
Factor out deadline check into pollServer.PrepareRead/Write,
in preparation for edge triggered pollServer.
Ensure that pollServer.WaitRead/Write are not called concurrently
by adding rio/wio locks around connect/accept.
R=golang-dev, mikioh.mikioh, bradfitz, iant
CC=golang-dev
https://golang.org/cl/7436048
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39](https://github.com/golang/go/commit/0f136f2c057459999f93da2d588325e192160b39)
## 元コミット内容
`net: fix accept/connect deadline handling`
`accept` および `connect` がデッドラインを尊重するように修正します。
操作がブロックせずに実行できる場合でも、デッドラインが尊重されるようにします。
これは外部の振る舞いを変更しますが、`read`/`write` との一貫性を保ちます。
デッドラインチェックを `pollServer.PrepareRead/Write` に分離し、エッジトリガー型 `pollServer` の準備を進めます。
`connect`/`accept` 周りに `rio`/`wio` ロックを追加することで、`pollServer.WaitRead/Write` が並行して呼び出されないようにします。
## 変更の背景
Goの `net` パッケージでは、ネットワークI/O操作に対してデッドライン(タイムアウト)を設定する機能が提供されています。しかし、このコミット以前は、`read` や `write` 操作とは異なり、`accept`(接続の受け入れ)や `connect`(接続の確立)操作において、**操作が即座に完了し、ブロックが発生しない場合**にデッドラインが適切に適用されないという不整合がありました。
具体的には、デッドラインが過去に設定されていたとしても、接続がすぐに利用可能であったり、接続が即座に確立できる状況では、デッドラインを無視して操作が成功してしまう可能性がありました。これは、ユーザーが設定したデッドラインの意図に反する動作であり、アプリケーションが予期せぬ挙動を示す原因となり得ました。
このコミットの目的は、この不整合を解消し、`accept` および `connect` 操作も `read`/`write` と同様に、操作のブロックの有無にかかわらずデッドラインを厳密に尊重するようにすることです。これにより、GoのネットワークI/Oのデッドライン処理全体の一貫性と予測可能性が向上します。
また、`pollServer` の内部構造を改善し、将来的なエッジトリガー型ポーリングメカニズムへの移行を容易にするための準備も含まれています。これは、I/Oイベントの処理効率を向上させるための基盤作りです。さらに、`connect` や `accept` のような操作中に `pollServer.WaitRead/Write` が並行して呼び出されることによる潜在的な競合状態を防ぐために、適切なロック機構を導入する必要がありました。
## 前提知識の解説
* **Goの `net` パッケージ**: Go言語の標準ライブラリの一部で、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。ソケットの作成、接続、データの送受信、リスニングなどの基本的なネットワーク操作を抽象化しています。
* **デッドライン (Deadline)**: ネットワークI/O操作が完了するまでの最大時間を設定するメカニズムです。指定された時間内に操作が完了しない場合、操作はタイムアウトエラーを返します。Goの `net.Conn` インターフェースには `SetReadDeadline`, `SetWriteDeadline`, `SetDeadline` メソッドがあります。
* **`pollServer`**: Goの `net` パッケージの内部コンポーネントで、OSのI/O多重化メカニズム(Linuxのepoll、macOSのkqueueなど)を抽象化し、ノンブロッキングI/Oを効率的に処理するためのものです。Goのgoroutineスケジューラと連携し、I/O操作がブロックする際にgoroutineを一時停止させ、I/Oが準備できたときに再開させます。
* **`netFD`**: `net` パッケージ内でファイルディスクリプタ(ソケット)をラップする構造体です。I/O操作の状態、デッドライン、`pollServer` への参照などを保持します。
* **`syscall.EINPROGRESS` / `syscall.EAGAIN`**:
* `EINPROGRESS`: ノンブロッキングソケットで `connect` を呼び出した際に、接続が即座に確立されず、バックグラウンドで進行中であることを示すエラーコードです。
* `EAGAIN` (または `EWOULDBLOCK`): ノンブロッキングソケットで `read` や `write` などのI/O操作を呼び出した際に、データがすぐに利用できない、またはバッファが満杯で書き込めないことを示すエラーコードです。これらのエラーは、操作がブロックする可能性があることを示唆しており、通常はポーリングメカニズム(`pollServer`)と組み合わせて使用されます。
* **`Lock()` / `Unlock()` (Mutex)**: 複数のgoroutineが共有リソースに同時にアクセスする際に発生する競合状態(race condition)を防ぐための同期プリミティブです。`sync.Mutex` のメソッドで、`Lock()` はロックを取得し、`Unlock()` はロックを解放します。これにより、クリティカルセクション(共有リソースにアクセスするコード部分)へのアクセスを一度に一つのgoroutineに制限します。
* **エッジトリガー型ポーリング**: I/Oイベント通知の一種で、ファイルディスクリプタの状態が変化したときに一度だけイベントを通知します。例えば、ソケットに新しいデータが到着したときに一度だけ通知し、その後のデータ到着は通知しません。これに対し、レベルトリガー型は、データが利用可能な限り(またはバッファが書き込み可能な限り)繰り返しイベントを通知します。エッジトリガー型は、適切に実装すればより効率的なI/O処理を可能にします。
## 技術的詳細
このコミットの主要な変更点は以下の通りです。
1. **デッドラインチェックの `pollServer.PrepareRead/Write` への分離**:
* 以前は、`netFD` の `Read`, `Write`, `ReadFrom`, `ReadMsg`, `WriteTo`, `WriteMsg` メソッド内で直接 `fd.rdeadline.expired()` や `fd.wdeadline.expired()` をチェックしていました。
* このコミットでは、`pollServer` に `PrepareRead(fd *netFD) error` と `PrepareWrite(fd *netFD) error` という新しいメソッドが追加されました。これらのメソッドは、I/O操作を開始する前にデッドラインが期限切れになっていないかをチェックし、期限切れであれば `errTimeout` を返します。
* これにより、デッドラインチェックのロジックが一元化され、`pollServer` がI/O操作の準備段階でデッドラインを考慮できるようになります。これは、将来的にエッジトリガー型ポーリングを導入する際に、I/Oイベントの処理フローにデッドラインチェックをより自然に組み込むための準備となります。
2. **`accept` および `connect` におけるデッドラインの尊重**:
* `netFD.connect` メソッドでは、`syscall.Connect` を呼び出す前に `fd.pollServer.PrepareWrite(fd)` を呼び出すようになりました。これにより、`connect` 操作がブロックしない場合でも、書き込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
* `netFD.accept` メソッドでは、`accept` システムコールを呼び出す前に `fd.pollServer.PrepareRead(fd)` を呼び出すようになりました。これにより、`accept` 操作がブロックしない場合でも、読み込みデッドラインが過去に設定されていれば即座にタイムアウトエラーが返されるようになります。
* この変更により、`accept` と `connect` のデッドライン処理が `read` と `write` と同様に、操作のブロックの有無にかかわらず一貫した振る舞いをするようになります。
3. **`connect` および `accept` 周りのロック (`rio`/`wio`)**:
* `netFD` 構造体には、`rio` (read I/O) と `wio` (write I/O) という `sync.Mutex` 型のフィールドが追加されました(既存のフィールドの利用または追加)。
* `netFD.connect` メソッドでは、`fd.wio.Lock()` と `defer fd.wio.Unlock()` が追加され、`connect` 操作全体が書き込みI/Oロックで保護されるようになりました。
* `netFD.accept` メソッドでは、`fd.rio.Lock()` と `defer fd.rio.Unlock()` が追加され、`accept` 操作全体が読み込みI/Oロックで保護されるようになりました。
* これらのロックは、`connect` や `accept` のような操作中に `pollServer.WaitRead` や `pollServer.WaitWrite` が並行して呼び出されることによる競合状態を防ぐために導入されました。これにより、`pollServer` の内部状態が安全に管理され、複数のgoroutineからの同時アクセスによるデータ破損や予期せぬ動作が回避されます。
4. **`timeout_test.go` の更新**:
* `TestAcceptDeadlineConnectionAvailable` と `TestConnectDeadlineInThePast` という新しいテストケースが追加されました。
* `TestAcceptDeadlineConnectionAvailable` は、接続がすぐに利用可能な状況で `accept` に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
* `TestConnectDeadlineInThePast` は、接続がブロックせずに確立できる状況で `DialTimeout` に過去のデッドラインを設定した場合に、正しくタイムアウトエラーが発生することを確認します。
* これらのテストは、コミットによって修正された `accept`/`connect` のデッドライン処理の新しい振る舞いを検証し、回帰を防ぐためのものです。
## コアとなるコードの変更箇所
`src/pkg/net/fd_unix.go`
* `pollServer` 構造体に `PrepareRead` と `PrepareWrite` メソッドが追加されました。
* `netFD.connect` メソッドに `fd.wio.Lock()` / `Unlock()` と `fd.pollServer.PrepareWrite(fd)` の呼び出しが追加されました。
* `netFD.Read`, `ReadFrom`, `ReadMsg`, `Write`, `WriteTo`, `WriteMsg` メソッドから、ループ内のデッドラインチェック (`fd.rdeadline.expired()` / `fd.wdeadline.expired()`) が削除され、代わりにループの前に `fd.pollServer.PrepareRead(fd)` / `fd.pollServer.PrepareWrite(fd)` の呼び出しが追加されました。
* `netFD.accept` メソッドに `fd.rio.Lock()` / `Unlock()` と `fd.pollServer.PrepareRead(fd)` の呼び出しが追加されました。
`src/pkg/net/timeout_test.go`
* `TestAcceptDeadlineConnectionAvailable` テスト関数が追加されました。
* `TestConnectDeadlineInThePast` テスト関数が追加されました。
* 既存のテストケースのコメントが一部修正されました。
## コアとなるコードの解説
### `src/pkg/net/fd_unix.go`
```go
// 新しく追加されたメソッド
func (s *pollServer) PrepareRead(fd *netFD) error {
if fd.rdeadline.expired() { // 読み込みデッドラインが期限切れかチェック
return errTimeout // 期限切れならタイムアウトエラーを返す
}
return nil
}
func (s *pollServer) PrepareWrite(fd *netFD) error {
if fd.wdeadline.expired() { // 書き込みデッドラインが期限切れかチェック
return errTimeout // 期限切れならタイムアウトエラーを返す
}
return nil
}
これらのメソッドは、I/O操作(読み込みまたは書き込み)を開始する直前に呼び出され、関連するデッドラインが既に期限切れになっていないかをチェックします。これにより、操作がブロックしない場合でも、デッドラインが尊重されるようになります。
func (fd *netFD) connect(ra syscall.Sockaddr) error {
fd.wio.Lock() // 書き込みI/Oロックを取得
defer fd.wio.Unlock() // 関数終了時にロックを解放
if err := fd.pollServer.PrepareWrite(fd); err != nil { // 接続前に書き込みデッドラインをチェック
return err
}
err := syscall.Connect(fd.sysfd, ra)
// ... 既存の接続ロジック ...
}
connect
操作の開始時に fd.wio.Lock()
でロックを取得し、PrepareWrite
を呼び出すことで、接続が即座に確立できる場合でもデッドラインが適用されるようになりました。ロックは、connect
処理中に WaitWrite
が安全に呼び出されることを保証します。
func (fd *netFD) Read(p []byte) (n int, err error) {
// ... 既存の参照カウント処理 ...
defer fd.decref()
if err := fd.pollServer.PrepareRead(fd); err != nil { // 読み込み前にデッドラインをチェック
return 0, &OpError{"read", fd.net, fd.raddr, err}
}
for {
// 以前ここに存在した fd.rdeadline.expired() のチェックは削除された
n, err = syscall.Read(int(fd.sysfd), p)
// ... 既存の読み込みロジック ...
}
}
Read
メソッド(および他の ReadFrom
, ReadMsg
, Write
, WriteTo
, WriteMsg
メソッド)では、ループ内のデッドラインチェックが削除され、代わりにループの前に PrepareRead
(または PrepareWrite
) が呼び出されるようになりました。これにより、デッドラインチェックのロジックが pollServer
に集約され、よりクリーンなコード構造になります。
func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (netfd *netFD, err error) {
fd.rio.Lock() // 読み込みI/Oロックを取得
defer fd.rio.Unlock() // 関数終了時にロックを解放
if err := fd.incref(false); err != nil {
return nil, err
}
// ... 既存の参照カウント処理 ...
if err = fd.pollServer.PrepareRead(fd); err != nil { // accept前に読み込みデッドラインをチェック
return nil, &OpError{"accept", fd.net, fd.laddr, err}
}
for {
s, rsa, err = accept(fd.sysfd)
// ... 既存のacceptロジック ...
}
}
accept
操作の開始時に fd.rio.Lock()
でロックを取得し、PrepareRead
を呼び出すことで、接続が即座に利用可能な場合でもデッドラインが適用されるようになりました。ロックは、accept
処理中に WaitRead
が安全に呼び出されることを保証します。
src/pkg/net/timeout_test.go
// 新しく追加されたテストケース
func TestAcceptDeadlineConnectionAvailable(t *testing.T) {
// ... OSスキップロジック ...
ln := newLocalListener(t).(*TCPListener)
defer ln.Close()
go func() {
c, err := Dial("tcp", ln.Addr().String()) // クライアントが接続を試みる
if err != nil {
t.Fatalf("Dial: %v", err)
}
defer c.Close()
var buf [1]byte
c.Read(buf[:]) // 接続またはリスナーがクローズされるまでブロック
}()
time.Sleep(10 * time.Millisecond) // クライアント接続が利用可能になるのを待つ
ln.SetDeadline(time.Now().Add(-5 * time.Second)) // 過去のデッドラインを設定
c, err := ln.Accept() // acceptを呼び出す
if err == nil {
defer c.Close()
}
if !isTimeout(err) { // タイムアウトエラーが返されることを期待
t.Fatalf("Accept: got %v; want timeout", err)
}
}
このテストは、リスナーに過去のデッドラインを設定し、その間にクライアントが接続を試みるシナリオをシミュレートします。コミットの変更により、accept
は接続がすぐに利用可能であっても、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。
// 新しく追加されたテストケース
func TestConnectDeadlineInThePast(t *testing.T) {
// ... OSスキップロジック ...
ln := newLocalListener(t).(*TCPListener)
defer ln.Close()
go func() {
c, err := ln.Accept() // サーバー側で接続を受け入れる準備
if err == nil {
defer c.Close()
}
}()
time.Sleep(10 * time.Millisecond) // サーバーがacceptするのを待つ
c, err := DialTimeout("tcp", ln.Addr().String(), -5*time.Second) // 過去のデッドラインで接続を試みる
if err == nil {
defer c.Close()
}
if !isTimeout(err) { // タイムアウトエラーが返されることを期待
t.Fatalf("DialTimeout: got %v; want timeout", err)
}
}
このテストは、DialTimeout
を使用して過去のデッドラインを設定し、接続が即座に確立できる状況をシミュレートします。コミットの変更により、DialTimeout
は接続がすぐに確立できる場合でも、設定されたデッドラインが過去であるためタイムアウトエラーを返すはずです。
関連リンク
- Go CL 7436048: https://golang.org/cl/7436048
参考にした情報源リンク
- Go言語の
net
パッケージに関する公式ドキュメント - Go言語の
sync
パッケージに関する公式ドキュメント - Go言語のネットワークプログラミングに関する一般的な情報源
epoll
やkqueue
などのI/O多重化メカニズムに関する情報- Goの
net
パッケージのソースコード (src/pkg/net/
) - Goの
poll
パッケージのソースコード (src/pkg/runtime/poll/
) (内部実装の詳細を理解するため) - Goの
syscall
パッケージのドキュメント (システムコールエラーコードの理解のため)