31
31
32
32
import com .google .api .core .InternalApi ;
33
33
import com .google .common .annotations .VisibleForTesting ;
34
+ import com .google .common .base .Preconditions ;
34
35
import com .google .common .collect .ImmutableList ;
35
36
import io .grpc .CallOptions ;
36
37
import io .grpc .Channel ;
46
47
import java .util .List ;
47
48
import java .util .concurrent .Executors ;
48
49
import java .util .concurrent .ScheduledExecutorService ;
49
- import java .util .concurrent .ScheduledFuture ;
50
50
import java .util .concurrent .TimeUnit ;
51
51
import java .util .concurrent .atomic .AtomicBoolean ;
52
52
import java .util .concurrent .atomic .AtomicInteger ;
53
53
import java .util .concurrent .atomic .AtomicReference ;
54
54
import java .util .logging .Level ;
55
55
import java .util .logging .Logger ;
56
- import javax .annotation .Nullable ;
57
56
import org .threeten .bp .Duration ;
58
57
59
58
/**
68
67
*/
69
68
class ChannelPool extends ManagedChannel {
70
69
private static final Logger LOG = Logger .getLogger (ChannelPool .class .getName ());
71
-
72
- // size greater than 1 to allow multiple channel to refresh at the same time
73
- // size not too large so refreshing channels doesn't use too many threads
74
- private static final int CHANNEL_REFRESH_EXECUTOR_SIZE = 2 ;
75
70
private static final Duration REFRESH_PERIOD = Duration .ofMinutes (50 );
76
- private static final double JITTER_PERCENTAGE = 0.15 ;
71
+
72
+ private final ChannelPoolSettings settings ;
73
+ private final ChannelFactory channelFactory ;
74
+ private final ScheduledExecutorService executor ;
77
75
78
76
private final Object entryWriteLock = new Object ();
79
77
private final AtomicReference <ImmutableList <Entry >> entries = new AtomicReference <>();
80
78
private final AtomicInteger indexTicker = new AtomicInteger ();
81
79
private final String authority ;
82
- // if set, ChannelPool will manage the life cycle of channelRefreshExecutorService
83
- @ Nullable private final ScheduledExecutorService channelRefreshExecutorService ;
84
- private final ChannelFactory channelFactory ;
85
-
86
- private volatile ScheduledFuture <?> nextScheduledRefresh = null ;
87
80
88
81
/**
89
82
* Factory method to create a non-refreshing channel pool
@@ -92,8 +85,9 @@ class ChannelPool extends ManagedChannel {
92
85
* @param channelFactory method to create the channels
93
86
* @return ChannelPool of non-refreshing channels
94
87
*/
88
+ @ VisibleForTesting
95
89
static ChannelPool create (int poolSize , ChannelFactory channelFactory ) throws IOException {
96
- return new ChannelPool (channelFactory , poolSize , null );
90
+ return new ChannelPool (ChannelPoolSettings . staticallySized ( poolSize ), channelFactory , null );
97
91
}
98
92
99
93
/**
@@ -103,58 +97,66 @@ static ChannelPool create(int poolSize, ChannelFactory channelFactory) throws IO
103
97
*
104
98
* @param poolSize number of channels in the pool
105
99
* @param channelFactory method to create the channels
106
- * @param channelRefreshExecutorService periodically refreshes the channels; its life cycle will
107
- * be managed by ChannelPool
100
+ * @param executor used to schedule maintenance tasks like refresh channels and resizing the pool.
108
101
* @return ChannelPool of refreshing channels
109
102
*/
110
103
@ VisibleForTesting
111
104
static ChannelPool createRefreshing (
112
- int poolSize ,
113
- ChannelFactory channelFactory ,
114
- ScheduledExecutorService channelRefreshExecutorService )
105
+ int poolSize , ChannelFactory channelFactory , ScheduledExecutorService executor )
115
106
throws IOException {
116
- return new ChannelPool (channelFactory , poolSize , channelRefreshExecutorService );
107
+ return new ChannelPool (
108
+ ChannelPoolSettings .staticallySized (poolSize )
109
+ .toBuilder ()
110
+ .setPreemptiveReconnectEnabled (true )
111
+ .build (),
112
+ channelFactory ,
113
+ executor );
117
114
}
118
115
119
- /**
120
- * Factory method to create a refreshing channel pool
121
- *
122
- * @param poolSize number of channels in the pool
123
- * @param channelFactory method to create the channels
124
- * @return ChannelPool of refreshing channels
125
- */
126
- static ChannelPool createRefreshing (int poolSize , final ChannelFactory channelFactory )
116
+ static ChannelPool create (ChannelPoolSettings settings , ChannelFactory channelFactory )
127
117
throws IOException {
128
- return createRefreshing (
129
- poolSize , channelFactory , Executors .newScheduledThreadPool (CHANNEL_REFRESH_EXECUTOR_SIZE ));
118
+ return new ChannelPool (settings , channelFactory , Executors .newSingleThreadScheduledExecutor ());
130
119
}
131
120
132
121
/**
133
122
* Initializes the channel pool. Assumes that all channels have the same authority.
134
123
*
124
+ * @param settings options for controling the ChannelPool sizing behavior
135
125
* @param channelFactory method to create the channels
136
- * @param poolSize number of channels in the pool
137
- * @param channelRefreshExecutorService periodically refreshes the channels
126
+ * @param executor periodically refreshes the channels. Must be single threaded
138
127
*/
139
- private ChannelPool (
128
+ @ InternalApi ("VisibleForTesting" )
129
+ ChannelPool (
130
+ ChannelPoolSettings settings ,
140
131
ChannelFactory channelFactory ,
141
- int poolSize ,
142
- @ Nullable ScheduledExecutorService channelRefreshExecutorService )
132
+ ScheduledExecutorService executor )
143
133
throws IOException {
134
+ this .settings = settings ;
144
135
this .channelFactory = channelFactory ;
145
136
146
137
ImmutableList .Builder <Entry > initialListBuilder = ImmutableList .builder ();
147
138
148
- for (int i = 0 ; i < poolSize ; i ++) {
139
+ for (int i = 0 ; i < settings . getInitialChannelCount () ; i ++) {
149
140
initialListBuilder .add (new Entry (channelFactory .createSingleChannel ()));
150
141
}
151
142
152
143
entries .set (initialListBuilder .build ());
153
144
authority = entries .get ().get (0 ).channel .authority ();
154
- this .channelRefreshExecutorService = channelRefreshExecutorService ;
155
-
156
- if (channelRefreshExecutorService != null ) {
157
- nextScheduledRefresh = scheduleNextRefresh ();
145
+ this .executor = executor ;
146
+
147
+ if (!settings .isStaticSize ()) {
148
+ executor .scheduleAtFixedRate (
149
+ this ::resizeSafely ,
150
+ ChannelPoolSettings .RESIZE_INTERVAL .getSeconds (),
151
+ ChannelPoolSettings .RESIZE_INTERVAL .getSeconds (),
152
+ TimeUnit .SECONDS );
153
+ }
154
+ if (settings .isPreemptiveReconnectEnabled ()) {
155
+ executor .scheduleAtFixedRate (
156
+ this ::refreshSafely ,
157
+ REFRESH_PERIOD .getSeconds (),
158
+ REFRESH_PERIOD .getSeconds (),
159
+ TimeUnit .SECONDS );
158
160
}
159
161
}
160
162
@@ -187,12 +189,9 @@ public ManagedChannel shutdown() {
187
189
for (Entry entry : localEntries ) {
188
190
entry .channel .shutdown ();
189
191
}
190
- if (nextScheduledRefresh != null ) {
191
- nextScheduledRefresh .cancel (true );
192
- }
193
- if (channelRefreshExecutorService != null ) {
192
+ if (executor != null ) {
194
193
// shutdownNow will cancel scheduled tasks
195
- channelRefreshExecutorService .shutdownNow ();
194
+ executor .shutdownNow ();
196
195
}
197
196
return this ;
198
197
}
@@ -206,7 +205,7 @@ public boolean isShutdown() {
206
205
return false ;
207
206
}
208
207
}
209
- return channelRefreshExecutorService == null || channelRefreshExecutorService .isShutdown ();
208
+ return executor == null || executor .isShutdown ();
210
209
}
211
210
212
211
/** {@inheritDoc} */
@@ -218,7 +217,8 @@ public boolean isTerminated() {
218
217
return false ;
219
218
}
220
219
}
221
- return channelRefreshExecutorService == null || channelRefreshExecutorService .isTerminated ();
220
+
221
+ return executor == null || executor .isTerminated ();
222
222
}
223
223
224
224
/** {@inheritDoc} */
@@ -228,11 +228,8 @@ public ManagedChannel shutdownNow() {
228
228
for (Entry entry : localEntries ) {
229
229
entry .channel .shutdownNow ();
230
230
}
231
- if (nextScheduledRefresh != null ) {
232
- nextScheduledRefresh .cancel (true );
233
- }
234
- if (channelRefreshExecutorService != null ) {
235
- channelRefreshExecutorService .shutdownNow ();
231
+ if (executor != null ) {
232
+ executor .shutdownNow ();
236
233
}
237
234
return this ;
238
235
}
@@ -249,25 +246,129 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE
249
246
}
250
247
entry .channel .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
251
248
}
252
- if (channelRefreshExecutorService != null ) {
249
+ if (executor != null ) {
253
250
long awaitTimeNanos = endTimeNanos - System .nanoTime ();
254
- channelRefreshExecutorService .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
251
+ executor .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
255
252
}
256
253
return isTerminated ();
257
254
}
258
255
259
- /** Scheduling loop. */
260
- private ScheduledFuture <?> scheduleNextRefresh () {
261
- long delayPeriod = REFRESH_PERIOD .toMillis ();
262
- long jitter = (long ) ((Math .random () - 0.5 ) * JITTER_PERCENTAGE * delayPeriod );
263
- long delay = jitter + delayPeriod ;
264
- return channelRefreshExecutorService .schedule (
265
- () -> {
266
- scheduleNextRefresh ();
267
- refresh ();
268
- },
269
- delay ,
270
- TimeUnit .MILLISECONDS );
256
+ void resizeSafely () {
257
+ try {
258
+ synchronized (entryWriteLock ) {
259
+ resize ();
260
+ }
261
+ } catch (Exception e ) {
262
+ LOG .log (Level .WARNING , "Failed to resize channel pool" , e );
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Resize the number of channels based on the number of outstanding RPCs.
268
+ *
269
+ * <p>This method is expected to be called on a fixed interval. On every invocation it will:
270
+ *
271
+ * <ul>
272
+ * <li>Get the maximum number of outstanding RPCs since last invocation
273
+ * <li>Determine a valid range of number of channels to handle that many outstanding RPCs
274
+ * <li>If the current number of channel falls outside of that range, add or remove at most
275
+ * {@link ChannelPoolSettings#MAX_RESIZE_DELTA} to get closer to middle of that range.
276
+ * </ul>
277
+ *
278
+ * <p>Not threadsafe, must be called under the entryWriteLock monitor
279
+ */
280
+ void resize () {
281
+ List <Entry > localEntries = entries .get ();
282
+ // Estimate the peak of RPCs in the last interval by summing the peak of RPCs per channel
283
+ int actualOutstandingRpcs =
284
+ localEntries .stream ().mapToInt (Entry ::getAndResetMaxOutstanding ).sum ();
285
+
286
+ // Number of channels if each channel operated at max capacity
287
+ int minChannels =
288
+ (int ) Math .ceil (actualOutstandingRpcs / (double ) settings .getMaxRpcsPerChannel ());
289
+ // Limit the threshold to absolute range
290
+ if (minChannels < settings .getMinChannelCount ()) {
291
+ minChannels = settings .getMinChannelCount ();
292
+ }
293
+
294
+ // Number of channels if each channel operated at minimum capacity
295
+ int maxChannels =
296
+ (int ) Math .ceil (actualOutstandingRpcs / (double ) settings .getMinRpcsPerChannel ());
297
+ // Limit the threshold to absolute range
298
+ if (maxChannels > settings .getMaxChannelCount ()) {
299
+ maxChannels = settings .getMaxChannelCount ();
300
+ }
301
+ if (maxChannels < minChannels ) {
302
+ maxChannels = minChannels ;
303
+ }
304
+
305
+ // If the pool were to be resized, try to aim for the middle of the bound, but limit rate of
306
+ // change.
307
+ int tentativeTarget = (maxChannels + minChannels ) / 2 ;
308
+ int currentSize = localEntries .size ();
309
+ int delta = tentativeTarget - currentSize ;
310
+ int dampenedTarget = tentativeTarget ;
311
+ if (Math .abs (delta ) > ChannelPoolSettings .MAX_RESIZE_DELTA ) {
312
+ dampenedTarget =
313
+ currentSize + (int ) Math .copySign (ChannelPoolSettings .MAX_RESIZE_DELTA , delta );
314
+ }
315
+
316
+ // Only resize the pool when thresholds are crossed
317
+ if (localEntries .size () < minChannels ) {
318
+ LOG .fine (
319
+ String .format (
320
+ "Detected throughput peak of %d, expanding channel pool size: %d -> %d." ,
321
+ actualOutstandingRpcs , currentSize , dampenedTarget ));
322
+
323
+ expand (tentativeTarget );
324
+ } else if (localEntries .size () > maxChannels ) {
325
+ LOG .fine (
326
+ String .format (
327
+ "Detected throughput drop to %d, shrinking channel pool size: %d -> %d." ,
328
+ actualOutstandingRpcs , currentSize , dampenedTarget ));
329
+
330
+ shrink (tentativeTarget );
331
+ }
332
+ }
333
+
334
+ /** Not threadsafe, must be called under the entryWriteLock monitor */
335
+ private void shrink (int desiredSize ) {
336
+ List <Entry > localEntries = entries .get ();
337
+ Preconditions .checkState (
338
+ localEntries .size () >= desiredSize , "desired size is already smaller than the current" );
339
+
340
+ // Set the new list
341
+ entries .set (ImmutableList .copyOf (localEntries .subList (0 , desiredSize )));
342
+ // clean up removed entries
343
+ List <Entry > removed = localEntries .subList (desiredSize , localEntries .size ());
344
+ removed .forEach (Entry ::requestShutdown );
345
+ }
346
+
347
+ /** Not threadsafe, must be called under the entryWriteLock monitor */
348
+ private void expand (int desiredSize ) {
349
+ List <Entry > localEntries = entries .get ();
350
+ Preconditions .checkState (
351
+ localEntries .size () <= desiredSize , "desired size is already bigger than the current" );
352
+
353
+ ImmutableList .Builder <Entry > newEntries = ImmutableList .<Entry >builder ().addAll (localEntries );
354
+
355
+ for (int i = 0 ; i < desiredSize - localEntries .size (); i ++) {
356
+ try {
357
+ newEntries .add (new Entry (channelFactory .createSingleChannel ()));
358
+ } catch (IOException e ) {
359
+ LOG .log (Level .WARNING , "Failed to add channel" , e );
360
+ }
361
+ }
362
+
363
+ entries .set (newEntries .build ());
364
+ }
365
+
366
+ private void refreshSafely () {
367
+ try {
368
+ refresh ();
369
+ } catch (Exception e ) {
370
+ LOG .log (Level .WARNING , "Failed to pre-emptively refresh channnels" , e );
371
+ }
271
372
}
272
373
273
374
/**
@@ -340,14 +441,21 @@ Entry getRetainedEntry(int affinity) {
340
441
private Entry getEntry (int affinity ) {
341
442
List <Entry > localEntries = entries .get ();
342
443
343
- int index = Math .abs (affinity % localEntries .size ());
444
+ int index = affinity % localEntries .size ();
445
+ index = Math .abs (index );
446
+ // If index is the most negative int, abs(index) is still negative.
447
+ if (index < 0 ) {
448
+ index = 0 ;
449
+ }
450
+
344
451
return localEntries .get (index );
345
452
}
346
453
347
454
/** Bundles a gRPC {@link ManagedChannel} with some usage accounting. */
348
455
private static class Entry {
349
456
private final ManagedChannel channel ;
350
457
private final AtomicInteger outstandingRpcs = new AtomicInteger (0 );
458
+ private final AtomicInteger maxOutstanding = new AtomicInteger ();
351
459
352
460
// Flag that the channel should be closed once all of the outstanding RPC complete.
353
461
private final AtomicBoolean shutdownRequested = new AtomicBoolean ();
@@ -358,6 +466,10 @@ private Entry(ManagedChannel channel) {
358
466
this .channel = channel ;
359
467
}
360
468
469
+ int getAndResetMaxOutstanding () {
470
+ return maxOutstanding .getAndSet (outstandingRpcs .get ());
471
+ }
472
+
361
473
/**
362
474
* Try to increment the outstanding RPC count. The method will return false if the channel is
363
475
* closing and the caller should pick a different channel. If the method returned true, the
@@ -366,7 +478,13 @@ private Entry(ManagedChannel channel) {
366
478
*/
367
479
private boolean retain () {
368
480
// register desire to start RPC
369
- outstandingRpcs .incrementAndGet ();
481
+ int currentOutstanding = outstandingRpcs .incrementAndGet ();
482
+
483
+ // Rough book keeping
484
+ int prevMax = maxOutstanding .get ();
485
+ if (currentOutstanding > prevMax ) {
486
+ maxOutstanding .incrementAndGet ();
487
+ }
370
488
371
489
// abort if the channel is closing
372
490
if (shutdownRequested .get ()) {
0 commit comments