[インデックス 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
およびunixpacket
、unixgram
)を使用するテストにおいて、テストが終了する際に、ソケットに関連付けられたファイルがソケット自体が完全にクローズされる前に削除されてしまうという問題がありました。
この「ソケットファイルがソケットクローズ前に削除される」という状況は、特に並行して実行されるテストや、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のクリーンアップ処理に悪影響を与える可能性がありました。
このコミットでは、この問題を解決するために以下の変更が加えられました。
-
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
されます。これにより、パケットコネクションがクローズされる直前、かつクローズ処理が完了した後に、関連するソケットファイルが削除されることが保証されます。
-
一時ファイル名の変更:
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
の変更点
-
一時ファイル名の変更:
{"unix", "/tmp/gotest.net"}
と{"unixpacket", "/tmp/gotest.net"}
が、それぞれ{"unix", "/tmp/gotest.net1"}
と{"unixpacket", "/tmp/gotest.net2"}
に変更されました。これにより、異なる種類のUnixドメインソケットテストが同じファイルパスを共有することによる潜在的な競合が回避されます。 -
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)
が実行されることが保証されます。これにより、ソケットがまだアクティブな状態でファイルが削除されるというタイミングの問題が解消されます。 -
古いファイル削除ロジックの削除: テストの最後にあった
switch tt.net
ブロック内のos.Remove(tt.addr)
の呼び出しが削除されました。これは、ファイル削除の責任がdefer
ブロックに移ったため、冗長になったためです。
packetconn_test.go
の変更点
-
closer
ヘルパー関数の導入:closer
という新しいヘルパー関数が定義されました。closer := func(c net.PacketConn, net, addr1, addr2 string) { c.Close() // パケットコネクションをクローズ switch net { case "unixgram": os.Remove(addr1) // クローズ後にソケットファイルを削除 os.Remove(addr2) } }
この関数は、パケットコネクションをクローズし、
unixgram
タイプの場合に、関連するソケットファイル(addr1
とaddr2
)を削除します。 -
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)
に置き換えられました。 これにより、c1
とc2
のパケットコネクションがクローズされる直前、かつクローズ処理が完了した後に、closer
関数が実行され、関連するunixgram
ソケットファイルが確実に削除されるようになります。 -
古いファイル削除ロジックの削除:
conn_test.go
と同様に、テストの最後にあったswitch netstr[0]
ブロック内のos.Remove
の呼び出しが削除されました。
これらの変更は、Goのdefer
メカニズムを効果的に利用して、リソース(この場合はUnixドメインソケットファイル)のライフサイクル管理を、そのリソースを使用するオブジェクト(リスナーやコネクション)のライフサイクルと密接に結びつけることで、テストの堅牢性と信頼性を大幅に向上させています。
関連リンク
- https://github.com/golang/go/commit/ebcaf081a7a58bbe0f7599aeb3e075202205f082
- https://golang.org/cl/7004044 (Go Code Review - Change List)
参考にした情報源リンク
- Go言語の
net
パッケージに関する公式ドキュメント - Go言語の
defer
文に関する公式ドキュメント - Unixドメインソケットに関する一般的な情報(例: Wikipedia, Linux man pages)
os.Remove
に関するGo言語の公式ドキュメント