buildlet: don't leak file descriptors to buildlets
Also, add count of fds and goroutines to the coordinator's status
page.
Change-Id: I857e609623cfa280716d5d079180d0e4021d0bac
Reviewed-on: https://go-review.googlesource.com/27550
Reviewed-by: Quentin Smith <quentin@golang.org>
diff --git a/buildlet/buildletclient.go b/buildlet/buildletclient.go
index 83fefe1..ef38a61 100644
--- a/buildlet/buildletclient.go
+++ b/buildlet/buildletclient.go
@@ -31,16 +31,17 @@
//
// This constructor returns immediately without testing the host or auth.
func NewClient(ipPort string, kp KeyPair) *Client {
+ tr := &http.Transport{
+ Dial: defaultDialer(),
+ DialTLS: kp.tlsDialer(),
+ IdleConnTimeout: time.Minute,
+ }
c := &Client{
- ipPort: ipPort,
- tls: kp,
- password: kp.Password(),
- httpClient: &http.Client{
- Transport: &http.Transport{
- Dial: defaultDialer(),
- DialTLS: kp.tlsDialer(),
- },
- },
+ ipPort: ipPort,
+ tls: kp,
+ password: kp.Password(),
+ httpClient: &http.Client{Transport: tr},
+ closeFuncs: []func(){tr.CloseIdleConnections},
}
c.setCommon()
return c
@@ -83,6 +84,9 @@
if err == nil {
err = ErrClosed
}
+ for _, fn := range c.closeFuncs {
+ fn()
+ }
c.setPeerDead(err) // which will also cause c.heartbeatFailure to run
})
return nil
@@ -133,6 +137,8 @@
password string // basic auth password or empty for none
remoteBuildlet string // non-empty if for remote buildlets
+ closeFuncs []func() // optional extra code to run on close
+
ctx context.Context
ctxCancel context.CancelFunc
heartbeatFailure func() // optional
diff --git a/cmd/coordinator/status.go b/cmd/coordinator/status.go
index 78cf610..f2a09a7 100644
--- a/cmd/coordinator/status.go
+++ b/cmd/coordinator/status.go
@@ -8,8 +8,11 @@
"bytes"
"fmt"
"html/template"
+ "io"
"net/http"
+ "os"
"os/exec"
+ "runtime"
"sort"
"strings"
"time"
@@ -27,11 +30,13 @@
statusMu.Lock()
data := statusData{
- Total: len(status),
- Uptime: round(time.Now().Sub(processStartTime)),
- Recent: append([]*buildStatus{}, statusDone...),
- DiskFree: df,
- Version: Version,
+ Total: len(status),
+ Uptime: round(time.Now().Sub(processStartTime)),
+ Recent: append([]*buildStatus{}, statusDone...),
+ DiskFree: df,
+ Version: Version,
+ NumFD: fdCount(),
+ NumGoroutine: runtime.NumGoroutine(),
}
for _, st := range status {
data.Active = append(data.Active, st)
@@ -85,6 +90,25 @@
buf.WriteTo(w)
}
+func fdCount() int {
+ f, err := os.Open("/proc/self/fd")
+ if err != nil {
+ return -1
+ }
+ defer f.Close()
+ n := 0
+ for {
+ names, err := f.Readdirnames(1000)
+ n += len(names)
+ if err == io.EOF {
+ return n
+ }
+ if err != nil {
+ return -1
+ }
+ }
+}
+
func diskFree() string {
out, _ := exec.Command("df", "-h").Output()
return string(out)
@@ -92,7 +116,9 @@
// statusData is the data that fills out statusTmpl.
type statusData struct {
- Total int
+ Total int // number of total builds active
+ NumFD int
+ NumGoroutine int
Uptime time.Duration
Active []*buildStatus
Recent []*buildStatus
@@ -157,6 +183,12 @@
<h2 id=disk><a href='#disk'>🔗</a> Disk Space</h2>
<pre>{{.DiskFree}}</pre>
+<h2 id=disk><a href='#fd'>🔗</a> File Descriptors</h2>
+<p>{{.NumFD}}</p>
+
+<h2 id=disk><a href='#goroutines'>🔗</a> Goroutines</h2>
+<p>{{.NumGoroutine}} <a href='/debug/goroutines'>goroutines</a></p>
+
</body>
</html>
`))