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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changes

- Add support for otlp in execution/grpc. [#3300](https://github.com/evstack/ev-node/pull/3300)
- Optimization of mutex usage in cache for reaper [#3286](https://github.com/evstack/ev-node/pull/3286)
- Add Unix domain socket support for gRPC execution endpoints via `unix:///path/to/socket` [#3297](https://github.com/evstack/ev-node/pull/3297)
- **BREAKING:** (execution/grpc)
- Move execution service where it belongs in execution/grpc. []()
- Move execution service where it belongs in execution/grpc. [#3302](https://github.com/evstack/ev-node/pull/3302)
- Replace legacy gRPC execution `txs` payload fields with `tx_batch` so clients and servers use contiguous transaction buffers [#3297](https://github.com/evstack/ev-node/pull/3297)
- Optimize metadata writes by making it async in cache store [#3298](https://github.com/evstack/ev-node/pull/3298)
- Reduce tx cache retention to avoid OOM under (really) heavy tx load [#3299](https://github.com/evstack/ev-node/pull/3299)
Expand Down
1 change: 1 addition & 0 deletions execution/grpc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func NewClient(url string, opts ...connect.ClientOption) (*Client, error) {
if err != nil {
return nil, err
}
opts = append([]connect.ClientOption{connect.WithInterceptors(outboundPropagationInterceptor())}, opts...)
return &Client{
client: v1connect.NewExecutorServiceClient(
httpClient,
Expand Down
14 changes: 13 additions & 1 deletion execution/grpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,20 @@ require (
connectrpc.com/connect v1.19.2
connectrpc.com/grpcreflect v1.3.0
github.com/evstack/ev-node/core v1.0.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/net v0.53.0
google.golang.org/protobuf v1.36.11
)

require golang.org/x/text v0.36.0 // indirect
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
)
33 changes: 33 additions & 0 deletions execution/grpc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,46 @@ connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo=
connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc=
connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/evstack/ev-node/core v1.0.0 h1:s0Tx0uWHme7SJn/ZNEtee4qNM8UO6PIxXnHhPbbKTz8=
github.com/evstack/ev-node/core v1.0.0/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions execution/grpc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
// - http.Handler: The configured HTTP handler
func NewExecutorServiceHandler(executor execution.Executor, opts ...connect.HandlerOption) http.Handler {
server := NewServer(executor)
opts = append([]connect.HandlerOption{connect.WithInterceptors(inboundPropagationInterceptor())}, opts...)

mux := http.NewServeMux()

Expand Down
29 changes: 29 additions & 0 deletions execution/grpc/otel_propagation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package grpc

import (
"context"

"connectrpc.com/connect"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)

func inboundPropagationInterceptor() connect.UnaryInterceptorFunc {
return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
prop := otel.GetTextMapPropagator()
ctx = prop.Extract(ctx, propagation.HeaderCarrier(req.Header()))
return next(ctx, req)
}
})
}

func outboundPropagationInterceptor() connect.UnaryInterceptorFunc {
return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
prop := otel.GetTextMapPropagator()
prop.Inject(ctx, propagation.HeaderCarrier(req.Header()))
return next(ctx, req)
}
})
}
226 changes: 226 additions & 0 deletions execution/grpc/otel_propagation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package grpc

import (
"context"
"net/http/httptest"
"testing"
"time"

"connectrpc.com/connect"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/baggage"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.opentelemetry.io/otel/trace"

"github.com/evstack/ev-node/core/execution"
)

func setupTracer(t *testing.T) (*tracetest.SpanRecorder, func()) {
t.Helper()
rec := tracetest.NewSpanRecorder()
tp := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(rec))
oldTP := otel.GetTracerProvider()
oldProp := otel.GetTextMapPropagator()
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return rec, func() {
_ = tp.Shutdown(context.Background())
otel.SetTracerProvider(oldTP)
otel.SetTextMapPropagator(oldProp)
}
}

func TestInboundMetadataCreatesChildSpanWithSameTraceID(t *testing.T) {
rec, cleanup := setupTracer(t)
defer cleanup()

tracer := otel.Tracer("test")
parentCtx, parent := tracer.Start(context.Background(), "parent")
defer parent.End()
parentTraceID := parent.SpanContext().TraceID()

mockExec := &mockExecutor{getTxsFunc: func(ctx context.Context) ([][]byte, error) {
_, span := tracer.Start(ctx, "server-child")
span.End()
return [][]byte{}, nil
}}

handler := NewExecutorServiceHandler(mockExec)
ts := httptest.NewServer(handler)
defer ts.Close()

client, err := NewClient(ts.URL)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

_, err = client.GetTxs(parentCtx)
if err != nil {
t.Fatalf("GetTxs failed: %v", err)
}

var found bool
for _, s := range rec.Ended() {
if s.Name() == "server-child" {
found = true
if s.SpanContext().TraceID() != parentTraceID {
t.Fatalf("trace id mismatch: got %s want %s", s.SpanContext().TraceID(), parentTraceID)
}
}
}
if !found {
t.Fatalf("server-child span not found")
}
}

func TestOutboundGRPCCallCarriesTraceparentMetadata(t *testing.T) {
rec, cleanup := setupTracer(t)
_ = rec
defer cleanup()

tracer := otel.Tracer("test")
ctx, parent := tracer.Start(context.Background(), "parent")
defer parent.End()

gotTraceparent := ""
captureHeader := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
gotTraceparent = req.Header().Get("traceparent")
return next(ctx, req)
}
})
Comment on lines +87 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f -i "otel_propagation_test.go" | head -20

Repository: evstack/ev-node

Length of output: 99


🏁 Script executed:

cat -n execution/grpc/otel_propagation_test.go | head -200

Repository: evstack/ev-node

Length of output: 7290


🏁 Script executed:

# Verify no explicit sync primitives protect these variables
rg -A 10 "var (gotTraceparent|gotBaggage|midSpan)" execution/grpc/otel_propagation_test.go

Repository: evstack/ev-node

Length of output: 1021


🏁 Script executed:

# Check if there are any sync primitives used elsewhere in the test file
rg "(sync\.|Mutex|WaitGroup|Channel)" execution/grpc/otel_propagation_test.go

Repository: evstack/ev-node

Length of output: 41


🏁 Script executed:

# Confirm that GetTxs and GetExecutionInfo are blocking calls that wait for completion
grep -A 5 "func.*GetTxs\|func.*GetExecutionInfo" execution/grpc/otel_propagation_test.go | head -20

Repository: evstack/ev-node

Length of output: 41


🏁 Script executed:

# Check if httptest.Server spawns goroutines (standard library behavior verification)
rg "httptest.NewServer" -B 2 -A 2 execution/grpc/otel_propagation_test.go

Repository: evstack/ev-node

Length of output: 865


🏁 Script executed:

# Check how client.GetTxs() is defined to confirm it's a blocking call
fd -t f "client.go" execution/grpc/ | head -5

Repository: evstack/ev-node

Length of output: 84


🏁 Script executed:

# Look for the client implementation
rg "func.*NewClient" -A 10 execution/grpc/ | head -30

Repository: evstack/ev-node

Length of output: 730


🏁 Script executed:

# Verify the blocking behavior by checking the generated client code signature
cat -n execution/grpc/client.go | grep -A 20 "func.*GetTxs"

Repository: evstack/ev-node

Length of output: 1082


🏁 Script executed:

# Confirm the blocking nature - check if there's an error return that would indicate completion
rg "client.GetTxs|client.GetExecutionInfo" execution/grpc/otel_propagation_test.go | grep -v "downstreamClient\|upstreamClient"

Repository: evstack/ev-node

Length of output: 240


Synchronize header capture and span access to avoid test data races.

gotTraceparent, gotBaggage, and midSpan are written in handler/interceptor goroutines and read in the test goroutine without synchronization. Although the blocking RPC calls provide implicit ordering, the Go race detector requires explicit synchronization primitives (mutex or channels) to recognize the happens-before relationship. Additionally, the fixed 10ms sleep at line 189 violates the determinism guideline; use explicit synchronization instead of timing-based waits.

Suggested fix (mutex-guarded capture)
 import (
 	"context"
 	"net/http/httptest"
+	"sync"
 	"testing"
 	"time"
@@
-	gotTraceparent := ""
+	var mu sync.Mutex
+	gotTraceparent := ""
 	captureHeader := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
 		return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
+			mu.Lock()
 			gotTraceparent = req.Header().Get("traceparent")
+			mu.Unlock()
 			return next(ctx, req)
 		}
 	})
