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

[インデックス 14704] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージにおけるUnixドメインソケット接続のテストの堅牢性を向上させるための変更です。具体的には、テスト中に作成されるUnixドメインソケットの基盤となるファイルが、ソケットが完全にクローズされる前に削除されてしまうことによるテストの不安定性を解消します。

コミット

commit ebcaf081a7a58bbe0f7599aeb3e075202205f082
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Fri Dec 21 14:19:33 2012 +0900

    net: make unix connection tests more robust
    
    Avoids unlink the underlying file before the socket close.
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/7004044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ebcaf081a7a58bbe0f7599aeb3e075202205f082

元コミット内容

net: make unix connection tests more robust Avoids unlink the underlying file before the socket close.

このコミットは、Unix接続のテストをより堅牢にするためのものです。ソケットがクローズされる前に、基盤となるファイルがアンリンク(削除)されることを回避します。

変更の背景

Go言語のnetパッケージには、TCP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための機能が含まれています。Unixドメインソケットは、同じホスト上のプロセス間通信(IPC)に特化したソケットの一種で、ファイルシステム上のパス名に関連付けられます。

このコミットが導入される以前は、netパッケージ内のUnixドメインソケット(unixおよびunixpacketunixgram)を使用するテストにおいて、テストが終了する際に、ソケットに関連付けられたファイルがソケット自体が完全にクローズされる前に削除されてしまうという問題がありました。

この「ソケットファイルがソケットクローズ前に削除される」という状況は、特に並行して実行されるテストや、OSのファイルシステム操作のタイミングに依存する環境において、テストの不安定性(flakiness)を引き起こす可能性がありました。例えば、ソケットがまだアクティブであるにもかかわらず、その参照先であるファイルが削除されてしまうと、後続の操作が失敗したり、予期せぬエラーが発生したりすることが考えられます。これは、テストが時々失敗するが、コード自体には問題がないように見えるという、デバッグが困難な状況を生み出します。

このコミットの目的は、このようなタイミングの問題を解消し、Unixドメインソケット関連のテストがより信頼性高く、安定して実行されるようにすることです。

前提知識の解説

Unixドメインソケット (Unix Domain Sockets / UDS)

Unixドメインソケットは、同じオペレーティングシステム上で動作するプロセス間で通信を行うためのメカニズムです。TCP/IPソケットがネットワークを介した通信に使用されるのに対し、Unixドメインソケットはカーネル内で直接データをやり取りするため、ネットワークオーバーヘッドがなく、非常に高速です。

Unixドメインソケットの大きな特徴は、ファイルシステム上のパス名(例: /tmp/my_socket)に関連付けられる点です。ソケットが作成されると、指定されたパスに特殊なファイル(ソケットファイル)が作成されます。クライアントはこのパス名を使ってサーバーソケットに接続します。ソケットが不要になった場合、このソケットファイルは明示的に削除(アンリンク)する必要があります。もし削除を忘れると、次回同じパスでソケットを作成しようとした際に「ファイルが存在する」というエラーが発生する可能性があります。

defer文 (Go言語)

Go言語のdefer文は、そのdefer文が記述された関数がリターンする直前に、指定された関数呼び出しを実行するものです。これは、リソースの解放(ファイルのクローズ、ロックの解除など)を確実に行うために非常に便利です。deferされた関数は、たとえエラーが発生して関数が途中で終了した場合でも実行されるため、リソースリークを防ぐのに役立ちます。

os.Remove() (Go言語)

os.Remove()関数は、指定されたパスのファイルまたは空のディレクトリを削除します。Unixドメインソケットの場合、この関数を使ってソケットファイルを削除します。

技術的詳細

このコミットの核心的な変更は、Unixドメインソケットのテストにおいて、ソケットファイルの削除タイミングをdefer文を使ってソケットのクローズ処理と同期させる点にあります。

以前のコードでは、Unixドメインソケットのテストにおいて、ソケットファイル(例: /tmp/gotest.net)の削除は、ソケットのクローズ処理とは独立して、テストの後半で行われていました。具体的には、conn_test.goでは<-doneチャネルからの受信後、packetconn_test.goではテストの最後にos.Removeが直接呼び出されていました。

このアプローチの問題点は、ソケットのクローズ処理(ln.Close()c.Close())が非同期的に行われたり、OS内部でのソケットリソースの解放に時間がかかったりする場合に、ソケットがまだ完全にクローズされていないにもかかわらず、関連するファイルがos.Removeによって削除されてしまう可能性があったことです。ソケットファイルが削除されてしまうと、ソケット自体がまだアクティブな状態であっても、ファイルシステム上の参照が失われ、後続の操作やOSのクリーンアップ処理に悪影響を与える可能性がありました。

