[インデックス 18223] ファイルの概要
このコミットは、Go言語のネットワークパッケージ(net
)におけるWindows環境でのAccept
処理の堅牢性を向上させるためのものです。具体的には、AcceptEx
システムコールが返す特定のエラー(WSAECONNRESET
とERROR_NETNAME_DELETED
)を無視し、接続の再試行を可能にすることで、不安定なネットワーク条件下でのサーバーアプリケーションの安定性を高めます。
コミット
commit c7ef348bad102b3427b4242018e92eba17d079ba
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Sun Jan 12 12:20:16 2014 +1100
net: ignore some errors in windows Accept
Fixes #6987
R=golang-codereviews, dvyukov
CC=golang-codereviews
https://golang.org/cl/49490043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c7ef348bad102b3427b4242018e92eba17d079ba
元コミット内容
net: ignore some errors in windows Accept
Fixes #6987
R=golang-codereviews, dvyukov
CC=golang-codereviews
https://golang.org/cl/49490043
変更の背景
この変更は、Go言語のIssue #6987に対応するものです。Windows環境でnet.Listen
とListener.Accept()
を使用してTCPサーバーを構築する際、クライアントが接続を確立する前に切断した場合(特にAcceptEx
が完了する前にリセットされた場合)、Accept
がWSAECONNRESET
やERROR_NETNAME_DELETED
といったエラーを返すことがありました。これらのエラーは、新しい接続試行に関連するものであり、リスナー自体が問題なく動作しているにもかかわらず、Accept
が失敗し、サーバーが新しい接続を受け付けられなくなる可能性がありました。
この挙動は、特に負荷の高い環境や不安定なネットワーク環境において、サーバーアプリケーションの可用性を低下させる原因となります。このコミットの目的は、これらの特定のエラーを無視し、Accept
ループを継続させることで、サーバーがより堅牢に動作し、一時的な接続リセットによってサービスが中断されないようにすることです。
前提知識の解説
Windows Sockets (Winsock) と AcceptEx
Windows環境におけるネットワークプログラミングは、Winsock(Windows Sockets API)を通じて行われます。AcceptEx
はWinsock APIの一部であり、非同期I/O(Overlapped I/O)をサポートする高度な関数です。これは、新しい接続を受け入れる際に、接続されたソケットの作成と、そのソケットに対する最初のデータ受信を効率的に結合するために使用されます。
通常のaccept
関数とは異なり、AcceptEx
はI/O完了ポート(IOCP)と組み合わせて使用されることが多く、多数の同時接続を効率的に処理するサーバーアプリケーションで利用されます。AcceptEx
は、接続を受け入れるだけでなく、接続されたソケットのローカルアドレスとリモートアドレスの情報を、事前に割り当てられたバッファに書き込む機能も持っています。
エラーコード WSAECONNRESET
と ERROR_NETNAME_DELETED
-
WSAECONNRESET
(10054): このエラーは、接続がピアによって強制的にリセットされたことを示します。TCP接続において、相手側が予期せず接続を終了した場合(例えば、RST
パケットを送信した場合)に発生します。AcceptEx
のコンテキストでは、クライアントが接続を試みたものの、AcceptEx
が完了する前に切断してしまった場合に発生する可能性があります。 -
ERROR_NETNAME_DELETED
(64): このエラーは、ネットワーク接続が失われたことを示します。これは、ネットワークケーブルが抜かれた、ネットワークアダプタが無効になった、またはリモートホストが利用できなくなったなど、より広範なネットワークの問題に関連して発生することがあります。AcceptEx
のコンテキストでは、接続試行中にネットワークパスが失われた場合に発生する可能性があります。
これらのエラーは、通常、アプリケーションレベルで処理されるべき「接続の問題」を示すものですが、AcceptEx
が新しい接続を受け入れる段階で発生すると、リスナーの動作を妨げる可能性があります。このコミットは、これらのエラーがリスナーの継続的な動作を妨げないように、特定の状況下でこれらを無視するアプローチを取っています。
Go言語の net
パッケージと syscall
パッケージ
Go言語のnet
パッケージは、TCP/IPネットワークプログラミングのための高レベルなインターフェースを提供します。このパッケージは、内部的にOS固有のシステムコール(WindowsではWinsock API)を呼び出すためにsyscall
パッケージを利用しています。
netFD
構造体は、ネットワークファイルディスクリプタ(ソケット)を抽象化し、readLock
やreadUnlock
などのメソッドを通じて、並行アクセスから保護します。operation
構造体は、非同期I/O操作の状態を管理するために使用されます。
技術的詳細
このコミットの主要な変更点は、src/pkg/net/fd_windows.go
ファイル内のnetFD.accept
メソッドのロジックです。
変更前は、netFD.accept
メソッド内で直接AcceptEx
を呼び出し、エラーが発生した場合はすぐにそれを返していました。
変更後は、netFD.accept
メソッドがacceptOne
という新しい内部ヘルパー関数を呼び出すようにリファクタリングされています。acceptOne
は単一のAcceptEx
操作を実行し、その結果を返します。
netFD.accept
メソッドは、acceptOne
をループ内で呼び出すようになりました。このループ内で、acceptOne
がエラーを返した場合、そのエラーが*OpError
型であり、かつその内部のエラーがsyscall.Errno
型であるかをチェックします。さらに、そのerrno
がsyscall.ERROR_NETNAME_DELETED
またはsyscall.WSAECONNRESET
であるかを判定します。
もしエラーがこれらの特定のエラーコードのいずれかであれば、そのエラーは無視され、ループは継続されます。これにより、AcceptEx
が一時的な接続リセットによって失敗しても、Accept
メソッドは新しい接続を待ち続けることができます。それ以外のエラーが発生した場合は、ループを抜けてエラーを呼び出し元に返します。
この変更により、Windows環境でのGoのネットワークサーバーは、一時的なネットワークの問題やクライアントの接続リセットに対してより堅牢になります。
また、src/pkg/syscall/ztypes_windows.go
ファイルには、ERROR_NETNAME_DELETED
とWSAECONNRESET
の定数が追加され、Goのsyscall
パッケージからこれらのWindowsエラーコードにアクセスできるようになっています。
さらに、src/pkg/net/net_windows_test.go
に新しいテストケースTestAcceptIgnoreSomeErrors
が追加されています。このテストは、子プロセスを起動し、意図的に接続をリセットすることで、AcceptEx
がWSAECONNRESET
などのエラーを返す状況をシミュレートします。そして、GoのAccept
実装がこれらのエラーを適切に無視し、後続の有効な接続を受け入れられることを検証します。これは、cmd.Process.Kill()
を使用して子プロセスを強制終了することで、接続リセットをトリガーしています。
コアとなるコードの変更箇所
src/pkg/net/fd_windows.go
netFD.accept
関数がリファクタリングされ、acceptOne
という新しいヘルパー関数が導入されました。netFD.accept
内でacceptOne
を呼び出すループが追加され、syscall.ERROR_NETNAME_DELETED
とsyscall.WSAECONNRESET
のエラーが無視されるようになりました。
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -513,12 +513,7 @@ func (fd *netFD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) {
})\n }\n \n-func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n-\tif err := fd.readLock(); err != nil {\n-\t\treturn nil, err\n-\t}\n-\tdefer fd.readUnlock()\n-\n+func (fd *netFD) acceptOne(toAddr func(syscall.Sockaddr) Addr, rawsa []syscall.RawSockaddrAny, o *operation) (*netFD, error) {\n \t// Get new socket.\n \ts, err := sysSocket(fd.family, fd.sotype, 0)\n \tif err != nil {\n@@ -537,9 +532,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n \t}\n \n \t// Submit accept request.\n-\to := &fd.rop\n \to.handle = s\n-\tvar rawsa [2]syscall.RawSockaddrAny\n \to.rsan = int32(unsafe.Sizeof(rawsa[0]))\n \t_, err = rsrv.ExecIO(o, "AcceptEx", func(o *operation) error {\n \t\treturn syscall.AcceptEx(o.fd.sysfd, o.handle, (*byte)(unsafe.Pointer(&rawsa[0])), 0, uint32(o.rsan), uint32(o.rsan), &o.qty, &o.o)\n@@ -556,6 +549,45 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n \t\treturn nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}\n \t}\n \n+\treturn netfd, nil\n+}\n+\n+func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n+\tif err := fd.readLock(); err != nil {\n+\t\treturn nil, err\n+\t}\n+\tdefer fd.readUnlock()\n+\n+\to := &fd.rop\n+\tvar netfd *netFD\n+\tvar err error\n+\tvar rawsa [2]syscall.RawSockaddrAny\n+\tfor {\n+\t\tnetfd, err = fd.acceptOne(toAddr, rawsa[:], o)\n+\t\tif err == nil {\n+\t\t\tbreak\n+\t\t}\n+\t\t// Sometimes we see WSAECONNRESET and ERROR_NETNAME_DELETED is\n+\t\t// returned here. These happen if connection reset is received\n+\t\t// before AcceptEx could complete. These errors relate to new\n+\t\t// connection, not to AcceptEx, so ignore broken connection and\n+\t\t// try AcceptEx again for more connections.\n+\t\toperr, ok := err.(*OpError)\n+\t\tif !ok {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\terrno, ok := operr.Err.(syscall.Errno)\n+\t\tif !ok {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tswitch errno {\n+\t\tcase syscall.ERROR_NETNAME_DELETED, syscall.WSAECONNRESET:\n+\t\t\t// ignore these and try again\n+\t\tdefault:\n+\t\t\treturn nil, err\n+\t\t}\n+\t}\n+\n \t// Get local and peer addr out of AcceptEx buffer.\n \tvar lrsa, rrsa *syscall.RawSockaddrAny\n \tvar llen, rlen int32
src/pkg/syscall/ztypes_windows.go
ERROR_NETNAME_DELETED
とWSAECONNRESET
の定数が追加されました。
--- a/src/pkg/syscall/ztypes_windows.go
+++ b/src/pkg/syscall/ztypes_windows.go
@@ -11,6 +11,7 @@ const (\n \tERROR_ACCESS_DENIED Errno = 5\n \tERROR_NO_MORE_FILES Errno = 18\n \tERROR_HANDLE_EOF Errno = 38\n+\tERROR_NETNAME_DELETED Errno = 64\n \tERROR_FILE_EXISTS Errno = 80\n \tERROR_BROKEN_PIPE Errno = 109\n \tERROR_BUFFER_OVERFLOW Errno = 111\n@@ -23,6 +24,7 @@ const (\n \tERROR_IO_PENDING Errno = 997\n \tERROR_NOT_FOUND Errno = 1168\n \tWSAEACCES Errno = 10013\n+\tWSAECONNRESET Errno = 10054\n )\n \n const (
src/pkg/net/net_windows_test.go
TestAcceptIgnoreSomeErrors
という新しいテストファイルとテストケースが追加されました。
--- /dev/null
+++ b/src/pkg/net/net_windows_test.go
@@ -0,0 +1,146 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package net
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "syscall"
+ "testing"
+ "time"
+)
+
+func TestAcceptIgnoreSomeErrors(t *testing.T) {
+ recv := func(ln Listener) (string, error) {
+ c, err := ln.Accept()
+ if err != nil {
+ // Display windows errno in error message.
+ operr, ok := err.(*OpError)
+ if !ok {
+ return "", err
+ }
+ errno, ok := operr.Err.(syscall.Errno)
+ if !ok {
+ return "", err
+ }
+ return "", fmt.Errorf("%v (windows errno=%d)", err, errno)
+ }
+ defer c.Close()
+
+ b := make([]byte, 100)
+ n, err := c.Read(b)
+ if err != nil && err != io.EOF {
+ return "", err
+ }
+ return string(b[:n]), nil
+ }
+
+ send := func(addr string, data string) error {
+ c, err := Dial("tcp", addr)
+ if err != nil {
+ return err
+ }
+ defer c.Close()
+
+ b := []byte(data)
+ n, err := c.Write(b)
+ if err != nil {
+ return err
+ }
+ if n != len(b) {
+ return fmt.Errorf(`Only %d chars of string "%s" sent`, n, data)
+ }
+ return nil
+ }
+
+ if envaddr := os.Getenv("GOTEST_DIAL_ADDR"); envaddr != "" {
+ // In child process.
+ c, err := Dial("tcp", envaddr)
+ if err != nil {
+ t.Fatalf("Dial failed: %v", err)
+ }
+ fmt.Printf("sleeping\n")
+ time.Sleep(time.Minute) // process will be killed here
+ c.Close()
+ }
+
+ ln, err := Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("Listen failed: %v", err)
+ }
+ defer ln.Close()
+
+ // Start child process that connects to our listener.
+ cmd := exec.Command(os.Args[0], "-test.run=TestAcceptIgnoreSomeErrors")
+ cmd.Env = append(os.Environ(), "GOTEST_DIAL_ADDR="+ln.Addr().String())
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ t.Fatalf("cmd.StdoutPipe failed: %v", err)
+ }
+ err = cmd.Start()
+ if err != nil {
+ t.Fatalf("cmd.Start failed: %v\n%s\n", err)
+ }\n\toutReader := bufio.NewReader(stdout)\n\tfor {\n\t\ts, err := outReader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tt.Fatalf("reading stdout failed: %v", err)\n\t\t}\n\t\tif s == "sleeping\\n" {\n\t\t\tbreak\n\t\t}\n\t}\n\tdefer cmd.Wait() // ignore error - we know it is getting killed\n\n\tconst alittle = 100 * time.Millisecond\n\ttime.Sleep(alittle)\n\tcmd.Process.Kill() // the only way to trigger the errors\n\ttime.Sleep(alittle)\n\n\t// Send second connection data (with delay in a separate goroutine).\n\tresult := make(chan error)\n\tgo func() {\n\t\ttime.Sleep(alittle)\n\t\terr = send(ln.Addr().String(), "abc")\n\t\tif err != nil {\n\t\t\tresult <- err\n\t\t}\n\t\tresult <- nil\n\t}()\n\tdefer func() {\n\t\terr := <-result\n\t\tif err != nil {\n\t\t\tt.Fatalf("send failed: %v", err)\n\t\t}\n\t}()\n\n\t// Receive first or second connection.\n\ts, err := recv(ln)\n\tif err != nil {\n\t\tt.Fatalf("recv failed: %v", err)\n\t}\n\tswitch s {\n\tcase "":\n\t\t// First connection data is received, lets get second connection data.\n\tcase "abc":\n\t\t// First connection is lost forever, but that is ok.\n\t\treturn\n\tdefault:\n\t\tt.Fatalf(`"%s" received from recv, but "" or "abc" expected`, s)\n\t}\n\n\t// Get second connection data.\n\ts, err = recv(ln)\n\tif err != nil {\n\t\tt.Fatalf("recv failed: %v", err)\n\t}\n\tif s != "abc" {\n\t\tt.Fatalf(`"%s" received from recv, but "abc" expected`, s)\n\t}\n}\n```
## コアとなるコードの解説
### `fd_windows.go` の変更
`netFD.accept`関数は、Goの`net`パッケージにおけるWindows固有の`Accept`実装の中核です。この関数は、新しいネットワーク接続を受け入れる役割を担います。
変更前は、`accept`関数内で直接`syscall.AcceptEx`を呼び出し、エラーが発生した場合は即座にそのエラーを返していました。これにより、`AcceptEx`が`WSAECONNRESET`や`ERROR_NETNAME_DELETED`のような一時的なエラーを返した場合でも、`accept`呼び出し全体が失敗し、サーバーが新しい接続を受け付けられなくなる可能性がありました。
変更後、`acceptOne`という新しいヘルパー関数が導入されました。この関数は、単一の`AcceptEx`操作を実行し、その結果(新しい`netFD`とエラー)を返します。
元の`netFD.accept`関数は、`acceptOne`を無限ループ(`for {}`)内で呼び出すように変更されました。
このループの内部では、`acceptOne`がエラーを返した場合に、そのエラーが`*OpError`型であるか、そしてその`Err`フィールドが`syscall.Errno`型であるかをチェックします。
さらに、その`errno`が`syscall.ERROR_NETNAME_DELETED`または`syscall.WSAECONNRESET`のいずれかであるかを`switch`文で判定します。
* `case syscall.ERROR_NETNAME_DELETED, syscall.WSAECONNRESET:` の場合:
これらのエラーは、接続がリセットされたり、ネットワーク名が削除されたりといった、一時的な接続の問題を示します。これらのエラーは、新しい接続試行に関連するものであり、リスナー自体が正常に機能している可能性が高いです。そのため、これらのエラーは無視され、`continue`によってループの次のイテレーションに進み、`AcceptEx`を再試行します。これにより、サーバーは一時的な問題にもかかわらず、新しい接続を受け入れ続けることができます。
* `default:` の場合:
上記以外のエラーが発生した場合は、そのエラーは無視できない重大な問題であると判断され、`return nil, err`によってエラーが呼び出し元に返され、`accept`ループは終了します。
このロジックにより、GoのWindowsネットワークサーバーは、特定の一般的な一時的エラーを透過的に処理し、より堅牢な動作を実現します。
### `ztypes_windows.go` の変更
このファイルは、Goの`syscall`パッケージがWindows APIとやり取りするために使用する型定義と定数を格納しています。
`ERROR_NETNAME_DELETED`と`WSAECONNRESET`の定数が追加されたことで、GoのコードからこれらのWindowsエラーコードを直接参照できるようになり、`fd_windows.go`でのエラーハンドリングが可能になりました。
### `net_windows_test.go` の追加
`TestAcceptIgnoreSomeErrors`テストは、このコミットの変更が意図通りに機能することを検証するために非常に重要です。
このテストは、以下のようなシナリオをシミュレートします。
1. メインプロセスがTCPリスナーを起動します。
2. 子プロセスを起動し、その子プロセスにリスナーのアドレスを環境変数で渡します。
3. 子プロセスはリスナーに接続し、意図的に長時間スリープします。
4. メインプロセスは、子プロセスがスリープしている間に、子プロセスを強制終了(`cmd.Process.Kill()`)します。これにより、子プロセスからの接続が予期せず切断され、`AcceptEx`が`WSAECONNRESET`などのエラーを返す状況を作り出します。
5. メインプロセスは、強制終了後に別のゴルーチンで新しい接続を送信します。
6. メインプロセスは、`Listener.Accept()`を呼び出し、最初の接続がエラーで失われたとしても、2番目の接続が正常に受け入れられることを検証します。
このテストは、Goの`Accept`実装が、一時的な接続リセットエラーを適切に無視し、後続の有効な接続を処理できることを実証します。
## 関連リンク
* Go Issue #6987: [net: ignore some errors in windows Accept](https://github.com/golang/go/issues/6987)
* Go Code Review: [https://golang.org/cl/49490043](https://golang.org/cl/49490043)
## 参考にした情報源リンク
* Microsoft Docs: [AcceptEx function](https://learn.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex)
* Microsoft Docs: [Winsock Error Codes](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2)
* Microsoft Docs: [System Error Codes (60-99)](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--60-99-) (for `ERROR_NETNAME_DELETED`)
* Go `net` package documentation: [https://pkg.go.dev/net](https://pkg.go.dev/net)
* Go `syscall` package documentation: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
* Go `os/exec` package documentation: [https://pkg.go.dev/os/exec](https://pkg.go.dev/os/exec)
* Go `testing` package documentation: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go `time` package documentation: [https://pkg.go.dev/time](https://pkg.go.dev/time)
* Go `fmt` package documentation: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
* Go `io` package documentation: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go `bufio` package documentation: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
* Go `os` package documentation: [https://pkg.go.dev/os](https://pkg.go.dev/os)
* Go `unsafe` package documentation: [https://pkg.go.dev/unsafe](https://pkg.go.dev/unsafe)
* Go `sync` package documentation: [https://pkg.go.dev/sync](https://pkg.go.dev/sync) (for `readLock`/`readUnlock` context)
* Go `net` package source code: [https://github.com/golang/go/tree/master/src/net](https://github.com/golang/go/tree/master/src/net)
* Go `syscall` package source code: [https://github.com/golang/go/tree/master/src/syscall](https://github.com/golang/go/tree/master/src/syscall)
* Go `src/pkg/net/fd_windows.go` (before this commit): [https://github.com/golang/go/blob/64d56c73e0/src/pkg/net/fd_windows.go](https://github.com/golang/go/blob/64d56c73e0/src/pkg/net/fd_windows.go)
* Go `src/pkg/syscall/ztypes_windows.go` (before this commit): [https://github.com/golang/go/blob/28cd3f6169/src/pkg/syscall/ztypes_windows.go](https://github.com/golang/go/blob/28cd3f6169/src/pkg/syscall/ztypes_windows.go)
* Go `src/pkg/net/net_windows_test.go` (after this commit): [https://github.com/golang/go/blob/c7ef348bad102b3427b4242018e92eba17d079ba/src/pkg/net/net_windows_test.go](https://github.com/golang/go/blob/c7ef348bad102b3427b4242018e92eba17d079ba/src/pkg/net/net_windows_test.go)