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

[インデックス 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によって捕捉されない場合、プログラムはクラッシュします。recoverdefer関数内で呼び出され、パニックからの回復を試みることができますが、これは通常、非常に限定的な状況(例: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変数の扱いも変更されています。以前はwaketrueの場合にのみ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の変更点

  1. パニックからエラーリターンへの変更:

    • 変更前: 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)を含むため、エラーのコンテキストが豊富になります。
  2. ロック解放のタイミング変更:

    • 変更前: s.Unlock()panicの後にあった。
    • 変更後: s.Unlock()s.poll.AddFDの直後に移動。 これは、s.poll.AddFDが内部でブロックする可能性があるため、ロックを保持したままブロックするのを避けるための変更です。ロックを早期に解放することで、他のゴルーチンがpollServerにアクセスできるようになり、並行性が向上します。
  3. Wakeup呼び出し条件の変更:

    • 変更前: if doWakeup { s.Wakeup() }
    • 変更後: if wake || doWakeup { s.Wakeup() } wake変数はs.poll.AddFDが返す値で、pollsterが即座にウェイクアップする必要があるかどうかを示します。この変更により、waketrueの場合、または以前のロジックでdoWakeupが設定されていた場合に、確実にs.Wakeup()が呼び出されるようになります。これは、pollsterの状態変更を即座に反映させるために重要です。

src/pkg/net/fd_unix_test.goの追加

この新しいテストファイルは、Issue 3590で報告された問題を再現し、修正が正しく機能することを確認するために追加されました。

  1. テストの目的: netFd.AddFDが基盤となるpollsterからパニックではなくエラーを返すことを検証します。

  2. テストシナリオ:

    • 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言語公式ドキュメント
  • Go言語のソースコード
  • Go言語のエラーハンドリングに関する一般的な情報源
  • Unix系システムのファイルディスクリプタとI/Oポーリング(epoll, kqueueなど)に関する情報源
  • Go言語のnetパッケージの内部実装に関する解説記事(もしあれば)
  • Dave Cheney氏のブログやGoに関する記事(もしあれば)