Skip to content

Commit

Permalink
Fix and detect race condition in clienttrace.go
Browse files Browse the repository at this point in the history
ct.root can be updated without holding the mutex on ct. Also, addEvent
can be called on a nil ct.root.

Fix both of these issues by moving the mutex acquisition logic so that
ct.root is only touched while the mutex is held.
  • Loading branch information
gregorynisbet-google committed Jun 11, 2024
1 parent 5c39041 commit eb1dc63
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 5 deletions.
18 changes: 13 additions & 5 deletions instrumentation/net/http/httptrace/otelhttptrace/clienttrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ func NewClientTrace(ctx context.Context, opts ...ClientTraceOption) *httptrace.C
}

func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue) {
ct.mtx.Lock()
defer ct.mtx.Unlock()

if !ct.useSpans {
if ct.root == nil {
ct.root = trace.SpanFromContext(ct.Context)
Expand All @@ -194,9 +197,6 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
return
}

ct.mtx.Lock()
defer ct.mtx.Unlock()

if hookCtx, found := ct.activeHooks[hook]; !found {
var sp trace.Span
ct.activeHooks[hook], sp = ct.tr.Start(ct.getParentContext(hook), spanName, trace.WithAttributes(attrs...), trace.WithSpanKind(trace.SpanKindClient))
Expand All @@ -214,6 +214,13 @@ func (ct *clientTracer) start(hook, spanName string, attrs ...attribute.KeyValue
}

func (ct *clientTracer) end(hook string, err error, attrs ...attribute.KeyValue) {
ct.mtx.Lock()
defer ct.mtx.Unlock()

if ct.root == nil {
return
}

if !ct.useSpans {
if err != nil {
attrs = append(attrs, attribute.String(hook+".error", err.Error()))
Expand All @@ -222,8 +229,6 @@ func (ct *clientTracer) end(hook string, err error, attrs ...attribute.KeyValue)
return
}

ct.mtx.Lock()
defer ct.mtx.Unlock()
if ctx, ok := ct.activeHooks[hook]; ok {
span := trace.SpanFromContext(ctx)
if err != nil {
Expand Down Expand Up @@ -321,6 +326,9 @@ func (ct *clientTracer) tlsHandshakeDone(_ tls.ConnectionState, err error) {
}

func (ct *clientTracer) wroteHeaderField(k string, v []string) {
if ct.root == nil {
return
}
if ct.useSpans && ct.span("http.headers") == nil {
ct.start("http.headers", "http.headers")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/http/httptrace"
"sync"
"testing"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
Expand All @@ -32,3 +35,39 @@ func ExampleNewClientTrace() {

fmt.Println(resp.Status)
}

// TestNewClientParallelismWithoutSubspans tests running many Gets on a client simultaneously,
// which would trigger a race condition if root were not protected by a mutex.
func TestNewClientParallelismWithoutSubspans(t *testing.T) {
t.Parallel()

makeClientTrace := func(ctx context.Context) *httptrace.ClientTrace {
return NewClientTrace(ctx, WithoutSubSpans())
}

server := httptest.NewServer(nil)

client := http.Client{
Transport: otelhttp.NewTransport(
server.Client().Transport,
otelhttp.WithClientTrace(makeClientTrace),
),
}

var wg sync.WaitGroup

for i := 1; i < 10000; i++ {
wg.Add(1)
go func() {
resp, err := client.Get("https://example.com")
if err != nil {
t.Error(err)
return
}
resp.Body.Close()
wg.Done()
}()
}

wg.Wait()
}

0 comments on commit eb1dc63

Please sign in to comment.