Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion internal/telemetry/launch_source.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package telemetry

import "sync"

// LaunchSource identifies how the current mcpproxy process was launched, for
// retention telemetry (spec 044). Detection happens once at process startup
// (via DetectLaunchSourceOnce, added in a later task), with a one-shot
Expand Down Expand Up @@ -110,11 +112,17 @@ var (
)

// launchSourceOnceT is a test-friendly sync.Once clone with reset support.
// Do is mutex-guarded: DetectLaunchSourceOnce is reached concurrently by every
// telemetry-reporting surface (two listeners' status handlers race here), and
// the mutex also orders the launchSourceCached write before any post-Do read.
type launchSourceOnceT struct {
mu sync.Mutex
done bool
}

func (o *launchSourceOnceT) Do(f func()) {
o.mu.Lock()
defer o.mu.Unlock()
if o.done {
return
}
Expand All @@ -124,7 +132,9 @@ func (o *launchSourceOnceT) Do(f func()) {

// resetLaunchSourceOnce is exposed for tests (lower-case).
func resetLaunchSourceOnce() {
launchSourceOnce = launchSourceOnceT{}
launchSourceOnce.mu.Lock()
defer launchSourceOnce.mu.Unlock()
launchSourceOnce.done = false
launchSourceCached = ""
}

Expand Down
28 changes: 28 additions & 0 deletions internal/telemetry/launch_source_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package telemetry

import (
"sync"
"testing"
)

Expand Down Expand Up @@ -166,3 +167,30 @@ func TestDetectLaunchSourceOnce_Cached(t *testing.T) {
t.Fatalf("DetectLaunchSourceOnce returned invalid %q", first)
}
}

// DetectLaunchSourceOnce is reached concurrently by every surface that reports
// telemetry (two listeners' /api/v1/status handlers race here in practice);
// the once-guard must be goroutine-safe. Red under -race before the guard
// gained its mutex.
func TestDetectLaunchSourceOnceConcurrent(t *testing.T) {
resetLaunchSourceOnce()
t.Cleanup(resetLaunchSourceOnce)

const goroutines = 32
results := make([]LaunchSource, goroutines)
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func(i int) {
defer wg.Done()
results[i] = DetectLaunchSourceOnce()
}(i)
}
wg.Wait()

for i := 1; i < goroutines; i++ {
if results[i] != results[0] {
t.Fatalf("goroutine %d saw %q, goroutine 0 saw %q", i, results[i], results[0])
}
}
}
Loading