このコミットでは、この問題を解決するために以下の変更が加えられました。

  1. defer文によるファイル削除の遅延実行:

    • conn_test.goでは、ln.Close()(リスナーのクローズ)をdeferする際に、そのdefer関数内でos.Remove(addr)も実行するように変更されました。これにより、リスナーがクローズされる直前、かつリスナーのクローズ処理が完了した後に、関連するソケットファイルが削除されることが保証されます。
    • packetconn_test.goでは、closerというヘルパー関数が導入されました。この関数はnet.PacketConnと関連するアドレスを受け取り、c.Close()を呼び出した後、unixgramの場合にos.Remove(addr1)os.Remove(addr2)を実行します。そして、このcloser関数がdeferされます。これにより、パケットコネクションがクローズされる直前、かつクローズ処理が完了した後に、関連するソケットファイルが削除されることが保証されます。
  2. 一時ファイル名の変更:

    • conn_test.goでは、unixおよびunixpacketのテストで使用される一時ファイル名が/tmp/gotest.netから/tmp/gotest.net1および/tmp/gotest.net2にそれぞれ変更されました。これは、異なるテストケースが同じファイル名を使用することによる競合を避けるため、またはテストの分離性を高めるための一般的なプラクティスです。

これらの変更により、ソケットファイルはソケットが完全にクローズされた後にのみ削除されるようになり、テストの実行がより予測可能で堅牢になりました。これは、リソースのライフサイクル管理においてdefer文を適切に使用することの重要性を示す良い例です。

コアとなるコードの変更箇所

src/pkg/net/conn_test.go

--- a/src/pkg/net/conn_test.go
+++ b/src/pkg/net/conn_test.go
@@ -17,8 +17,8 @@ var connTests = []struct {
 	addr string
 } {
 	{"tcp", "127.0.0.1:0"},
-	{"unix", "/tmp/gotest.net"},
-	{"unixpacket", "/tmp/gotest.net"},
+	{"unix", "/tmp/gotest.net1"},
+	{"unixpacket", "/tmp/gotest.net2"},
 }
 
 func TestConnAndListener(t *testing.T) {
@@ -41,7 +41,13 @@ func TestConnAndListener(t *testing.T) {
 			return
 		}
 		ln.Addr()
-		defer ln.Close()
+		defer func(ln net.Listener, net, addr string) {
+			ln.Close()
+			switch net {
+			case "unix", "unixpacket":
+				os.Remove(addr)
+			}
+		}(ln, tt.net, tt.addr)
 
 		done := make(chan int)
 		go transponder(t, ln, done)
@@ -68,10 +74,6 @@ func TestConnAndListener(t *testing.T) {
 		}
 
 		<-done
-		switch tt.net {
-		case "unix", "unixpacket":
-			os.Remove(tt.addr)
-		}
 	}
 }

src/pkg/net/packetconn_test.go

