Skip to content

Commit 8844cf7

Browse files
committedMay 14, 2024·
core: Fully delegate picks to DelayedClientTransport
DelayedClientTransport already had to handle all the cases, so ManagedChannelImpl picking was acting only as an optimization. Optimizing DelayedClientTransport to avoid the lock when not queuing makes ManagedChannelImpl picking entirely redundant, and allows us to remove the duplicate race-handling logic. This avoids double-picking when queuing, where ManagedChannelImpl does a pick, decides to queue, and then DelayedClientTransport re-performs the pick because it doesn't know which pick version was used. This was noticed with RLS, which mutates state within the picker.
1 parent d9e09c2 commit 8844cf7

File tree

2 files changed

+58
-88
lines changed

2 files changed

+58
-88
lines changed
 

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

+54-45
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,8 @@ final class DelayedClientTransport implements ManagedClientTransport {
6969
@GuardedBy("lock")
7070
private Collection<PendingStream> pendingStreams = new LinkedHashSet<>();
7171

72-
/**
73-
* When {@code shutdownStatus != null && !hasPendingStreams()}, then the transport is considered
74-
* terminated.
75-
*/
76-
@GuardedBy("lock")
77-
private Status shutdownStatus;
78-
79-
/**
80-
* The last picker that {@link #reprocess} has used. May be set to null when the channel has moved
81-
* to idle.
82-
*/
83-
@GuardedBy("lock")
84-
@Nullable
85-
private SubchannelPicker lastPicker;
86-
87-
@GuardedBy("lock")
88-
private long lastPickerVersion;
72+
/** Immutable state needed for picking. 'lock' must be held for writing. */
73+
private volatile PickerState pickerState = new PickerState(null, null);
8974

9075
/**
9176
* Creates a new delayed transport.
@@ -139,33 +124,30 @@ public final ClientStream newStream(
139124
try {
140125
PickSubchannelArgs args = new PickSubchannelArgsImpl(
141126
method, headers, callOptions, new PickDetailsConsumerImpl(tracers));
142-
SubchannelPicker picker = null;
143-
long pickerVersion = -1;
127+
PickerState state = pickerState;
144128
while (true) {
145-
synchronized (lock) {
146-
if (shutdownStatus != null) {
147-
return new FailingClientStream(shutdownStatus, tracers);
148-
}
149-
if (lastPicker == null) {
150-
return createPendingStream(args, tracers);
151-
}
152-
// Check for second time through the loop, and whether anything changed
153-
if (picker != null && pickerVersion == lastPickerVersion) {
154-
return createPendingStream(args, tracers);
155-
}
156-
picker = lastPicker;
157-
pickerVersion = lastPickerVersion;
129+
if (state.shutdownStatus != null) {
130+
return new FailingClientStream(state.shutdownStatus, tracers);
158131
}
159-
PickResult pickResult = picker.pickSubchannel(args);
160-
ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
161-
callOptions.isWaitForReady());
162-
if (transport != null) {
163-
return transport.newStream(
164-
args.getMethodDescriptor(), args.getHeaders(), args.getCallOptions(),
165-
tracers);
132+
if (state.lastPicker != null) {
133+
PickResult pickResult = state.lastPicker.pickSubchannel(args);
134+
ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
135+
callOptions.isWaitForReady());
136+
if (transport != null) {
137+
return transport.newStream(
138+
args.getMethodDescriptor(), args.getHeaders(), args.getCallOptions(),
139+
tracers);
140+
}
166141
}
167142
// This picker's conclusion is "buffer". If there hasn't been a newer picker set (possible
168143
// race with reprocess()), we will buffer it. Otherwise, will try with the new picker.
144+
synchronized (lock) {
145+
PickerState newerState = pickerState;
146+
if (state == newerState) {
147+
return createPendingStream(args, tracers);
148+
}
149+
state = newerState;
150+
}
169151
}
170152
} finally {
171153
syncContext.drain();
@@ -210,10 +192,10 @@ public ListenableFuture<SocketStats> getStats() {
210192
@Override
211193
public final void shutdown(final Status status) {
212194
synchronized (lock) {
213-
if (shutdownStatus != null) {
195+
if (pickerState.shutdownStatus != null) {
214196
return;
215197
}
216-
shutdownStatus = status;
198+
pickerState = pickerState.withShutdownStatus(status);
217199
syncContext.executeLater(new Runnable() {
218200
@Override
219201
public void run() {
@@ -288,8 +270,7 @@ final int getPendingStreamsCount() {
288270
final void reprocess(@Nullable SubchannelPicker picker) {
289271
ArrayList<PendingStream> toProcess;
290272
synchronized (lock) {
291-
lastPicker = picker;
292-
lastPickerVersion++;
273+
pickerState = pickerState.withPicker(picker);
293274
if (picker == null || !hasPendingStreams()) {
294275
return;
295276
}
@@ -338,7 +319,7 @@ final void reprocess(@Nullable SubchannelPicker picker) {
338319
// (which would shutdown the transports and LoadBalancer) because the gap should be shorter
339320
// than IDLE_MODE_DEFAULT_TIMEOUT_MILLIS (1 second).
340321
syncContext.executeLater(reportTransportNotInUse);
341-
if (shutdownStatus != null && reportTransportTerminated != null) {
322+
if (pickerState.shutdownStatus != null && reportTransportTerminated != null) {
342323
syncContext.executeLater(reportTransportTerminated);
343324
reportTransportTerminated = null;
344325
}
@@ -384,7 +365,7 @@ public void cancel(Status reason) {
384365
boolean justRemovedAnElement = pendingStreams.remove(this);
385366
if (!hasPendingStreams() && justRemovedAnElement) {
386367
syncContext.executeLater(reportTransportNotInUse);
387-
if (shutdownStatus != null) {
368+
if (pickerState.shutdownStatus != null) {
388369
syncContext.executeLater(reportTransportTerminated);
389370
reportTransportTerminated = null;
390371
}
@@ -409,4 +390,32 @@ public void appendTimeoutInsight(InsightBuilder insight) {
409390
super.appendTimeoutInsight(insight);
410391
}
411392
}
393+
394+
static final class PickerState {
395+
/**
396+
* The last picker that {@link #reprocess} has used. May be set to null when the channel has
397+
* moved to idle.
398+
*/
399+
@Nullable
400+
final SubchannelPicker lastPicker;
401+
/**
402+
* When {@code shutdownStatus != null && !hasPendingStreams()}, then the transport is considered
403+
* terminated.
404+
*/
405+
@Nullable
406+
final Status shutdownStatus;
407+
408+
private PickerState(SubchannelPicker lastPicker, Status shutdownStatus) {
409+
this.lastPicker = lastPicker;
410+
this.shutdownStatus = shutdownStatus;
411+
}
412+
413+
public PickerState withPicker(SubchannelPicker newPicker) {
414+
return new PickerState(newPicker, this.shutdownStatus);
415+
}
416+
417+
public PickerState withShutdownStatus(Status newShutdownStatus) {
418+
return new PickerState(this.lastPicker, newShutdownStatus);
419+
}
420+
}
412421
}

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

