3434import java .time .Clock ;
3535import java .util .ArrayList ;
3636import java .util .List ;
37+ import java .util .Random ;
3738import java .util .concurrent .CancellationException ;
3839import java .util .concurrent .ConcurrentLinkedQueue ;
3940import java .util .concurrent .Executors ;
@@ -71,6 +72,7 @@ public class BigtableChannelPool extends ManagedChannel {
7172 private final ChannelPoolHealthChecker channelPoolHealthChecker ;
7273 private final AtomicInteger indexTicker = new AtomicInteger ();
7374 private final String authority ;
75+ private final Random rng = new Random ();
7476
7577 public static BigtableChannelPool create (
7678 BigtableChannelPoolSettings settings ,
@@ -138,19 +140,92 @@ public String authority() {
138140 }
139141
140142 /**
141- * Create a {@link ClientCall} on a Channel from the pool chosen in a round-robin fashion to the
142- * remote operation specified by the given {@link MethodDescriptor}. The returned {@link
143- * ClientCall} does not trigger any remote behavior until {@link
144- * ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
143+ * Create a {@link ClientCall} on a Channel from the pool to the remote operation specified by the
144+ * given {@link MethodDescriptor}. The returned {@link ClientCall} does not trigger any remote
145+ * behavior until {@link ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
145146 */
146147 @ Override
147148 public <ReqT , RespT > ClientCall <ReqT , RespT > newCall (
148149 MethodDescriptor <ReqT , RespT > methodDescriptor , CallOptions callOptions ) {
149- return getChannel ( indexTicker . getAndIncrement ()).newCall (methodDescriptor , callOptions );
150+ return new AffinityChannel ( pickEntryIndex ()).newCall (methodDescriptor , callOptions );
150151 }
151152
152- Channel getChannel (int affinity ) {
153- return new AffinityChannel (affinity );
153+ /**
154+ * Pick the index of an entry to use for the next call. The returned value *should* be within
155+ * range, but callers should not assume that this is always the case as race conditions are
156+ * possible.
157+ */
158+ private int pickEntryIndex () {
159+ switch (settings .getLoadBalancingStrategy ()) {
160+ case ROUND_ROBIN :
161+ return pickEntryIndexRoundRobin ();
162+ case LEAST_IN_FLIGHT :
163+ return pickEntryIndexLeastInFlight ();
164+ case POWER_OF_TWO_LEAST_IN_FLIGHT :
165+ return pickEntryIndexPowerOfTwoLeastInFlight ();
166+ default :
167+ LOG .warning (String .format (
168+ "Unknown load balancing strategy %s, falling back to ROUND_ROBIN." ,
169+ settings .getLoadBalancingStrategy ()));
170+ return pickEntryIndexRoundRobin ();
171+
172+ }
173+ }
174+
175+ /** Pick an entry using the Round Robin algorithm. */
176+ private int pickEntryIndexRoundRobin () {
177+ return Math .abs (indexTicker .getAndIncrement () % entries .get ().size ());
178+ }
179+
180+ /** Pick an entry at random. */
181+ private int pickEntryIndexRandom () {
182+ return rng .nextInt (entries .get ().size ());
183+ }
184+
185+ /** Pick an entry using the least-in-flight algorithm. */
186+ private int pickEntryIndexLeastInFlight () {
187+ List <Entry > localEntries = entries .get ();
188+ int minRpcs = Integer .MAX_VALUE ;
189+ List <Integer > candidates = new ArrayList <>();
190+
191+ for (int i = 0 ; i < localEntries .size (); i ++) {
192+ Entry entry = localEntries .get (i );
193+ int rpcs = entry .outstandingRpcs .get ();
194+ if (rpcs < minRpcs ) {
195+ minRpcs = rpcs ;
196+ candidates .clear ();
197+ candidates .add (i );
198+ } else if (rpcs == minRpcs ) {
199+ candidates .add (i );
200+ }
201+ }
202+ if (candidates .isEmpty ()) {
203+ LOG .warning (
204+ "Least-in-flight picker couldn't find available channel. Picking at random." );
205+ return pickEntryIndexRandom ();
206+ }
207+ // If there are multiple matching entries, pick one at random.
208+ return candidates .get (rng .nextInt (candidates .size ()));
209+ }
210+
211+ /** Pick an entry using the power-of-two algorithm. */
212+ private int pickEntryIndexPowerOfTwoLeastInFlight () {
213+ List <Entry > localEntries = entries .get ();
214+ int choice1 = pickEntryIndexRandom ();
215+ int choice2 = pickEntryIndexRandom ();
216+ if (choice1 == choice2 ) {
217+ // Try to pick two different entries. If this picks the same entry again, it's likely that
218+ // there's only one healthy channel in the pool and we should proceed anyway.
219+ choice2 = pickEntryIndexRandom ();
220+ }
221+
222+ Entry entry1 = localEntries .get (choice1 );
223+ Entry entry2 = localEntries .get (choice2 );
224+ return entry1 .outstandingRpcs .get () < entry2 .outstandingRpcs .get () ? choice1 : choice2 ;
225+ }
226+
227+ Channel getChannel (int index ) {
228+ return new AffinityChannel (index );
154229 }
155230
156231 /** {@inheritDoc} */
@@ -395,7 +470,9 @@ void refresh() {
395470 * Get and retain a Channel Entry. The returned Entry will have its rpc count incremented,
396471 * preventing it from getting recycled.
397472 */
398- Entry getRetainedEntry (int affinity ) {
473+ private Entry getRetainedEntry (int affinity ) {
474+ // If an entry is not retainable, that usually means that it's about to be replaced and if we
475+ // retry we should get a new useable entry.
399476 // The maximum number of concurrent calls to this method for any given time span is at most 2,
400477 // so the loop can actually be 2 times. But going for 5 times for a safety margin for potential
401478 // code evolving
@@ -543,10 +620,10 @@ private void shutdown() {
543620
544621 /** Thin wrapper to ensure that new calls are properly reference counted. */
545622 private class AffinityChannel extends Channel {
546- private final int affinity ;
623+ private final int index ;
547624
548- public AffinityChannel (int affinity ) {
549- this .affinity = affinity ;
625+ public AffinityChannel (int index ) {
626+ this .index = index ;
550627 }
551628
552629 @ Override
@@ -557,9 +634,7 @@ public String authority() {
557634 @ Override
558635 public <RequestT , ResponseT > ClientCall <RequestT , ResponseT > newCall (
559636 MethodDescriptor <RequestT , ResponseT > methodDescriptor , CallOptions callOptions ) {
560-
561- Entry entry = getRetainedEntry (affinity );
562-
637+ Entry entry = getRetainedEntry (index );
563638 return new ReleasingClientCall <>(entry .channel .newCall (methodDescriptor , callOptions ), entry );
564639 }
565640 }
0 commit comments