3333import java .io .IOException ;
3434import java .time .Clock ;
3535import java .util .ArrayList ;
36+ import java .util .function .Supplier ;
3637import java .util .List ;
38+ import java .util .Random ;
3739import java .util .concurrent .CancellationException ;
3840import java .util .concurrent .ConcurrentLinkedQueue ;
3941import java .util .concurrent .Executors ;
@@ -71,6 +73,8 @@ public class BigtableChannelPool extends ManagedChannel {
7173 private final ChannelPoolHealthChecker channelPoolHealthChecker ;
7274 private final AtomicInteger indexTicker = new AtomicInteger ();
7375 private final String authority ;
76+ private final Random rng = new Random ();
77+ private final Supplier <Integer > picker ;
7478
7579 public static BigtableChannelPool create (
7680 BigtableChannelPoolSettings settings ,
@@ -113,6 +117,25 @@ public static BigtableChannelPool create(
113117
114118 entries .set (initialListBuilder .build ());
115119 authority = entries .get ().get (0 ).channel .authority ();
120+
121+ switch (settings .getLoadBalancingStrategy ()) {
122+ case ROUND_ROBIN :
123+ picker = this ::pickEntryIndexRoundRobin ;
124+ break ;
125+ case LEAST_IN_FLIGHT :
126+ picker = this ::pickEntryIndexLeastInFlight ;
127+ break ;
128+ case POWER_OF_TWO_LEAST_IN_FLIGHT :
129+ picker = this ::pickEntryIndexPowerOfTwoLeastInFlight ;
130+ break ;
131+ default :
132+ LOG .warning (String .format (
133+ "Unknown load balancing strategy %s, falling back to ROUND_ROBIN." ,
134+ settings .getLoadBalancingStrategy ()));
135+ picker = this ::pickEntryIndexRoundRobin ;
136+
137+ }
138+
116139 this .executor = executor ;
117140
118141 if (!settings .isStaticSize ()) {
@@ -138,19 +161,74 @@ public String authority() {
138161 }
139162
140163 /**
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.
164+ * Create a {@link ClientCall} on a Channel from the pool to the remote operation specified by the
165+ * given {@link MethodDescriptor}. The returned {@link ClientCall} does not trigger any remote
166+ * behavior until {@link ClientCall#start(ClientCall.Listener, io.grpc.Metadata)} is invoked.
145167 */
146168 @ Override
147169 public <ReqT , RespT > ClientCall <ReqT , RespT > newCall (
148170 MethodDescriptor <ReqT , RespT > methodDescriptor , CallOptions callOptions ) {
149- return getChannel ( indexTicker . getAndIncrement ()).newCall (methodDescriptor , callOptions );
171+ return new AffinityChannel ( pickEntryIndex ()).newCall (methodDescriptor , callOptions );
150172 }
151173
152- Channel getChannel (int affinity ) {
153- return new AffinityChannel (affinity );
174+ /**
175+ * Pick the index of an entry to use for the next call. The returned value *should* be within
176+ * range, but callers should not assume that this is always the case as race conditions are
177+ * possible.
178+ */
179+ private int pickEntryIndex () {
180+ return picker .get ();
181+ }
182+
183+ /** Pick an entry using the Round Robin algorithm. */
184+ private int pickEntryIndexRoundRobin () {
185+ return Math .abs (indexTicker .getAndIncrement () % entries .get ().size ());
186+ }
187+
188+ /** Pick an entry at random. */
189+ private int pickEntryIndexRandom () {
190+ return rng .nextInt (entries .get ().size ());
191+ }
192+
193+ /** Pick an entry using the least-in-flight algorithm. */
194+ private int pickEntryIndexLeastInFlight () {
195+ List <Entry > localEntries = entries .get ();
196+ int minRpcs = Integer .MAX_VALUE ;
197+ List <Integer > candidates = new ArrayList <>();
198+
199+ for (int i = 0 ; i < localEntries .size (); i ++) {
200+ Entry entry = localEntries .get (i );
201+ int rpcs = entry .outstandingRpcs .get ();
202+ if (rpcs < minRpcs ) {
203+ minRpcs = rpcs ;
204+ candidates .clear ();
205+ candidates .add (i );
206+ } else if (rpcs == minRpcs ) {
207+ candidates .add (i );
208+ }
209+ }
210+ // If there are multiple matching entries, pick one at random.
211+ return candidates .get (rng .nextInt (candidates .size ()));
212+ }
213+
214+ /** Pick an entry using the power-of-two algorithm. */
215+ private int pickEntryIndexPowerOfTwoLeastInFlight () {
216+ List <Entry > localEntries = entries .get ();
217+ int choice1 = pickEntryIndexRandom ();
218+ int choice2 = pickEntryIndexRandom ();
219+ if (choice1 == choice2 ) {
220+ // Try to pick two different entries. If this picks the same entry again, it's likely that
221+ // there's only one healthy channel in the pool and we should proceed anyway.
222+ choice2 = pickEntryIndexRandom ();
223+ }
224+
225+ Entry entry1 = localEntries .get (choice1 );
226+ Entry entry2 = localEntries .get (choice2 );
227+ return entry1 .outstandingRpcs .get () < entry2 .outstandingRpcs .get () ? choice1 : choice2 ;
228+ }
229+
230+ Channel getChannel (int index ) {
231+ return new AffinityChannel (index );
154232 }
155233
156234 /** {@inheritDoc} */
@@ -395,7 +473,9 @@ void refresh() {
395473 * Get and retain a Channel Entry. The returned Entry will have its rpc count incremented,
396474 * preventing it from getting recycled.
397475 */
398- Entry getRetainedEntry (int affinity ) {
476+ private Entry getRetainedEntry (int affinity ) {
477+ // If an entry is not retainable, that usually means that it's about to be replaced and if we
478+ // retry we should get a new useable entry.
399479 // The maximum number of concurrent calls to this method for any given time span is at most 2,
400480 // so the loop can actually be 2 times. But going for 5 times for a safety margin for potential
401481 // code evolving
@@ -543,10 +623,10 @@ private void shutdown() {
543623
544624 /** Thin wrapper to ensure that new calls are properly reference counted. */
545625 private class AffinityChannel extends Channel {
546- private final int affinity ;
626+ private final int index ;
547627
548- public AffinityChannel (int affinity ) {
549- this .affinity = affinity ;
628+ public AffinityChannel (int index ) {
629+ this .index = index ;
550630 }
551631
552632 @ Override
@@ -557,9 +637,7 @@ public String authority() {
557637 @ Override
558638 public <RequestT , ResponseT > ClientCall <RequestT , ResponseT > newCall (
559639 MethodDescriptor <RequestT , ResponseT > methodDescriptor , CallOptions callOptions ) {
560-
561- Entry entry = getRetainedEntry (affinity );
562-
640+ Entry entry = getRetainedEntry (index );
563641 return new ReleasingClientCall <>(entry .channel .newCall (methodDescriptor , callOptions ), entry );
564642 }
565643 }
0 commit comments