+4-43
Original file line numberDiff line numberDiff line change
@@ -471,57 +471,20 @@ private void refreshNameResolution() {
471471
private final class ChannelStreamProvider implements ClientStreamProvider {
472472
volatile Throttle throttle;
473473

474-
private ClientTransport getTransport(PickSubchannelArgs args) {
475-
SubchannelPicker pickerCopy = subchannelPicker;
476-
if (shutdown.get()) {
477-
// If channel is shut down, delayedTransport is also shut down which will fail the stream
478-
// properly.
479-
return delayedTransport;
480-
}
481-
if (pickerCopy == null) {
482-
final class ExitIdleModeForTransport implements Runnable {
483-
@Override
484-
public void run() {
485-
exitIdleMode();
486-
}
487-
}
488-
489-
syncContext.execute(new ExitIdleModeForTransport());
490-
return delayedTransport;
491-
}
492-
// There is no need to reschedule the idle timer here.
493-
//
494-
// pickerCopy != null, which means idle timer has not expired when this method starts.
495-
// Even if idle timer expires right after we grab pickerCopy, and it shuts down LoadBalancer
496-
// which calls Subchannel.shutdown(), the InternalSubchannel will be actually shutdown after
497-
// SUBCHANNEL_SHUTDOWN_DELAY_SECONDS, which gives the caller time to start RPC on it.
498-
//
499-
// In most cases the idle timer is scheduled to fire after the transport has created the
500-
// stream, which would have reported in-use state to the channel that would have cancelled
501-
// the idle timer.
502-
PickResult pickResult = pickerCopy.pickSubchannel(args);
503-
ClientTransport transport = GrpcUtil.getTransportFromPickResult(
504-
pickResult, args.getCallOptions().isWaitForReady());
505-
if (transport != null) {
506-
return transport;
507-
}
508-
return delayedTransport;
509-
}
510-
511474
@Override
512475
public ClientStream newStream(
513476
final MethodDescriptor<?, ?> method,
514477
final CallOptions callOptions,
515478
final Metadata headers,
516479
final Context context) {
480+
// There is no need to reschedule the idle timer here. If the channel isn't shut down, either
481+
// the delayed transport or a real transport will go in-use and cancel the idle timer.
517482
if (!retryEnabled) {
518483
ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers(
519484
callOptions, headers, 0, /* isTransparentRetry= */ false);
520-
ClientTransport transport = getTransport(new PickSubchannelArgsImpl(
521-
method, headers, callOptions, new PickDetailsConsumerImpl(tracers)));
522485
Context origContext = context.attach();
523486
try {
524-
return transport.newStream(method, headers, callOptions, tracers);
487+
return delayedTransport.newStream(method, headers, callOptions, tracers);
525488
} finally {
526489
context.detach(origContext);
527490
}
@@ -562,11 +525,9 @@ ClientStream newSubstream(
562525
CallOptions newOptions = callOptions.withStreamTracerFactory(factory);
563526
ClientStreamTracer[] tracers = GrpcUtil.getClientStreamTracers(
564527
newOptions, newHeaders, previousAttempts, isTransparentRetry);
565-
ClientTransport transport = getTransport(new PickSubchannelArgsImpl(
566-
method, newHeaders, newOptions, new PickDetailsConsumerImpl(tracers)));
567528
Context origContext = context.attach();
568529
try {
569-
return transport.newStream(method, newHeaders, newOptions, tracers);
530+
return delayedTransport.newStream(method, newHeaders, newOptions, tracers);
570531
} finally {
571532
context.detach(origContext);
572533
}

0 commit comments

Comments
 (0)
Please sign in to comment.