• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20494

30 Sep 2026 08:29AM UTC coverage: 89.351% (+0.05%) from 89.306%
#20494

push

github

web-flow
core: Implement [A121](https://github.com/grpc/proposal/pull/556) (#12893)

39251 of 43929 relevant lines covered (89.35%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.82
/../core/src/main/java/io/grpc/internal/DelayedClientTransport.java
1
/*
2
 * Copyright 2015 The 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 io.grpc.internal;
18

19
import com.google.common.annotations.VisibleForTesting;
20
import com.google.common.util.concurrent.ListenableFuture;
21
import com.google.common.util.concurrent.SettableFuture;
22
import com.google.errorprone.annotations.concurrent.GuardedBy;
23
import io.grpc.CallOptions;
24
import io.grpc.ClientStreamTracer;
25
import io.grpc.Context;
26
import io.grpc.InternalChannelz.SocketStats;
27
import io.grpc.InternalLogId;
28
import io.grpc.LoadBalancer.PickResult;
29
import io.grpc.LoadBalancer.PickSubchannelArgs;
30
import io.grpc.LoadBalancer.SubchannelPicker;
31
import io.grpc.Metadata;
32
import io.grpc.MethodDescriptor;
33
import io.grpc.Status;
34
import io.grpc.SynchronizationContext;
35
import io.grpc.internal.ClientStreamListener.RpcProgress;
36
import java.util.ArrayList;
37
import java.util.Collection;
38
import java.util.Collections;
39
import java.util.LinkedHashSet;
40
import java.util.concurrent.Executor;
41
import javax.annotation.Nonnull;
42
import javax.annotation.Nullable;
43

44
/**
45
 * A client transport that queues requests before a real transport is available. When {@link
46
 * #reprocess} is called, this class applies the provided {@link SubchannelPicker} to pick a
47
 * transport for each pending stream.
48
 *
49
 * <p>This transport owns every stream that it has created until a real transport has been picked
50
 * for that stream, at which point the ownership of the stream is transferred to the real transport,
51
 * thus the delayed transport stops owning the stream.
52
 */
53
final class DelayedClientTransport implements ManagedClientTransport {
54
  // lazily allocated, since it is infrequently used.
55
  private final InternalLogId logId =
1 ✔
56
      InternalLogId.allocate(DelayedClientTransport.class, /*details=*/ null);
1 ✔
57

58
  private final Object lock = new Object();
1 ✔
59

60
  private final Executor defaultAppExecutor;
61
  private final SynchronizationContext syncContext;
62

63
  private Runnable reportTransportInUse;
64
  private Runnable reportTransportNotInUse;
65
  private Runnable reportTransportTerminated;
66
  private Listener listener;
67

68
  @Nonnull
1 ✔
69
  @GuardedBy("lock")
70
  private Collection<PendingStream> pendingStreams = new LinkedHashSet<>();
71

72
  /** Immutable state needed for picking. 'lock' must be held for writing. */
73
  private volatile PickerState pickerState = new PickerState(null, null);
1 ✔
74

75
  /**
76
   * Creates a new delayed transport.
77
   *
78
   * @param defaultAppExecutor pending streams will create real streams and run buffered operations
79
   *        in an application executor, which will be this executor, unless there is on provided in
80
   *        {@link CallOptions}.
81
   * @param syncContext all listener callbacks of the delayed transport will be run from this
82
   *        SynchronizationContext.
83
   */
84
  DelayedClientTransport(Executor defaultAppExecutor, SynchronizationContext syncContext) {
1 ✔
85
    this.defaultAppExecutor = defaultAppExecutor;
1 ✔
86
    this.syncContext = syncContext;
1 ✔
87
  }
1 ✔
88

89
  @Override
90
  public final Runnable start(final Listener listener) {
91
    this.listener = listener;
1 ✔
92
    reportTransportInUse = new Runnable() {
1 ✔
93
        @Override
94
        public void run() {
95
          listener.transportInUse(true);
1 ✔
96
        }
1 ✔
97
      };
98
    reportTransportNotInUse = new Runnable() {
1 ✔
99
        @Override
100
        public void run() {
101
          listener.transportInUse(false);
1 ✔
102
        }
1 ✔
103
      };
104
    reportTransportTerminated = new Runnable() {
1 ✔
105
        @Override
106
        public void run() {
107
          listener.transportTerminated();
1 ✔
108
        }
1 ✔
109
      };
110
    return null;
1 ✔
111
  }
112

113
  /**
114
   * If a {@link SubchannelPicker} is being, or has been provided via {@link #reprocess}, the last
115
   * picker will be consulted.
116
   *
117
   * <p>Otherwise, if the delayed transport is not shutdown, then a {@link PendingStream} is
118
   * returned; if the transport is shutdown, then a {@link FailingClientStream} is returned.
119
   */
120
  @Override
121
  public final ClientStream newStream(
122
      MethodDescriptor<?, ?> method, Metadata headers, CallOptions callOptions,
123
      ClientStreamTracer[] tracers) {
124
    try {
125
      PickSubchannelArgs args = new PickSubchannelArgsImpl(
1 ✔
126
          method, headers, callOptions, new PickDetailsConsumerImpl(tracers));
127
      PickerState state = pickerState;
1 ✔
128
      while (true) {
129
        if (state.shutdownStatus != null) {
1 ✔
130
          return new FailingClientStream(state.shutdownStatus, tracers);
1 ✔
131
        }
132
        PickResult pickResult = null;
1 ✔
133
        if (state.lastPicker != null) {
1 ✔
134
          pickResult = state.lastPicker.pickSubchannel(args);
1 ✔
135
          callOptions = args.getCallOptions();
1 ✔
136
          // User code provided authority takes precedence over the LB provided one.
137
          if (callOptions.getAuthority() == null
1 ✔
138
              && pickResult.getAuthorityOverride() != null) {
1 ✔
139
            callOptions = callOptions.withAuthority(pickResult.getAuthorityOverride());
1 ✔
140
          }
141
          ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
1 ✔
142
              callOptions.isWaitForReady());
1 ✔
143
          if (transport != null) {
1 ✔
144
            ClientStream stream = transport.newStream(
1 ✔
145
                args.getMethodDescriptor(), args.getHeaders(), callOptions,
1 ✔
146
                tracers);
147
            // User code provided authority takes precedence over the LB provided one; this will be
148
            // overwritten by ClientCallImpl if the application sets an authority override
149
            if (pickResult.getAuthorityOverride() != null) {
1 ✔
150
              stream.setAuthority(pickResult.getAuthorityOverride());
1 ✔
151
            }
152
            return stream;
1 ✔
153
          }
154
        }
155
        // This picker's conclusion is "buffer".  If there hasn't been a newer picker set (possible
156
        // race with reprocess()), we will buffer the RPC.  Otherwise, will try with the new picker.
157
        synchronized (lock) {
1 ✔
158
          PickerState newerState = pickerState;
1 ✔
159
          if (state == newerState) {
1 ✔
160
            String delayType = determineQueuingDelayType(pickResult);
1 ✔
161
            String delayReason = determineQueuingDelayReason(pickResult);
1 ✔
162
            return createPendingStream(args, tracers, pickResult, delayType, delayReason);
1 ✔
163
          }
164
          state = newerState;
1 ✔
165
        }
1 ✔
166
      }
1 ✔
167
    } finally {
168
      syncContext.drain();
1 ✔
169
    }
170
  }
171

172
  /**
173
   * Caller must call {@code syncContext.drain()} outside of lock because this method may
174
   * schedule tasks on syncContext.
175
   */
176
  @GuardedBy("lock")
177
  private PendingStream createPendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers,
178
      PickResult pickResult, String delayType, String delayReason) {
179
    PendingStream pendingStream = new PendingStream(args, tracers, delayType, delayReason);
1 ✔
180
    if (args.getCallOptions().isWaitForReady() && pickResult != null && pickResult.hasResult()) {
1 ✔
181
      pendingStream.lastPickStatus = pickResult.getStatus();
1 ✔
182
    }
183
    pendingStreams.add(pendingStream);
1 ✔
184
    if (getPendingStreamsCount() == 1) {
1 ✔
185
      syncContext.executeLater(reportTransportInUse);
1 ✔
186
    }
187
    for (ClientStreamTracer streamTracer : tracers) {
1 ✔
188
      streamTracer.createPendingStream();
1 ✔
189
    }
190
    return pendingStream;
1 ✔
191
  }
192

193
  @Override
194
  public final void ping(final PingCallback callback, Executor executor) {
195
    throw new UnsupportedOperationException("This method is not expected to be called");
×
196
  }
197

198
  @Override
199
  public ListenableFuture<SocketStats> getStats() {
200
    SettableFuture<SocketStats> ret = SettableFuture.create();
×
201
    ret.set(null);
×
202
    return ret;
×
203
  }
204

205
  /**
206
   * Prevents creating any new streams. Buffered streams are not failed and may still proceed
207
   * when {@link #reprocess} is called. The delayed transport will be terminated when there is no
208
   * more buffered streams.
209
   */
210
  @Override
211
  public final void shutdown(final Status status) {
212
    synchronized (lock) {
1 ✔
213
      if (pickerState.shutdownStatus != null) {
1 ✔
214
        return;
1 ✔
215
      }
216
      pickerState = pickerState.withShutdownStatus(status);
1 ✔
217
      syncContext.executeLater(new Runnable() {
1 ✔
218
          @Override
219
          public void run() {
220
            listener.transportShutdown(status, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN);
1 ✔
221
          }
1 ✔
222
        });
223
      if (!hasPendingStreams() && reportTransportTerminated != null) {
1 ✔
224
        syncContext.executeLater(reportTransportTerminated);
1 ✔
225
        reportTransportTerminated = null;
1 ✔
226
      }
227
    }
1 ✔
228
    syncContext.drain();
1 ✔
229
  }
1 ✔
230

231
  /**
232
   * Shuts down this transport and cancels all streams that it owns, hence immediately terminates
233
   * this transport.
234
   */
235
  @Override
236
  public final void shutdownNow(Status status) {
237
    shutdown(status);
1 ✔
238
    Collection<PendingStream> savedPendingStreams;
239
    Runnable savedReportTransportTerminated;
240
    synchronized (lock) {
1 ✔
241
      savedPendingStreams = pendingStreams;
1 ✔
242
      savedReportTransportTerminated = reportTransportTerminated;
1 ✔
243
      reportTransportTerminated = null;
1 ✔
244
      if (!pendingStreams.isEmpty()) {
1 ✔
245
        pendingStreams = Collections.emptyList();
1 ✔
246
      }
247
    }
1 ✔
248
    if (savedReportTransportTerminated != null) {
1 ✔
249
      for (PendingStream stream : savedPendingStreams) {
1 ✔
250
        Runnable runnable = stream.setStreamAndEndDelay(
1 ✔
251
            new FailingClientStream(status, RpcProgress.REFUSED, stream.tracers));
1 ✔
252
        if (runnable != null) {
1 ✔
253
          // Drain in-line instead of using an executor as failing stream just throws everything
254
          // away. This is essentially the same behavior as DelayedStream.cancel() but can be done
255
          // before stream.start().
256
          runnable.run();
1 ✔
257
        }
258
      }
1 ✔
259
      syncContext.execute(savedReportTransportTerminated);
1 ✔
260
    }
261
    // If savedReportTransportTerminated == null, transportTerminated() has already been called in
262
    // shutdown().
263
  }
1 ✔
264

265
  public final boolean hasPendingStreams() {
266
    synchronized (lock) {
1 ✔
267
      return !pendingStreams.isEmpty();
1 ✔
268
    }
269
  }
270

271
  @VisibleForTesting
272
  final int getPendingStreamsCount() {
273
    synchronized (lock) {
1 ✔
274
      return pendingStreams.size();
1 ✔
275
    }
276
  }
277

278
  /**
279
   * Use the picker to try picking a transport for every pending stream, proceed the stream if the
280
   * pick is successful, otherwise keep it pending.
281
   *
282
   * <p>This method may be called concurrently with {@code newStream()}, and it's safe.  All pending
283
   * streams will be served by the latest picker (if a same picker is given more than once, they are
284
   * considered different pickers) as soon as possible.
285
   *
286
   * <p>This method <strong>must not</strong> be called concurrently with itself.
287
   */
288
  final void reprocess(@Nullable SubchannelPicker picker) {
289
    ArrayList<PendingStream> toProcess;
290
    synchronized (lock) {
1 ✔
291
      pickerState = pickerState.withPicker(picker);
1 ✔
292
      if (picker == null || !hasPendingStreams()) {
1 ✔
293
        return;
1 ✔
294
      }
295
      toProcess = new ArrayList<>(pendingStreams);
1 ✔
296
    }
1 ✔
297
    ArrayList<PendingStream> toRemove = new ArrayList<>();
1 ✔
298

299
    for (final PendingStream stream : toProcess) {
1 ✔
300
      PickResult pickResult = picker.pickSubchannel(stream.args);
1 ✔
301
      CallOptions callOptions = stream.args.getCallOptions();
1 ✔
302
      if (callOptions.isWaitForReady() && pickResult.hasResult()) {
1 ✔
303
        stream.lastPickStatus = pickResult.getStatus();
1 ✔
304
      }
305
      final ClientTransport transport = GrpcUtil.getTransportFromPickResult(pickResult,
1 ✔
306
          callOptions.isWaitForReady());
1 ✔
307
      if (transport != null) {
1 ✔
308
        stream.endDelay();
1 ✔
309
        Executor executor = defaultAppExecutor;
1 ✔
310
        // createRealStream may be expensive. It will start real streams on the transport. If
311
        // there are pending requests, they will be serialized too, which may be expensive. Since
312
        // we are now on transport thread, we need to offload the work to an executor.
313
        if (callOptions.getExecutor() != null) {
1 ✔
314
          executor = callOptions.getExecutor();
1 ✔
315
        }
316
        Runnable runnable = stream.createRealStream(transport, pickResult.getAuthorityOverride());
1 ✔
317
        if (runnable != null) {
1 ✔
318
          executor.execute(runnable);
1 ✔
319
        }
320
        toRemove.add(stream);
1 ✔
321
      } else { // stay pending
1 ✔
322
        String delayType = determineQueuingDelayType(pickResult);
1 ✔
323
        String delayReason = determineQueuingDelayReason(pickResult);
1 ✔
324
        stream.updateDelay(delayType, delayReason);
1 ✔
325
      }
326
    }
1 ✔
327

328
    synchronized (lock) {
1 ✔
329
      // Between this synchronized and the previous one:
330
      //   - Streams may have been cancelled, which may turn pendingStreams into emptiness.
331
      //   - shutdown() may be called, which may turn pendingStreams into null.
332
      if (!hasPendingStreams()) {
1 ✔
333
        return;
1 ✔
334
      }
335
      // Avoid pendingStreams.removeAll() as it can degrade to calling toRemove.contains() for each
336
      // element in pendingStreams.
337
      for (PendingStream stream : toRemove) {
1 ✔
338
        pendingStreams.remove(stream);
1 ✔
339
      }
1 ✔
340
      // Because delayed transport is long-lived, we take this opportunity to down-size the
341
      // hashmap.
342
      if (pendingStreams.isEmpty()) {
1 ✔
343
        pendingStreams = new LinkedHashSet<>();
1 ✔
344
      }
345
      if (!hasPendingStreams()) {
1 ✔
346
        // There may be a brief gap between delayed transport clearing in-use state, and first real
347
        // transport starting streams and setting in-use state.  During the gap the whole channel's
348
        // in-use state may be false. However, it shouldn't cause spurious switching to idleness
349
        // (which would shutdown the transports and LoadBalancer) because the gap should be shorter
350
        // than IDLE_MODE_DEFAULT_TIMEOUT_MILLIS (1 second).
351
        syncContext.executeLater(reportTransportNotInUse);
1 ✔
352
        if (pickerState.shutdownStatus != null && reportTransportTerminated != null) {
1 ✔
353
          syncContext.executeLater(reportTransportTerminated);
1 ✔
354
          reportTransportTerminated = null;
1 ✔
355
        }
356
      }
357
    }
1 ✔
358
    syncContext.drain();
1 ✔
359
  }
1 ✔
360

361
  @Override
362
  public InternalLogId getLogId() {
363
    return logId;
×
364
  }
365

366
  private static String determineQueuingDelayType(@Nullable PickResult pickResult) {
367
    if (pickResult == null) {
1 ✔
368
      return "connecting";
1 ✔
369
    }
370
    if (pickResult.getSubchannel() != null) {
1 ✔
371
      return "subchannel_state_mismatch";
1 ✔
372
    }
373
    if (!pickResult.getStatus().isOk()) {
1 ✔
374
      return "picker_failing_with_wait_for_ready";
1 ✔
375
    }
376
    if (pickResult.getDelayType() != null) {
1 ✔
377
      return pickResult.getDelayType();
1 ✔
378
    }
379
    return "connecting";
1 ✔
380
  }
381

382
  private static String determineQueuingDelayReason(@Nullable PickResult pickResult) {
383
    if (pickResult == null) {
1 ✔
384
      return "client channel: waiting for picker";
1 ✔
385
    }
386
    if (pickResult.getSubchannel() != null) {
1 ✔
387
      return "subchannel returned by LB picker has no connected subchannel";
1 ✔
388
    }
389
    if (!pickResult.getStatus().isOk()) {
1 ✔
390
      Status status = pickResult.getStatus();
1 ✔
391
      // Status.toString() would append the cause's stack trace.
392
      return "wait_for_ready RPC failed with status: " + status.getCode()
1 ✔
393
          + (status.getDescription() == null ? "" : ": " + status.getDescription());
1 ✔
394
    }
395
    if (pickResult.getDelayReason() != null) {
1 ✔
396
      return pickResult.getDelayReason();
1 ✔
397
    }
398
    return "client channel: waiting for picker";
1 ✔
399
  }
400

401
  private class PendingStream extends DelayedStream {
402
    private final PickSubchannelArgs args;
403
    private final Context context = Context.current();
1 ✔
404
    private final ClientStreamTracer[] tracers;
405
    private volatile Status lastPickStatus;
406
    @GuardedBy("this")
407
    @Nullable private String activeDelayType;
408
    @GuardedBy("this")
409
    @Nullable private String activeDelayReason;
410

411
    private PendingStream(PickSubchannelArgs args, ClientStreamTracer[] tracers,
412
        String delayType, String delayReason) {
1 ✔
413
      super("connecting_and_lb");
1 ✔
414
      this.args = args;
1 ✔
415
      this.tracers = tracers;
1 ✔
416
      this.activeDelayType = delayType;
1 ✔
417
      this.activeDelayReason = delayReason;
1 ✔
418
      for (ClientStreamTracer tracer : tracers) {
1 ✔
419
        tracer.recordDelayStart(delayType, delayReason);
1 ✔
420
      }
421
    }
1 ✔
422

423
    /**
424
     * Updates active attempt delay telemetry state upon load balancing state transitions.
425
     *
426
     * <p>If {@code newType} differs from the active delay type, active segment timers and child
427
     * spans are ended and a new segment is initiated. If only {@code newReason} changes, a
428
     * structured transition event is appended to the active span without span re-creation.
429
     */
430
    synchronized void updateDelay(String newType, String newReason) {
431
      if (getRealStream() != null) {
1 ✔
432
        return;
1 ✔
433
      }
434
      if (!newType.equals(activeDelayType)) {
1 ✔
435
        // Delay type changed (e.g., from RLS lookup to connecting). End the previous delay.
436
        endDelay();
1 ✔
437
        activeDelayType = newType;
1 ✔
438
        activeDelayReason = newReason;
1 ✔
439
        for (ClientStreamTracer tracer : tracers) {
1 ✔
440
          tracer.recordDelayStart(newType, newReason);
1 ✔
441
        }
442
      } else if (!newReason.equals(activeDelayReason)) {
1 ✔
443
        // Delay type is unchanged, but the reason changed (e.g., priority failover).
444
        activeDelayReason = newReason;
1 ✔
445
        for (ClientStreamTracer tracer : tracers) {
1 ✔
446
          tracer.recordDelayReasonChanged(newType, newReason);
1 ✔
447
        }
448
      }
449
    }
1 ✔
450

451
    /**
452
     * Ends active attempt delay segment telemetry upon stream creation or stream cancellation.
453
     */
454
    synchronized void endDelay() {
455
      String delayType = activeDelayType;
1 ✔
456
      if (delayType != null) {
1 ✔
457
        activeDelayType = null;
1 ✔
458
        activeDelayReason = null;
1 ✔
459
        for (ClientStreamTracer tracer : tracers) {
1 ✔
460
          tracer.recordDelayEnd(delayType);
1 ✔
461
        }
462
      }
463
    }
1 ✔
464

465
    Runnable setStreamAndEndDelay(ClientStream stream) {
466
      endDelay();
1 ✔
467
      return setStream(stream);
1 ✔
468
    }
469

470
    /** Runnable may be null. */
471
    private Runnable createRealStream(ClientTransport transport, String authorityOverride) {
472
      ClientStream realStream;
473
      Context origContext = context.attach();
1 ✔
474
      try {
475
        realStream = transport.newStream(
1 ✔
476
            args.getMethodDescriptor(), args.getHeaders(), args.getCallOptions(),
1 ✔
477
            tracers);
478
      } finally {
479
        context.detach(origContext);
1 ✔
480
      }
481
      if (authorityOverride != null) {
1 ✔
482
        // User code provided authority takes precedence over the LB provided one; this will be
483
        // overwritten by an enqueud call from ClientCallImpl if the application sets an authority
484
        // override. We must call the real stream directly because stream.start() has likely already
485
        // been called on the delayed stream.
486
        realStream.setAuthority(authorityOverride);
1 ✔
487
      }
488
      return setStreamAndEndDelay(realStream);
1 ✔
489
    }
490

491
    @Override
492
    public void cancel(Status reason) {
493
      super.cancel(reason);
1 ✔
494
      synchronized (lock) {
1 ✔
495
        if (reportTransportTerminated != null) {
1 ✔
496
          boolean justRemovedAnElement = pendingStreams.remove(this);
1 ✔
497
          if (!hasPendingStreams() && justRemovedAnElement) {
1 ✔
498
            syncContext.executeLater(reportTransportNotInUse);
1 ✔
499
            if (pickerState.shutdownStatus != null) {
1 ✔
500
              syncContext.executeLater(reportTransportTerminated);
1 ✔
501
              reportTransportTerminated = null;
1 ✔
502
            }
503
          }
504
        }
505
      }
1 ✔
506
      syncContext.drain();
1 ✔
507
    }
1 ✔
508

509
    @Override
510
    protected void onEarlyCancellation(Status reason) {
511
      endDelay();
1 ✔
512
      for (ClientStreamTracer tracer : tracers) {
1 ✔
513
        tracer.streamClosed(reason);
1 ✔
514
      }
515
    }
1 ✔
516

517
    @Override
518
    public void appendTimeoutInsight(InsightBuilder insight) {
519
      if (args.getCallOptions().isWaitForReady()) {
1 ✔
520
        insight.append("wait_for_ready");
1 ✔
521
        Status status = lastPickStatus;
1 ✔
522
        if (status != null && !status.isOk()) {
1 ✔
523
          insight.appendKeyValue("Last Pick Failure", status);
1 ✔
524
        }
525
      }
526
      super.appendTimeoutInsight(insight);
1 ✔
527
    }
1 ✔
528
  }
529

530
  static final class PickerState {
531
    /**
532
     * The last picker that {@link #reprocess} has used. May be set to null when the channel has
533
     * moved to idle.
534
     */
535
    @Nullable
536
    final SubchannelPicker lastPicker;
537
    /**
538
     * When {@code shutdownStatus != null && !hasPendingStreams()}, then the transport is considered
539
     * terminated.
540
     */
541
    @Nullable
542
    final Status shutdownStatus;
543

544
    private PickerState(SubchannelPicker lastPicker, Status shutdownStatus) {
1 ✔
545
      this.lastPicker = lastPicker;
1 ✔
546
      this.shutdownStatus = shutdownStatus;
1 ✔
547
    }
1 ✔
548

549
    public PickerState withPicker(SubchannelPicker newPicker) {
550
      return new PickerState(newPicker, this.shutdownStatus);
1 ✔
551
    }
552

553
    public PickerState withShutdownStatus(Status newShutdownStatus) {
554
      return new PickerState(this.lastPicker, newShutdownStatus);
1 ✔
555
    }
556
  }
557
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc