[インデックス 14159] ファイルの概要
このコミットは、Go言語のnet
パッケージにおける重要なバグ修正を扱っています。具体的には、pollster
(ポーリング機構)がエラーを返すべき状況でパニックを引き起こしていた問題を解決し、より堅牢なエラーハンドリングを導入しています。
コミット
commit 8c2b131cd11cde8b8d3008e22604b366694fb083
Author: Dave Cheney <dave@cheney.net>
Date: Wed Oct 17 09:41:00 2012 +1100
net: return error from pollster rather than panicing
Fixes #3590.
R=bradfitz, mikioh.mikioh, iant, bsiegert
CC=golang-dev
https://golang.org/cl/6684054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8c2b131cd11cde8b8d3008e22604b366694fb083
元コミット内容
このコミットの元の内容は、net
パッケージのpollster
がエラー発生時にパニックを起こすのではなく、適切なエラーを返すように変更することです。これにより、ネットワーク操作における予期せぬ終了を防ぎ、より予測可能な動作を実現します。
変更の背景
この変更は、Go言語のIssue #3590に対応するものです。Issue #3590は、「netFd.AddFD should return an error from the underlying pollster rather than panicing」と題されており、net
パッケージの内部でファイルディスクリプタ(FD)をポーリングシステムに追加する際に、基盤となるポーリング機構(pollster
)がエラーを返すべき状況でパニック(プログラムの強制終了)を引き起こすという問題が報告されていました。
パニックはGo言語において回復不能なエラーを示すものであり、通常はプログラムのバグや予期せぬ状態を示すために使用されます。しかし、ネットワーク操作のようなI/O処理においては、一時的なシステムリソースの枯渇や不正なファイルディスクリプタなど、回復可能なエラーが発生する可能性があります。このような場合にパニックが発生すると、アプリケーション全体がクラッシュし、サービスの可用性に大きな影響を与えます。
このコミットの目的は、このような回復可能なエラーに対してパニックではなく、Goのエラーインターフェース(error
型)を返すように修正することで、呼び出し元がエラーを適切に処理し、回復できるようにすることでした。これにより、net
パッケージを利用するアプリケーションの堅牢性と安定性が向上します。
前提知識の解説
Go言語のエラーハンドリング
Go言語では、エラーはerror
インターフェースを実装した値として扱われます。関数は通常、最後の戻り値としてerror
型を返し、呼び出し元はそのエラーをチェックして適切な処理を行います。これは、例外処理(try-catch)とは異なり、エラーが明示的に扱われることを強制します。
パニック (Panic) と回復 (Recover)
Go言語のpanic
は、プログラムが回復不能な状態に陥ったことを示すために使用されます。パニックが発生すると、現在の関数の実行が停止し、遅延関数(defer
)が実行され、コールスタックを遡ってパニックが伝播します。最終的に、パニックがrecover
によって捕捉されない場合、プログラムはクラッシュします。recover
はdefer
関数内で呼び出され、パニックからの回復を試みることができますが、これは通常、非常に限定的な状況(例:Webサーバーのハンドラで個々のリクエストのパニックを捕捉する)でのみ推奨されます。
net
パッケージとファイルディスクリプタ (FD)
Go言語のnet
パッケージは、ネットワークI/O操作を提供します。Unix系システムでは、ネットワークソケットもファイルディスクリプタ(FD)として扱われます。net
パッケージは、これらのFDを効率的に管理し、非同期I/Oを実現するために、内部的にポーリング機構(epoll
, kqueue
, select
など)を使用しています。
pollster
pollster
は、Goのnet
パッケージ内部で使用されるI/Oポーリング機構の抽象化です。これは、複数のファイルディスクリプタからのI/Oイベント(読み込み可能、書き込み可能など)を効率的に監視し、準備ができたFDをアプリケーションに通知する役割を担います。これにより、GoのネットワークI/Oはノンブロッキングで動作し、多数の同時接続を効率的に処理できます。
netFD
netFD
は、net
パッケージ内でネットワークファイルディスクリプタを表現する構造体です。これには、実際のシステムコールで使用されるファイルディスクリプタの番号や、そのFDに関連する状態(読み書きの準備ができているかなど)が含まれます。
OpError
net
パッケージで発生するエラーは、しばしばnet.OpError
型にラップされます。OpError
は、エラーが発生した操作(Op
)、ネットワークタイプ(Net
)、ローカルアドレス(Source
)、リモートアドレス(Addr
)、および元のエラー(Err
)に関する情報を含む構造体です。これにより、エラーの発生源と種類を詳細に把握できます。
技術的詳細
このコミットの核心は、src/pkg/net/fd_unix.go
内のpollServer.AddFD
メソッドの変更にあります。このメソッドは、新しいファイルディスクリプタをpollster
に追加する役割を担っています。
変更前は、s.poll.AddFD
がエラーを返した場合、panic("pollServer AddFD " + err.Error())
という形でパニックを引き起こしていました。これは、pollster
へのFDの追加が失敗するという、通常は回復可能なエラーシナリオにおいて、アプリケーション全体をクラッシュさせる原因となっていました。
変更後は、s.poll.AddFD
がエラーを返した場合、パニックではなく、&OpError{"addfd", fd.net, fd.laddr, err}
というnet.OpError
型の値を返します。これにより、呼び出し元はAddFD
操作が失敗したことを検出し、そのエラーを適切に処理できるようになります。例えば、リソースの再試行や、接続のクリーンアップなど、より洗練されたエラー回復ロジックを実装することが可能になります。
また、s.Unlock()
の呼び出し位置も変更されています。以前はs.Unlock()
がif err != nil { panic(...) }
の後にありましたが、変更後はs.Unlock()
がs.poll.AddFD
の直後に移動しています。これは、s.poll.AddFD
がブロックする可能性があるため、ロックを早期に解放することでデッドロックのリスクを減らし、並行性を向上させるための変更と考えられます。
さらに、wake
変数の扱いも変更されています。以前はwake
がtrue
の場合にのみdoWakeup
が設定されていましたが、変更後はwake || doWakeup
という条件でs.Wakeup()
が呼び出されるようになりました。これは、AddFD
操作がpollster
の状態を変更し、即座にポーリングイベントをトリガーする必要がある場合に、確実にWakeup
が呼び出されるようにするための調整です。
新しいテストファイルsrc/pkg/net/fd_unix_test.go
が追加され、この修正が正しく機能することを確認しています。このテストでは、意図的に閉じられたpollServer
を使用してAddFD
がエラーを返す状況を作り出し、そのエラーがパニックではなく*OpError
として返されることを検証しています。
コアとなるコードの変更箇所
src/pkg/net/fd_unix.go
--- a/src/pkg/net/fd_unix.go
+++ b/src/pkg/net/fd_unix.go
@@ -97,15 +97,11 @@ func (s *pollServer) AddFD(fd *netFD, mode int) error {
}
wake, err := s.poll.AddFD(intfd, mode, false)
+ s.Unlock()
if err != nil {
- panic("pollServer AddFD " + err.Error())
- }
- if wake {
- doWakeup = true
+ return &OpError{"addfd", fd.net, fd.laddr, err}
}
- s.Unlock()
-
- if doWakeup {
+ if wake || doWakeup {
s.Wakeup()
}
return nil
src/pkg/net/fd_unix_test.go
--- /dev/null
+++ b/src/pkg/net/fd_unix_test.go
@@ -0,0 +1,60 @@
+// Copyright 2012 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.
+
+// +build darwin freebsd linux netbsd openbsd
+
+package net
+
+import (
+ "testing"
+)
+
+// Issue 3590. netFd.AddFD should return an error
+// from the underlying pollster rather than panicing.
+func TestAddFDReturnsError(t *testing.T) {
+ l, err := Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer l.Close()
+
+ go func() {
+ for {
+ c, err := l.Accept()
+ if err != nil {
+ return
+ }
+ defer c.Close()
+ }
+ }()
+
+ c, err := Dial("tcp", l.Addr().String())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer c.Close()
+
+ // replace c's pollServer with a closed version.
+ ps, err := newPollServer()
+ if err != nil {
+ t.Fatal(err)
+ }
+ ps.poll.Close()
+ c.(*TCPConn).conn.fd.pollServer = ps
+
+ var b [1]byte
+ _, err = c.Read(b[:])
+ if err, ok := err.(*OpError); ok {
+ if err.Op == "addfd" {
+ return
+ }
+ if err, ok := err.Err.(*OpError); ok {
+ // the err is sometimes wrapped by another OpError
+ if err.Op == "addfd" {
+ return
+ }
+ }
+ }
+ t.Error(err)
+}
コアとなるコードの解説
src/pkg/net/fd_unix.go
の変更点
-
パニックからエラーリターンへの変更:
- 変更前:
if err != nil { panic("pollServer AddFD " + err.Error()) }
- 変更後:
if err != nil { return &OpError{"addfd", fd.net, fd.laddr, err} }
この変更が最も重要です。s.poll.AddFD
からのエラーがパニックを引き起こす代わりに、net.OpError
型として返されるようになりました。これにより、呼び出し元はエラーを捕捉し、適切に処理できるようになります。OpError
は、エラーが発生した操作("addfd"
)、ネットワークタイプ(fd.net
)、ローカルアドレス(fd.laddr
)、および元のエラー(err
)を含むため、エラーのコンテキストが豊富になります。
- 変更前:
-
ロック解放のタイミング変更:
- 変更前:
s.Unlock()
がpanic
の後にあった。 - 変更後:
s.Unlock()
がs.poll.AddFD
の直後に移動。 これは、s.poll.AddFD
が内部でブロックする可能性があるため、ロックを保持したままブロックするのを避けるための変更です。ロックを早期に解放することで、他のゴルーチンがpollServer
にアクセスできるようになり、並行性が向上します。
- 変更前:
-
Wakeup
呼び出し条件の変更:- 変更前:
if doWakeup { s.Wakeup() }
- 変更後:
if wake || doWakeup { s.Wakeup() }
wake
変数はs.poll.AddFD
が返す値で、pollster
が即座にウェイクアップする必要があるかどうかを示します。この変更により、wake
がtrue
の場合、または以前のロジックでdoWakeup
が設定されていた場合に、確実にs.Wakeup()
が呼び出されるようになります。これは、pollster
の状態変更を即座に反映させるために重要です。
- 変更前:
src/pkg/net/fd_unix_test.go
の追加
この新しいテストファイルは、Issue 3590
で報告された問題を再現し、修正が正しく機能することを確認するために追加されました。
-
テストの目的:
netFd.AddFD
が基盤となるpollster
からパニックではなくエラーを返すことを検証します。 -
テストシナリオ:
- TCPリスナーとクライアント接続を確立します。
- クライアント接続の内部にある
pollServer
を、意図的に閉じられた新しいpollServer
に置き換えます。ps.poll.Close()
を呼び出すことで、このpollServer
は無効な状態になります。 - クライアント接続に対して
c.Read(b[:])
を呼び出します。Read
操作は内部的にpollServer.AddFD
を呼び出して、ソケットをポーリングシステムに登録しようとします。 - 閉じられた
pollServer
に対してAddFD
が呼び出されるため、s.poll.AddFD
はエラーを返します。 - テストは、このエラーがパニックではなく
*OpError
型として返され、そのOp
フィールドが"addfd"
であることを検証します。 - エラーが
*OpError
でラップされている可能性も考慮し、ネストされたOpError
もチェックしています。
このテストは、修正が期待通りに動作し、pollster
からのエラーが適切にerror
インターフェースを通じて伝播されることを保証します。
関連リンク
- Go Issue #3590: https://github.com/golang/go/issues/3590
- Go CL 6684054: https://golang.org/cl/6684054
参考にした情報源リンク
- Go言語公式ドキュメント
- Go言語のソースコード
- Go言語のエラーハンドリングに関する一般的な情報源
- Unix系システムのファイルディスクリプタとI/Oポーリング(epoll, kqueueなど)に関する情報源
- Go言語の
net
パッケージの内部実装に関する解説記事(もしあれば) - Dave Cheney氏のブログやGoに関する記事(もしあれば)