Skip to content

Commit 60a5a48

Browse files
add test
1 parent d25ecfe commit 60a5a48

File tree

1 file changed

+229
-0
lines changed

1 file changed

+229
-0
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright 2024 gRPC authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package opentelemetry_test
18+
19+
import (
20+
"context"
21+
"io"
22+
"slices"
23+
"strconv"
24+
"testing"
25+
"time"
26+
27+
"go.opentelemetry.io/otel/attribute"
28+
"go.opentelemetry.io/otel/sdk/trace/tracetest"
29+
oteltrace "go.opentelemetry.io/otel/trace"
30+
"google.golang.org/grpc"
31+
"google.golang.org/grpc/codes"
32+
"google.golang.org/grpc/credentials/insecure"
33+
"google.golang.org/grpc/internal/stubserver"
34+
"google.golang.org/grpc/metadata"
35+
"google.golang.org/grpc/resolver"
36+
"google.golang.org/grpc/resolver/manual"
37+
"google.golang.org/grpc/stats/opentelemetry"
38+
"google.golang.org/grpc/status"
39+
40+
testgrpc "google.golang.org/grpc/interop/grpc_testing"
41+
testpb "google.golang.org/grpc/interop/grpc_testing"
42+
)
43+
44+
// TestRetryCounter_IncrementsPerAttempt verifies that the previous-rpc-attempts
45+
// counter increments correctly across multiple retry attempts. This test ensures
46+
// that the counter is tracked per-call (not per-attempt) as required by issue #8700.
47+
func (s) TestRetryCounter_IncrementsPerAttempt(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
setupStub func() *stubserver.StubServer
51+
doCall func(context.Context, testgrpc.TestServiceClient) error
52+
spanName string
53+
}{
54+
{
55+
name: "unary",
56+
setupStub: func() *stubserver.StubServer {
57+
return &stubserver.StubServer{
58+
UnaryCallF: func(ctx context.Context, _ *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
59+
md, _ := metadata.FromIncomingContext(ctx)
60+
headerAttempts := 0
61+
if h := md["grpc-previous-rpc-attempts"]; len(h) > 0 {
62+
headerAttempts, _ = strconv.Atoi(h[0])
63+
}
64+
// Fail first 2 attempts, succeed on 3rd
65+
if headerAttempts < 2 {
66+
return nil, status.Errorf(codes.Unavailable, "retry (%d)", headerAttempts)
67+
}
68+
return &testpb.SimpleResponse{}, nil
69+
},
70+
}
71+
},
72+
doCall: func(ctx context.Context, client testgrpc.TestServiceClient) error {
73+
_, err := client.UnaryCall(ctx, &testpb.SimpleRequest{})
74+
return err
75+
},
76+
spanName: "Attempt.grpc.testing.TestService.UnaryCall",
77+
},
78+
{
79+
name: "streaming",
80+
setupStub: func() *stubserver.StubServer {
81+
return &stubserver.StubServer{
82+
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
83+
md, _ := metadata.FromIncomingContext(stream.Context())
84+
headerAttempts := 0
85+
if h := md["grpc-previous-rpc-attempts"]; len(h) > 0 {
86+
headerAttempts, _ = strconv.Atoi(h[0])
87+
}
88+
// Fail first 2 attempts, succeed on 3rd
89+
if headerAttempts < 2 {
90+
return status.Errorf(codes.Unavailable, "retry (%d)", headerAttempts)
91+
}
92+
for {
93+
_, err := stream.Recv()
94+
if err == io.EOF {
95+
return nil
96+
}
97+
if err != nil {
98+
return err
99+
}
100+
}
101+
},
102+
}
103+
},
104+
doCall: func(ctx context.Context, client testgrpc.TestServiceClient) error {
105+
stream, err := client.FullDuplexCall(ctx)
106+
if err != nil {
107+
return err
108+
}
109+
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
110+
return err
111+
}
112+
if err := stream.CloseSend(); err != nil {
113+
return err
114+
}
115+
_, err = stream.Recv()
116+
if err != nil && err != io.EOF {
117+
return err
118+
}
119+
return nil
120+
},
121+
spanName: "Attempt.grpc.testing.TestService.FullDuplexCall",
122+
},
123+
}
124+
125+
for _, tt := range tests {
126+
t.Run(tt.name, func(t *testing.T) {
127+
mo, _ := defaultMetricsOptions(t, nil)
128+
to, exporter := defaultTraceOptions(t)
129+
rb := manual.NewBuilderWithScheme("retry-test")
130+
ss := tt.setupStub()
131+
opts := opentelemetry.Options{MetricsOptions: *mo, TraceOptions: *to}
132+
if err := ss.Start([]grpc.ServerOption{opentelemetry.ServerOption(opts)}); err != nil {
133+
t.Fatal(err)
134+
}
135+
defer ss.Stop()
136+
137+
retryPolicy := `{
138+
"methodConfig": [{
139+
"name": [{"service": "grpc.testing.TestService"}],
140+
"retryPolicy": {
141+
"maxAttempts": 3,
142+
"initialBackoff": "0.05s",
143+
"maxBackoff": "0.2s",
144+
"backoffMultiplier": 1.0,
145+
"retryableStatusCodes": ["UNAVAILABLE"]
146+
}
147+
}]
148+
}`
149+
cc, err := grpc.NewClient(
150+
rb.Scheme()+":///test.server",
151+
grpc.WithTransportCredentials(insecure.NewCredentials()),
152+
grpc.WithResolvers(rb),
153+
opentelemetry.DialOption(opts),
154+
grpc.WithDefaultServiceConfig(retryPolicy),
155+
)
156+
if err != nil {
157+
t.Fatal(err)
158+
}
159+
defer cc.Close()
160+
161+
// Update resolver state
162+
rb.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: ss.Address}}})
163+
164+
client := testgrpc.NewTestServiceClient(cc)
165+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
166+
defer cancel()
167+
168+
if err := tt.doCall(ctx, client); err != nil {
169+
t.Fatalf("%s call failed: %v", tt.name, err)
170+
}
171+
172+
// Wait for spans to be exported
173+
// We expect at least 3 attempt spans
174+
var attemptSpans []tracetest.SpanStub
175+
for ; ctx.Err() == nil; <-time.After(time.Millisecond) {
176+
attemptSpans = nil
177+
allSpans := exporter.GetSpans()
178+
for _, span := range allSpans {
179+
if span.Name == tt.spanName && span.SpanKind == oteltrace.SpanKindInternal {
180+
attemptSpans = append(attemptSpans, span)
181+
}
182+
}
183+
if len(attemptSpans) >= 3 {
184+
break
185+
}
186+
}
187+
if ctx.Err() != nil {
188+
t.Fatalf("Timed out waiting for attempt spans: %v", ctx.Err())
189+
}
190+
191+
if got, want := len(attemptSpans), 3; got != want {
192+
t.Fatalf("Got %d attempt spans, want %d", got, want)
193+
}
194+
195+
// Sort attempt spans by start time to get them in chronological order
196+
slices.SortFunc(attemptSpans, func(a, b tracetest.SpanStub) int {
197+
if a.StartTime.Before(b.StartTime) {
198+
return -1
199+
}
200+
return 1
201+
})
202+
203+
// Verify each attempt has the correct previous-rpc-attempts value
204+
// Attempt 1 should have 0, Attempt 2 should have 1, Attempt 3 should have 2
205+
for i, span := range attemptSpans {
206+
var previousAttemptsAttr *attribute.KeyValue
207+
for _, attr := range span.Attributes {
208+
if attr.Key == "previous-rpc-attempts" {
209+
previousAttemptsAttr = &attr
210+
break
211+
}
212+
}
213+
214+
if previousAttemptsAttr == nil {
215+
t.Errorf("Attempt %d (span %q) missing previous-rpc-attempts attribute", i+1, span.Name)
216+
continue
217+
}
218+
219+
got := previousAttemptsAttr.Value.AsInt64()
220+
want := int64(i)
221+
if got != want {
222+
t.Errorf("Attempt %d (span %q): previous-rpc-attempts = %d, want %d", i+1, span.Name, got, want)
223+
} else {
224+
t.Logf("✓ Attempt %d correctly has previous-rpc-attempts = %d", i+1, got)
225+
}
226+
}
227+
})
228+
}
229+
}

0 commit comments

Comments
 (0)