summaryrefslogtreecommitdiffhomepage
path: root/block/block.go
diff options
context:
space:
mode:
Diffstat (limited to 'block/block.go')
-rw-r--r--block/block.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/block/block.go b/block/block.go
new file mode 100644
index 000000000..4b610579d
--- /dev/null
+++ b/block/block.go
@@ -0,0 +1,112 @@
+// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package block TODO TODO TODO.
+package block
+
+// Next steps:
+// * refactor out chunk selection, with tests.
+// * support AllowNextLine
+// * add docs
+
+import (
+ "bytes"
+ "runtime"
+ "strconv"
+ "time"
+
+ "tailscale.com/types/logger"
+)
+
+func Watch(maxMinutes int, logf logger.Logf) {
+ buf := make([]byte, 4096)
+ for {
+ time.Sleep(time.Duration(maxMinutes) * time.Minute)
+
+ // Read all goroutine stacks.
+ // It'd be nicer to use pprof.Lookup("goroutine"),
+ // but it doesn't have the per-goroutine header that includes
+ // how long that goroutine has been blocked.
+ for {
+ n := runtime.Stack(buf, true)
+ if n < len(buf) {
+ buf = buf[:n]
+ break
+ }
+ buf = buf[:cap(buf)]
+ buf = append(buf, 0)
+ }
+
+ // Parse the goroutine stacks, looking for goroutines that have been blocked for a long time.
+ // This is best-effort; the formatting that the runtime uses can change.
+ // See runtime.goroutineheader for the code that writes the header.
+
+ // Stacks come in goroutine chunks separated by blank lines.
+ chunks := bytes.Split(buf, doubleNewline)
+
+ // Check each goroutine to see whether it is over the time limit.
+ for _, chunk := range chunks {
+ minutes, ok := goroutineMinutesBlocked(chunk)
+ if !ok {
+ continue
+ }
+ if minutes > maxMinutes {
+ // Dump all stacks.
+ logf("detected goroutines blocked > %d minutes\n%q", maxMinutes, buf)
+ break
+ }
+ }
+ }
+}
+
+func AllowNextLine() {
+
+}
+
+func AllowLine() {
+
+}
+
+var (
+ doubleNewline = []byte("\n\n")
+ goroutine = []byte("goroutine ")
+ commaSpace = []byte(", ")
+ spaceMinutes = []byte(" minutes")
+)
+
+// goroutineMinutesBlocked reports the number of minutes the goroutine
+// whose stack is in buf was blocked for (and whether the parse succeeded).
+func goroutineMinutesBlocked(stack []byte) (minutes int, ok bool) {
+ // Each chunk begins like
+ // goroutine 0 [idle]:
+ // or
+ // goroutine 1 [chan receive, 9 minutes]:
+ // We only care about lines that have a minutes count.
+ if !bytes.HasPrefix(stack, goroutine) {
+ return 0, false
+ }
+ // Extract first line.
+ i := bytes.IndexByte(stack, '\n')
+ if i < 0 {
+ return 0, false
+ }
+ stack = stack[:i]
+ // Find the part between the comma and the m.
+ i = bytes.Index(stack, commaSpace)
+ if i < 0 {
+ return 0, false
+ }
+ stack = stack[i+len(commaSpace):]
+ i = bytes.Index(stack, spaceMinutes)
+ if i < 0 {
+ return 0, false
+ }
+ stack = stack[:i]
+ // Attempt to decode the number of minutes.
+ minutes, err := strconv.Atoi(string(stack))
+ if err != nil {
+ return 0, false
+ }
+ return minutes, true
+}