Skip to content

Commit ac4d252

Browse files
authored
core: Implement [A121](grpc/proposal#556) (#12893)
1 parent 96e4d30 commit ac4d252

21 files changed

Lines changed: 2093 additions & 362 deletions

‎api/src/main/java/io/grpc/ClientStreamTracer.java‎

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,42 +58,45 @@ public void createPendingStream() {
5858
}
5959

6060
/**
61-
* Called when an attempt-level delay segment (such as waiting for a load balancing pick or
62-
* connection establishment) starts.
61+
* Called when an attempt-level delay (such as waiting for a load balancing pick or connection
62+
* establishment) starts, or when the {@code delayType} of the ongoing delay changes, in which
63+
* case {@link #recordDelayEnd} is called for the previous delay first.
6364
*
64-
* <p>This method is invoked synchronously on the attempt thread. Implementations should start
65-
* internal timers or child tracing spans (named strictly {@code "Attempt Delay"}) carrying the
66-
* canonical {@code grpc.delay_type} attribute.
65+
* <p>Implementations should start a timer and open a child tracing span (named strictly
66+
* {@code "Delay"}) carrying the canonical {@code grpc.delay_type} attribute.
6767
*
6868
* @param delayType canonical low-cardinality label categorizing the delay (e.g., "connecting")
6969
* @param delayReason high-cardinality diagnostic string describing granular runtime conditions
70-
* @since 1.82.0
70+
* @since 1.86.0
7171
*/
72-
public void recordAttemptDelayStart(String delayType, String delayReason) {
72+
public void recordDelayStart(String delayType, String delayReason) {
7373
}
7474

7575
/**
76-
* Called when an attempt-level delay reason changes while the overall delay type remains
77-
* constant (for example, when a priority load balancing policy fails over between tiers).
76+
* Called when an attempt-level delay reason changes while the delay type remains constant (for
77+
* example, when a load balancing policy updates its connection status detail).
7878
*
79-
* <p>Implementations should record structured events (such as {@code "Delay state transition"})
80-
* on the active delay span without recreating the span or resetting cumulative timers.
79+
* <p>Implementations should record a structured event (such as {@code "Delay triggered"}) on
80+
* the active delay span without recreating the span or resetting cumulative timers.
8181
*
82+
* @param delayType canonical low-cardinality label of the ongoing delay
8283
* @param delayReason updated high-cardinality diagnostic string describing new conditions
83-
* @since 1.82.0
84+
* @since 1.86.0
8485
*/
85-
public void recordAttemptDelayReasonChanged(String delayReason) {
86+
public void recordDelayReasonChanged(String delayType, String delayReason) {
8687
}
8788

8889
/**
89-
* Called when an attempt-level delay segment ends upon successful pick or stream creation.
90+
* Called when an attempt-level delay ends upon successful pick or stream creation, or when the
91+
* attempt is cancelled or reaches its deadline while still waiting.
9092
*
91-
* <p>Implementations should simultaneously close active child tracing spans and record elapsed
92-
* duration to the {@code grpc.client.attempt.delay.duration} histogram.
93+
* <p>Implementations should close the active child tracing span and record the elapsed duration
94+
* to the {@code grpc.client.attempt.delay.duration} histogram labeled with {@code delayType}.
9395
*
94-
* @since 1.82.0
96+
* @param delayType canonical low-cardinality label of the delay being ended
97+
* @since 1.86.0
9598
*/
96-
public void recordAttemptDelayEnd() {
99+
public void recordDelayEnd(String delayType) {
97100
}
98101

99102
/**
@@ -156,6 +159,47 @@ public abstract static class Factory {
156159
public ClientStreamTracer newClientStreamTracer(StreamInfo info, Metadata headers) {
157160
throw new UnsupportedOperationException("Not implemented");
158161
}
162+
163+
/**
164+
* Called when a call-level delay (such as waiting for name resolution) starts before any
165+
* individual RPC attempt is created, or when the {@code delayType} of the ongoing delay
166+
* changes, in which case {@link #recordDelayEnd} is called for the previous delay first.
167+
*
168+
* <p>Implementations should start a timer and open a child tracing span (named strictly
169+
* {@code "Delay"}) carrying the canonical {@code grpc.delay_type} attribute.
170+
*
171+
* @param delayType canonical low-cardinality label categorizing the delay (e.g., "resolving")
172+
* @param delayReason high-cardinality diagnostic string describing granular runtime conditions
173+
* @since 1.86.0
174+
*/
175+
public void recordDelayStart(String delayType, String delayReason) {
176+
}
177+
178+
/**
179+
* Called when a call-level delay reason changes while the delay type remains constant.
180+
*
181+
* <p>Implementations should record a structured event (such as {@code "Delay triggered"}) on
182+
* the active call delay span without recreating the span or resetting timers.
183+
*
184+
* @param delayType canonical low-cardinality label of the ongoing delay
185+
* @param delayReason updated high-cardinality diagnostic string describing new conditions
186+
* @since 1.86.0
187+
*/
188+
public void recordDelayReasonChanged(String delayType, String delayReason) {
189+
}
190+
191+
/**
192+
* Called when a call-level delay ends upon successful name resolution, or when the RPC is
193+
* cancelled or reaches its deadline before resolution completes.
194+
*
195+
* <p>Implementations should close the active call delay span and record the elapsed duration
196+
* to the {@code grpc.client.call.delay.duration} histogram labeled with {@code delayType}.
197+
*
198+
* @param delayType canonical low-cardinality label of the delay being ended
199+
* @since 1.86.0
200+
*/
201+
public void recordDelayEnd(String delayType) {
202+
}
159203
}
160204

161205
/**

‎api/src/main/java/io/grpc/LoadBalancer.java‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -739,21 +739,29 @@ public static PickResult withNoResult() {
739739
*
740740
* @param delayType low-cardinality root cause label (e.g., "connecting")
741741
* @param delayReason high-cardinality diagnostic string for trace events
742-
* @since 1.82.0
742+
* @since 1.86.0
743743
*/
744744
public static PickResult withNoResult(String delayType, String delayReason) {
745745
Preconditions.checkNotNull(delayType, "delayType");
746746
Preconditions.checkNotNull(delayReason, "delayReason");
747747
return new PickResult(null, null, Status.OK, false, null, delayType, delayReason);
748748
}
749749

750-
/** Returns the delay type label if any. */
750+
/**
751+
* Returns the delay type label if any.
752+
*
753+
* @since 1.86.0
754+
*/
751755
@Nullable
752756
public String getDelayType() {
753757
return delayType;
754758
}
755759

756-
/** Returns the diagnostic delay reason if any. */
760+
/**
761+
* Returns the diagnostic delay reason if any.
762+
*
763+
* @since 1.86.0
764+
*/
757765
@Nullable
758766
public String getDelayReason() {
759767
return delayReason;

‎api/src/test/java/io/grpc/ClientStreamTracerTest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,17 @@ public void streamInfo_toBuilder() {
5757
StreamInfo info2 = info1.toBuilder().build();
5858
assertThat(info2.getCallOptions()).isSameInstanceAs(callOptions);
5959
}
60+
61+
@Test
62+
public void defaultDelayMethodsNoOp() {
63+
ClientStreamTracer tracer = new ClientStreamTracer() {};
64+
tracer.recordDelayStart("connecting", "test");
65+
tracer.recordDelayReasonChanged("connecting", "test2");
66+
tracer.recordDelayEnd("connecting");
67+
68+
ClientStreamTracer.Factory factory = new ClientStreamTracer.Factory() {};
69+
factory.recordDelayStart("resolving", "test");
70+
factory.recordDelayReasonChanged("resolving", "test2");
71+
factory.recordDelayEnd("resolving");
72+
}
6073
}

‎api/src/test/java/io/grpc/LoadBalancerTest.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,19 @@ public void pickResult_withNoResult() {
9191
assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
9292
assertThat(result.getStreamTracerFactory()).isNull();
9393
assertThat(result.isDrop()).isFalse();
94+
assertThat(result.getDelayType()).isNull();
95+
assertThat(result.getDelayReason()).isNull();
96+
}
97+
98+
@Test
99+
public void pickResult_withNoResult_withDelay() {
100+
PickResult result = PickResult.withNoResult("connecting", "trying backends");
101+
assertThat(result.getSubchannel()).isNull();
102+
assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
103+
assertThat(result.getStreamTracerFactory()).isNull();
104+
assertThat(result.isDrop()).isFalse();
105+
assertThat(result.getDelayType()).isEqualTo("connecting");
106+
assertThat(result.getDelayReason()).isEqualTo("trying backends");
94107
}
95108

96109
@Test
@@ -118,6 +131,10 @@ public void pickResult_equals() {
118131
PickResult sc3 = PickResult.withSubchannel(subchannel, tracerFactory);
119132
PickResult sc4 = PickResult.withSubchannel(subchannel2);
120133
PickResult nr = PickResult.withNoResult();
134+
PickResult nrDelay1 = PickResult.withNoResult("connecting", "trying 10.0.0.1");
135+
PickResult nrDelay2 = PickResult.withNoResult("connecting", "trying 10.0.0.1");
136+
PickResult nrDelayDiffReason = PickResult.withNoResult("connecting", "trying 10.0.0.2");
137+
PickResult nrDelayDiffType = PickResult.withNoResult("rls_lookup_pending", "trying 10.0.0.1");
121138
PickResult error1 = PickResult.withError(status);
122139
PickResult error2 = PickResult.withError(status2);
123140
PickResult error3 = PickResult.withError(status2);
@@ -132,6 +149,12 @@ public void pickResult_equals() {
132149
assertThat(sc1).isNotEqualTo(sc3);
133150
assertThat(sc1).isNotEqualTo(sc4);
134151

152+
assertThat(nr).isEqualTo(nrDelay1);
153+
assertThat(nrDelay1).isEqualTo(nrDelay2);
154+
assertThat(nrDelay1.hashCode()).isEqualTo(nrDelay2.hashCode());
155+
assertThat(nrDelay1).isEqualTo(nrDelayDiffReason);
156+
assertThat(nrDelay1).isEqualTo(nrDelayDiffType);
157+
135158
assertThat(error1).isNotEqualTo(error2);
136159
assertThat(error2).isEqualTo(error3);
137160

‎core/src/main/java/io/grpc/internal/DelayedClientTransport.java‎

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.util.Collection;
3838
import java.util.Collections;
3939
import java.util.LinkedHashSet;
40-
import java.util.Objects;
4140
import java.util.concurrent.Executor;
4241
import javax.annotation.Nonnull;
4342
import javax.annotation.Nullable;
@@ -176,7 +175,7 @@ public final ClientStream newStream(
176175
*/
177176
@GuardedBy("lock")
178177
private PendingStream createPendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers,
179-
PickResult pickResult, @Nullable String delayType, @Nullable String delayReason) {
178+
PickResult pickResult, String delayType, String delayReason) {
180179
PendingStream pendingStream = new PendingStream(args, tracers, delayType, delayReason);
181180
if (args.getCallOptions().isWaitForReady() && pickResult != null && pickResult.hasResult()) {
182181
pendingStream.lastPickStatus = pickResult.getStatus();
@@ -388,7 +387,10 @@ private static String determineQueuingDelayReason(@Nullable PickResult pickResul
388387
return "subchannel returned by LB picker has no connected subchannel";
389388
}
390389
if (!pickResult.getStatus().isOk()) {
391-
return "wait_for_ready RPC failed with status: " + pickResult.getStatus();
390+
Status status = pickResult.getStatus();
391+
// Status.toString() would append the cause's stack trace.
392+
return "wait_for_ready RPC failed with status: " + status.getCode()
393+
+ (status.getDescription() == null ? "" : ": " + status.getDescription());
392394
}
393395
if (pickResult.getDelayReason() != null) {
394396
return pickResult.getDelayReason();
@@ -407,16 +409,14 @@ private class PendingStream extends DelayedStream {
407409
@Nullable private String activeDelayReason;
408410

409411
private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers,
410-
@Nullable String initialType, @Nullable String initialReason) {
412+
String delayType, String delayReason) {
411413
super("connecting_and_lb");
412414
this.args = args;
413415
this.tracers = tracers;
414-
this.activeDelayType = initialType;
415-
this.activeDelayReason = initialReason;
416-
if (initialType != null) {
417-
for (ClientStreamTracer tracer : tracers) {
418-
tracer.recordAttemptDelayStart(initialType, initialReason != null ? initialReason : "");
419-
}
416+
this.activeDelayType = delayType;
417+
this.activeDelayReason = delayReason;
418+
for (ClientStreamTracer tracer : tracers) {
419+
tracer.recordDelayStart(delayType, delayReason);
420420
}
421421
}
422422

@@ -427,30 +427,23 @@ private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers,
427427
* spans are ended and a new segment is initiated. If only {@code newReason} changes, a
428428
* structured transition event is appended to the active span without span re-creation.
429429
*/
430-
synchronized void updateDelay(@Nullable String newType, @Nullable String newReason) {
430+
synchronized void updateDelay(String newType, String newReason) {
431431
if (getRealStream() != null) {
432432
return;
433433
}
434-
if (!Objects.equals(activeDelayType, newType)) {
434+
if (!newType.equals(activeDelayType)) {
435435
// Delay type changed (e.g., from RLS lookup to connecting). End the previous delay.
436-
if (activeDelayType != null) {
437-
for (ClientStreamTracer tracer : tracers) {
438-
tracer.recordAttemptDelayEnd();
439-
}
440-
}
436+
endDelay();
441437
activeDelayType = newType;
442-
activeDelayReason = null;
443-
if (newType != null) {
444-
for (ClientStreamTracer tracer : tracers) {
445-
tracer.recordAttemptDelayStart(newType, newReason != null ? newReason : "");
446-
}
438+
activeDelayReason = newReason;
439+
for (ClientStreamTracer tracer : tracers) {
440+
tracer.recordDelayStart(newType, newReason);
447441
}
448-
}
449-
if (newType != null && newReason != null && !Objects.equals(activeDelayReason, newReason)) {
442+
} else if (!newReason.equals(activeDelayReason)) {
450443
// Delay type is unchanged, but the reason changed (e.g., priority failover).
451444
activeDelayReason = newReason;
452445
for (ClientStreamTracer tracer : tracers) {
453-
tracer.recordAttemptDelayReasonChanged(newReason);
446+
tracer.recordDelayReasonChanged(newType, newReason);
454447
}
455448
}
456449
}
@@ -459,12 +452,13 @@ synchronized void updateDelay(@Nullable String newType, @Nullable String newReas
459452
* Ends active attempt delay segment telemetry upon stream creation or stream cancellation.
460453
*/
461454
synchronized void endDelay() {
462-
if (activeDelayType != null) {
463-
for (ClientStreamTracer tracer : tracers) {
464-
tracer.recordAttemptDelayEnd();
465-
}
455+
String delayType = activeDelayType;
456+
if (delayType != null) {
466457
activeDelayType = null;
467458
activeDelayReason = null;
459+
for (ClientStreamTracer tracer : tracers) {
460+
tracer.recordDelayEnd(delayType);
461+
}
468462
}
469463
}
470464

‎core/src/main/java/io/grpc/internal/ForwardingClientStreamTracer.java‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,18 @@ public void createPendingStream() {
4040
}
4141

4242
@Override
43-
public void recordAttemptDelayStart(String delayType, String delayReason) {
44-
delegate().recordAttemptDelayStart(delayType, delayReason);
43+
public void recordDelayStart(String delayType, String delayReason) {
44+
delegate().recordDelayStart(delayType, delayReason);
4545
}
4646

4747
@Override
48-
public void recordAttemptDelayReasonChanged(String delayReason) {
49-
delegate().recordAttemptDelayReasonChanged(delayReason);
48+
public void recordDelayReasonChanged(String delayType, String delayReason) {
49+
delegate().recordDelayReasonChanged(delayType, delayReason);
5050
}
5151

5252
@Override
53-
public void recordAttemptDelayEnd() {
54-
delegate().recordAttemptDelayEnd();
53+
public void recordDelayEnd(String delayType) {
54+
delegate().recordDelayEnd(delayType);
5555
}
5656

5757
@Override

0 commit comments

Comments
 (0)