diff --git a/pkg/index/sqlindex/sqlindex.go b/pkg/index/sqlindex/sqlindex.go index e8176ccf2..8d1f5af9f 100644 --- a/pkg/index/sqlindex/sqlindex.go +++ b/pkg/index/sqlindex/sqlindex.go @@ -224,6 +224,7 @@ func (t *iter) Next() bool { } if !t.rows.Next() { if t.seen == t.batchSize { + t.rows.Close() // required for <= Go 1.1, but not Go 1.2, iirc. t.rows = nil return t.Next() } diff --git a/pkg/index/sqlite/sqlite_test.go b/pkg/index/sqlite/sqlite_test.go index 709fa760e..6d6ef9de5 100644 --- a/pkg/index/sqlite/sqlite_test.go +++ b/pkg/index/sqlite/sqlite_test.go @@ -17,10 +17,12 @@ limitations under the License. package sqlite_test import ( + "bytes" "database/sql" "fmt" "io/ioutil" "os" + "os/exec" "sync" "testing" @@ -129,3 +131,47 @@ func TestConcurrency(t *testing.T) { } } } + +func numFDs(t *testing.T) int { + lsofPath, err := exec.LookPath("lsof") + if err != nil { + t.Skipf("No lsof available; skipping test") + } + out, err := exec.Command(lsofPath, "-n", "-p", fmt.Sprint(os.Getpid())).Output() + if err != nil { + t.Skipf("Error running lsof; skipping test: %s", err) + } + return bytes.Count(out, []byte("\n")) - 1 // hacky +} + +func TestFDLeak(t *testing.T) { + if testing.Short() { + t.Skip("Skipping in short mode.") + } + fd0 := numFDs(t) + t.Logf("fd0 = %d", fd0) + + s, clean := makeStorage(t) + defer clean() + + bm := s.BeginBatch() + const numRows = 150 // 3x the batchSize of 50 in sqlindex.go; to gaurantee we do multiple batches + for i := 0; i < numRows; i++ { + bm.Set(fmt.Sprintf("key:%05d", i), fmt.Sprint(i)) + } + if err := s.CommitBatch(bm); err != nil { + t.Fatal(err) + } + for i := 0; i < 5; i++ { + it := s.Find("key:") + n := 0 + for it.Next() { + n++ + } + if n != numRows { + t.Errorf("iterated over %d rows; want %d", n, numRows) + } + it.Close() + t.Logf("fd after iteration %d = %d", i, numFDs(t)) + } +}