Skip to content

Commit 4a2bbc0

Browse files
author
gluck
committed
diag: label agent goroutines with their aria id
A hung daemon is diagnosed from a goroutine dump. Ours was unattributable: eighty goroutines, a dozen of them agents, every agent stack byte-identical (actor.Queue.Recv <- Inbox.Recv <- act <- actProtected <- runWithRecovery), and no way to tell which one belonged to the aria that was not answering. Diagnosing a real stall meant guessing from goroutine ages and correlating against timestamps in another file. pprof.Do costs a map on one goroutine, and the label is INHERITED by every goroutine that one spawns: the turn loop, the tool dispatcher, the provider's stream reader. So the entire subtree of a stuck turn names itself, which is the case that matters - the interesting frame is never the agent loop, it is whatever the agent is blocked inside three levels down. Labels render in /debug/pprof/goroutine?debug=1 and in `go tool pprof -tagfocus=figaro.aria`. They do NOT render in the debug=2 full stack dump, which is the one people reach for first; the comment says so.
1 parent 0ab7ae2 commit 4a2bbc0

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

‎internal/figaro/agent.go‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"log/slog"
77
"runtime"
8+
"runtime/pprof"
89
"slices"
910
"sync"
1011
"sync/atomic"
@@ -308,7 +309,20 @@ func NewAgent(cfg Config) *Agent {
308309

309310
a.resumeStudies()
310311
a.publishMetadata()
311-
go a.runWithRecovery(ctx)
312+
// Label the agent goroutine with its aria id.
313+
//
314+
// A hung daemon is diagnosed from a goroutine dump, and until now that
315+
// dump was unattributable: eighty goroutines, a dozen of them agents,
316+
// every stack identical, and no way to tell which one belonged to the
317+
// aria that was not answering. The label costs a map on one goroutine
318+
// and is INHERITED by every goroutine this one spawns - the turn loop,
319+
// the tool dispatcher, the provider's stream reader - so the whole
320+
// subtree of a stuck turn names itself.
321+
//
322+
// Labels show in `/debug/pprof/goroutine?debug=1` and in `go tool pprof
323+
// -tagfocus`; the debug=2 full stack dump does not render them, so reach
324+
// for debug=1 first when asking "which aria".
325+
go pprof.Do(ctx, pprof.Labels("figaro.aria", a.id), a.runWithRecovery)
312326
return a
313327
}
314328

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package figaro
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"runtime/pprof"
7+
"strings"
8+
"testing"
9+
"time"
10+
)
11+
12+
// A hung daemon is diagnosed from a goroutine dump, and an unlabeled dump
13+
// cannot answer the only question worth asking: WHICH aria is stuck. This
14+
// test asserts the label reaches the profile, and that it is inherited by
15+
// goroutines the agent spawns - the turn loop and the provider stream reader
16+
// are children, and they are where a hang actually lives.
17+
func TestAgentGoroutineLabelsCarryAriaID(t *testing.T) {
18+
ctx, cancel := context.WithCancel(context.Background())
19+
defer cancel()
20+
21+
const ariaID = "94f0752b"
22+
started := make(chan struct{})
23+
childStarted := make(chan struct{})
24+
25+
go pprof.Do(ctx, pprof.Labels("figaro.aria", ariaID), func(ctx context.Context) {
26+
// A child goroutine, as the turn loop and tool dispatch are.
27+
go func() {
28+
close(childStarted)
29+
<-ctx.Done()
30+
}()
31+
close(started)
32+
<-ctx.Done()
33+
})
34+
35+
<-started
36+
<-childStarted
37+
// Give the child a moment to be observable in the profile.
38+
time.Sleep(10 * time.Millisecond)
39+
40+
var buf bytes.Buffer
41+
// debug=1 is the format that renders labels; debug=2 (the full stack
42+
// dump) drops them, which is worth knowing before you go looking.
43+
if err := pprof.Lookup("goroutine").WriteTo(&buf, 1); err != nil {
44+
t.Fatal(err)
45+
}
46+
got := buf.String()
47+
if !strings.Contains(got, ariaID) {
48+
t.Fatalf("goroutine profile carries no aria label; a dump of this daemon is unattributable")
49+
}
50+
if n := strings.Count(got, ariaID); n < 2 {
51+
t.Errorf("label appears %d time(s); it must be inherited by child goroutines too", n)
52+
}
53+
}

0 commit comments

Comments
 (0)