--- a/src/pkg/net/packetconn_test.go
+++ b/src/pkg/net/packetconn_test.go
@@ -24,6 +24,15 @@ var packetConnTests = []struct {
 }
 
 func TestPacketConn(t *testing.T) {
+	closer := func(c net.PacketConn, net, addr1, addr2 string) {
+		c.Close()
+		switch net {
+		case "unixgram":
+			os.Remove(addr1)
+			os.Remove(addr2)
+		}
+	}
+
 	for _, tt := range packetConnTests {
 		var wb []byte
 		netstr := strings.Split(tt.net, ":")
@@ -39,7 +48,7 @@ func TestPacketConn(t *testing.T) {
 			case "ip4:icmp", "ip6:ipv6-icmp":
 				continue
 			}
 			id := os.Getpid() & 0xffff
-			wb = newICMPEchoRequest(id, 1, 128, []byte("IP PACKETCONN TEST "))
+			wb = newICMPEchoRequest(id, 1, 128, []byte("IP PACKETCONN TEST"))
 		case "unixgram":
 			switch runtime.GOOS {
 			case "plan9", "windows":
@@ -60,7 +69,7 @@ func TestPacketConn(t *testing.T) {
 		c1.SetDeadline(time.Now().Add(100 * time.Millisecond))
 		c1.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
 		c1.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
-		defer c1.Close()
+		defer closer(c1, netstr[0], tt.addr1, tt.addr2)
 
 		c2, err := net.ListenPacket(tt.net, tt.addr2)
 		if err != nil {
@@ -70,7 +79,7 @@ func TestPacketConn(t *testing.T) {
 		c2.SetDeadline(time.Now().Add(100 * time.Millisecond))
 		c2.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
 		c2.SetWriteDeadline(time.Now().Add(100 * time.Millisecond))
-		defer c2.Close()
+		defer closer(c2, netstr[0], tt.addr1, tt.addr2)
 
 		if _, err := c1.WriteTo(wb, c2.LocalAddr()); err != nil {
 			t.Fatalf("net.PacketConn.WriteTo failed: %v", err)
@@ -86,12 +95,6 @@ func TestPacketConn(t *testing.T) {
 		if _, _, err := c1.ReadFrom(rb1); err != nil {
 			t.Fatalf("net.PacketConn.ReadFrom failed: %v", err)
 		}
-
-		switch netstr[0] {
-		case "unixgram":
-			os.Remove(tt.addr1)
-			os.Remove(tt.addr2)
-		}
 	}
 }

コアとなるコードの解説

conn_test.goの変更点

  1. 一時ファイル名の変更: {"unix", "/tmp/gotest.net"}{"unixpacket", "/tmp/gotest.net"} が、それぞれ {"unix", "/tmp/gotest.net1"}{"unixpacket", "/tmp/gotest.net2"} に変更されました。これにより、異なる種類のUnixドメインソケットテストが同じファイルパスを共有することによる潜在的な競合が回避されます。

  2. deferブロック内のファイル削除: 最も重要な変更は、defer ln.Close() の部分です。以前は単にリスナーをクローズするだけでしたが、新しいコードでは匿名関数をdeferし、その中でリスナーのクローズと、unixまたはunixpacketの場合にos.Remove(addr)を実行するようにしました。

    defer func(ln net.Listener, net, addr string) {
        ln.Close() // リスナーをクローズ
        switch net {
        case "unix", "unixpacket":
            os.Remove(addr) // リスナーがクローズされた後にソケットファイルを削除
        }
    }(ln, tt.net, tt.addr)
    

    この変更により、ln.Close()が実行され、その処理が完了した後にのみos.Remove(addr)が実行されることが保証されます。これにより、ソケットがまだアクティブな状態でファイルが削除されるというタイミングの問題が解消されます。

  3. 古いファイル削除ロジックの削除: テストの最後にあったswitch tt.netブロック内のos.Remove(tt.addr)の呼び出しが削除されました。これは、ファイル削除の責任がdeferブロックに移ったため、冗長になったためです。

packetconn_test.goの変更点

  1. closerヘルパー関数の導入: closerという新しいヘルパー関数が定義されました。

    closer := func(c net.PacketConn, net, addr1, addr2 string) {
        c.Close() // パケットコネクションをクローズ
        switch net {
        case "unixgram":
            os.Remove(addr1) // クローズ後にソケットファイルを削除
            os.Remove(addr2)
        }
    }
    

    この関数は、パケットコネクションをクローズし、unixgramタイプの場合に、関連するソケットファイル(addr1addr2)を削除します。

  2. defer closer(...)への置き換え: 以前は defer c1.Close()defer c2.Close() が直接呼び出されていましたが、これらが defer closer(c1, netstr[0], tt.addr1, tt.addr2)defer closer(c2, netstr[0], tt.addr1, tt.addr2) に置き換えられました。 これにより、c1c2のパケットコネクションがクローズされる直前、かつクローズ処理が完了した後に、closer関数が実行され、関連するunixgramソケットファイルが確実に削除されるようになります。

  3. 古いファイル削除ロジックの削除: conn_test.goと同様に、テストの最後にあったswitch netstr[0]ブロック内のos.Removeの呼び出しが削除されました。

これらの変更は、Goのdeferメカニズムを効果的に利用して、リソース(この場合はUnixドメインソケットファイル)のライフサイクル管理を、そのリソースを使用するオブジェクト(リスナーやコネクション)のライフサイクルと密接に結びつけることで、テストの堅牢性と信頼性を大幅に向上させています。

関連リンク

参考にした情報源リンク

  • Go言語のnetパッケージに関する公式ドキュメント
  • Go言語のdefer文に関する公式ドキュメント
  • Unixドメインソケットに関する一般的な情報(例: Wikipedia, Linux man pages)
  • os.Removeに関するGo言語の公式ドキュメント