@@
-	if gotTraceparent == "" {
+	mu.Lock()
+	traceparent := gotTraceparent
+	mu.Unlock()
+	if traceparent == "" {
 		t.Fatalf("expected traceparent metadata to be propagated")
 	}

Apply the same pattern to gotBaggage (lines 123-131) and midSpan (lines 156-193). For the end-to-end test, use a channel or sync primitive to wait for the handler to populate midSpan instead of the fixed sleep.

Also applies to: 97-103, 123-131, 139-148, 156-193

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@execution/grpc/otel_propagation_test.go` around lines 83 - 89, The test has
data races because gotTraceparent, gotBaggage, and midSpan are written from
interceptor/handler goroutines and read from the test goroutine; replace the
unsynchronized string/bool accesses with a sync primitive (e.g., a single
sync.Mutex protecting gotTraceparent and gotBaggage and a channel or sync.Cond
to signal midSpan readiness) so writes in captureHeader (UnaryInterceptorFunc)
and the handler acquire the mutex before setting values and signal the channel
after setting midSpan, and the test goroutine waits on that channel (or locks
the mutex) instead of sleeping; update all locations referencing gotTraceparent,
gotBaggage, and midSpan to use the chosen synchronization to eliminate race
detector warnings.


mockExec := &mockExecutor{}
handler := NewExecutorServiceHandler(mockExec, connect.WithInterceptors(captureHeader))
ts := httptest.NewServer(handler)
defer ts.Close()

client, err := NewClient(ts.URL)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

if _, err = client.GetTxs(ctx); err != nil {
t.Fatalf("GetTxs failed: %v", err)
}
if gotTraceparent == "" {
t.Fatalf("expected traceparent metadata to be propagated")
}
}

func TestOutboundGRPCCallCarriesPropagationHeaders(t *testing.T) {
rec, cleanup := setupTracer(t)
_ = rec
defer cleanup()

tracer := otel.Tracer("test")
ctx, parent := tracer.Start(context.Background(), "parent")
defer parent.End()
member, err := baggage.NewMember("tenant", "alpha")
if err != nil {
t.Fatalf("failed to create baggage member: %v", err)
}
bg, err := baggage.New(member)
if err != nil {
t.Fatalf("failed to create baggage: %v", err)
}
ctx = baggage.ContextWithBaggage(ctx, bg)

var gotTraceparent string
var gotBaggage string
captureHeader := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
gotTraceparent = req.Header().Get("traceparent")
gotBaggage = req.Header().Get("baggage")
return next(ctx, req)
}
})

mockExec := &mockExecutor{}
handler := NewExecutorServiceHandler(mockExec, connect.WithInterceptors(captureHeader))
ts := httptest.NewServer(handler)
defer ts.Close()

client, err := NewClient(ts.URL)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

if _, err = client.GetTxs(ctx); err != nil {
t.Fatalf("GetTxs failed: %v", err)
}

if gotTraceparent == "" {
t.Fatalf("expected traceparent metadata to be propagated")
}
if gotBaggage == "" {
t.Fatalf("expected baggage metadata to be propagated")
}
}

func TestEndToEndParentChildAcrossServerClientHop(t *testing.T) {
rec, cleanup := setupTracer(t)
defer cleanup()

tracer := otel.Tracer("test")
var midSpan trace.Span

Comment on lines +168 to +169
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n execution/grpc/otel_propagation_test.go | head -200

Repository: evstack/ev-node

Length of output: 7290


🏁 Script executed:

tail -n +195 execution/grpc/otel_propagation_test.go | head -50

Repository: evstack/ev-node

Length of output: 332


Fix concurrent access to midSpan without race condition or timing-dependent waits.

midSpan is written in the handler goroutine (line 170) and read in the test goroutine (line 192) without synchronization. The time.Sleep(10ms) is brittle and timing-dependent, violating the determinism requirement for tests. Use channel-based synchronization to pass the trace ID explicitly instead.

Suggested fix (channel-based synchronization)
-	var midSpan trace.Span
+	midTraceIDCh := make(chan trace.TraceID, 1)
@@
 	upstreamExec := &mockExecutor{getTxsFunc: func(ctx context.Context) ([][]byte, error) {
 		ctx, span := tracer.Start(ctx, "upstream-mid")
-		midSpan = span
+		midTraceIDCh <- span.SpanContext().TraceID()
 		defer span.End()
@@
-	time.Sleep(10 * time.Millisecond)
-
 	rootTraceID := root.SpanContext().TraceID()
-	if midSpan.SpanContext().TraceID() != rootTraceID {
+	var midTraceID trace.TraceID
+	select {
+	case midTraceID = <-midTraceIDCh:
+	case <-time.After(time.Second):
+		t.Fatalf("timeout waiting for upstream-mid span")
+	}
+	if midTraceID != rootTraceID {
 		t.Fatalf("mid span trace id mismatch")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@execution/grpc/otel_propagation_test.go` around lines 156 - 157, The test
currently writes to shared variable midSpan from the handler goroutine and reads
it from the test goroutine with a time.Sleep, causing a race; change this to a
channel-based handoff: create a chan trace.Span or chan trace.SpanContext (or
chan string for traceID) in the test, have the handler (the goroutine that
currently assigns midSpan) send the span (or span.Context()/TraceID) on that
channel, and have the test goroutine receive from the channel before asserting
(replace uses of midSpan with the received value); update references to midSpan
in the handler and assertions to use the received value to eliminate the race
and remove the timing-dependent sleep.

downstreamExec := &mockExecutor{getExecutionInfoFunc: func(ctx context.Context) (executionInfo execution.ExecutionInfo, err error) {
_, span := tracer.Start(ctx, "downstream-child")
span.End()
return execution.ExecutionInfo{MaxGas: 1}, nil
}}
downstreamHandler := NewExecutorServiceHandler(downstreamExec)
downstreamSrv := httptest.NewServer(downstreamHandler)
defer downstreamSrv.Close()
downstreamClient, err := NewClient(downstreamSrv.URL)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

upstreamExec := &mockExecutor{getTxsFunc: func(ctx context.Context) ([][]byte, error) {
ctx, span := tracer.Start(ctx, "upstream-mid")
midSpan = span
defer span.End()
_, err := downstreamClient.GetExecutionInfo(ctx)
if err != nil {
return nil, err
}
return [][]byte{}, nil
}}
upstreamHandler := NewExecutorServiceHandler(upstreamExec)
upstreamSrv := httptest.NewServer(upstreamHandler)
defer upstreamSrv.Close()

client, err := NewClient(upstreamSrv.URL)
if err != nil {
t.Fatalf("NewClient failed: %v", err)
}

rootCtx, root := tracer.Start(context.Background(), "root")
defer root.End()
if _, err := client.GetTxs(rootCtx); err != nil {
t.Fatalf("GetTxs failed: %v", err)
}

time.Sleep(10 * time.Millisecond)

rootTraceID := root.SpanContext().TraceID()
if midSpan.SpanContext().TraceID() != rootTraceID {
t.Fatalf("mid span trace id mismatch")
}
var found bool
for _, s := range rec.Ended() {
if s.Name() == "downstream-child" {
found = true
if s.SpanContext().TraceID() != rootTraceID {
t.Fatalf("downstream trace id mismatch")
}
}
}
if !found {
t.Fatalf("downstream-child span not found")
}
}
Loading