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
- private final AtomicReference <ImmutableList <Entry >> entries = new AtomicReference <>();
77
+ @ VisibleForTesting 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
-
88
- /**
89
- * Factory method to create a non-refreshing channel pool
90
- *
91
- * @param poolSize number of channels in the pool
92
- * @param channelFactory method to create the channels
93
- * @return ChannelPool of non-refreshing channels
94
- */
95
- static ChannelPool create (int poolSize , ChannelFactory channelFactory ) throws IOException {
96
- return new ChannelPool (channelFactory , poolSize , null );
97
- }
98
80
99
- /**
100
- * Factory method to create a refreshing channel pool
101
- *
102
- * <p>Package-private for testing purposes only
103
- *
104
- * @param poolSize number of channels in the pool
105
- * @param channelFactory method to create the channels
106
- * @param channelRefreshExecutorService periodically refreshes the channels; its life cycle will
107
- * be managed by ChannelPool
108
- * @return ChannelPool of refreshing channels
109
- */
110
- @ VisibleForTesting
111
- static ChannelPool createRefreshing (
112
- int poolSize ,
113
- ChannelFactory channelFactory ,
114
- ScheduledExecutorService channelRefreshExecutorService )
81
+ static ChannelPool create (ChannelPoolSettings settings , ChannelFactory channelFactory )
115
82
throws IOException {
116
- return new ChannelPool (channelFactory , poolSize , channelRefreshExecutorService );
117
- }
118
-
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 )
127
- throws IOException {
128
- return createRefreshing (
129
- poolSize , channelFactory , Executors .newScheduledThreadPool (CHANNEL_REFRESH_EXECUTOR_SIZE ));
83
+ return new ChannelPool (settings , channelFactory , Executors .newSingleThreadScheduledExecutor ());
130
84
}
131
85
132
86
/**
133
87
* Initializes the channel pool. Assumes that all channels have the same authority.
134
88
*
89
+ * @param settings options for controling the ChannelPool sizing behavior
135
90
* @param channelFactory method to create the channels
136
- * @param poolSize number of channels in the pool
137
- * @param channelRefreshExecutorService periodically refreshes the channels
91
+ * @param executor periodically refreshes the channels
138
92
*/
139
- private ChannelPool (
93
+ @ VisibleForTesting
94
+ ChannelPool (
95
+ ChannelPoolSettings settings ,
140
96
ChannelFactory channelFactory ,
141
- int poolSize ,
142
- @ Nullable ScheduledExecutorService channelRefreshExecutorService )
97
+ ScheduledExecutorService executor )
143
98
throws IOException {
99
+ this .settings = settings ;
144
100
this .channelFactory = channelFactory ;
145
101
146
102
ImmutableList .Builder <Entry > initialListBuilder = ImmutableList .builder ();
147
103
148
- for (int i = 0 ; i < poolSize ; i ++) {
104
+ for (int i = 0 ; i < settings . getInitialChannelCount () ; i ++) {
149
105
initialListBuilder .add (new Entry (channelFactory .createSingleChannel ()));
150
106
}
151
107
152
108
entries .set (initialListBuilder .build ());
153
109
authority = entries .get ().get (0 ).channel .authority ();
154
- this .channelRefreshExecutorService = channelRefreshExecutorService ;
155
-
156
- if (channelRefreshExecutorService != null ) {
157
- nextScheduledRefresh = scheduleNextRefresh ();
110
+ this .executor = executor ;
111
+
112
+ if (!settings .isStaticSize ()) {
113
+ executor .scheduleAtFixedRate (
114
+ this ::resizeSafely ,
115
+ ChannelPoolSettings .RESIZE_INTERVAL .getSeconds (),
116
+ ChannelPoolSettings .RESIZE_INTERVAL .getSeconds (),
117
+ TimeUnit .SECONDS );
118
+ }
119
+ if (settings .isPreemptiveRefreshEnabled ()) {
120
+ executor .scheduleAtFixedRate (
121
+ this ::refreshSafely ,
122
+ REFRESH_PERIOD .getSeconds (),
123
+ REFRESH_PERIOD .getSeconds (),
124
+ TimeUnit .SECONDS );
158
125
}
159
126
}
160
127
@@ -187,12 +154,9 @@ public ManagedChannel shutdown() {
187
154
for (Entry entry : localEntries ) {
188
155
entry .channel .shutdown ();
189
156
}
190
- if (nextScheduledRefresh != null ) {
191
- nextScheduledRefresh .cancel (true );
192
- }
193
- if (channelRefreshExecutorService != null ) {
157
+ if (executor != null ) {
194
158
// shutdownNow will cancel scheduled tasks
195
- channelRefreshExecutorService .shutdownNow ();
159
+ executor .shutdownNow ();
196
160
}
197
161
return this ;
198
162
}
@@ -206,7 +170,7 @@ public boolean isShutdown() {
206
170
return false ;
207
171
}
208
172
}
209
- return channelRefreshExecutorService == null || channelRefreshExecutorService .isShutdown ();
173
+ return executor == null || executor .isShutdown ();
210
174
}
211
175
212
176
/** {@inheritDoc} */
@@ -218,7 +182,8 @@ public boolean isTerminated() {
218
182
return false ;
219
183
}
220
184
}
221
- return channelRefreshExecutorService == null || channelRefreshExecutorService .isTerminated ();
185
+
186
+ return executor == null || executor .isTerminated ();
222
187
}
223
188
224
189
/** {@inheritDoc} */
@@ -228,11 +193,8 @@ public ManagedChannel shutdownNow() {
228
193
for (Entry entry : localEntries ) {
229
194
entry .channel .shutdownNow ();
230
195
}
231
- if (nextScheduledRefresh != null ) {
232
- nextScheduledRefresh .cancel (true );
233
- }
234
- if (channelRefreshExecutorService != null ) {
235
- channelRefreshExecutorService .shutdownNow ();
196
+ if (executor != null ) {
197
+ executor .shutdownNow ();
236
198
}
237
199
return this ;
238
200
}
@@ -249,25 +211,131 @@ public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedE
249
211
}
250
212
entry .channel .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
251
213
}
252
- if (channelRefreshExecutorService != null ) {
214
+ if (executor != null ) {
253
215
long awaitTimeNanos = endTimeNanos - System .nanoTime ();
254
- channelRefreshExecutorService .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
216
+ executor .awaitTermination (awaitTimeNanos , TimeUnit .NANOSECONDS );
255
217
}
256
218
return isTerminated ();
257
219
}
258
220
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 );
221
+ private void resizeSafely () {
222
+ try {
223
+ synchronized (entryWriteLock ) {
224
+ resize ();
225
+ }
226
+ } catch (Exception e ) {
227
+ LOG .log (Level .WARNING , "Failed to resize channel pool" , e );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Resize the number of channels based on the number of outstanding RPCs.
233
+ *
234
+ * <p>This method is expected to be called on a fixed interval. On every invocation it will:
235
+ *
236
+ * <ul>
237
+ * <li>Get the maximum number of outstanding RPCs since last invocation
238
+ * <li>Determine a valid range of number of channels to handle that many outstanding RPCs
239
+ * <li>If the current number of channel falls outside of that range, add or remove at most
240
+ * {@link ChannelPoolSettings#MAX_RESIZE_DELTA} to get closer to middle of that range.
241
+ * </ul>
242
+ *
243
+ * <p>Not threadsafe, must be called under the entryWriteLock monitor
244
+ */
245
+ @ VisibleForTesting
246
+ void resize () {
247
+ List <Entry > localEntries = entries .get ();
248
+ // Estimate the peak of RPCs in the last interval by summing the peak of RPCs per channel
249
+ int actualOutstandingRpcs =
250
+ localEntries .stream ().mapToInt (Entry ::getAndResetMaxOutstanding ).sum ();
251
+
252
+ // Number of channels if each channel operated at max capacity
253
+ int minChannels =
254
+ (int ) Math .ceil (actualOutstandingRpcs / (double ) settings .getMaxRpcsPerChannel ());
255
+ // Limit the threshold to absolute range
256
+ if (minChannels < settings .getMinChannelCount ()) {
257
+ minChannels = settings .getMinChannelCount ();
258
+ }
259
+
260
+ // Number of channels if each channel operated at minimum capacity
261
+ // Note: getMinRpcsPerChannel() can return 0, but division by 0 shouldn't cause a problem.
262
+ int maxChannels =
263
+ (int ) Math .ceil (actualOutstandingRpcs / (double ) settings .getMinRpcsPerChannel ());
264
+ // Limit the threshold to absolute range
265
+ if (maxChannels > settings .getMaxChannelCount ()) {
266
+ maxChannels = settings .getMaxChannelCount ();
267
+ }
268
+ if (maxChannels < minChannels ) {
269
+ maxChannels = minChannels ;
270
+ }
271
+
272
+ // If the pool were to be resized, try to aim for the middle of the bound, but limit rate of
273
+ // change.
274
+ int tentativeTarget = (maxChannels + minChannels ) / 2 ;
275
+ int currentSize = localEntries .size ();
276
+ int delta = tentativeTarget - currentSize ;
277
+ int dampenedTarget = tentativeTarget ;
278
+ if (Math .abs (delta ) > ChannelPoolSettings .MAX_RESIZE_DELTA ) {
279
+ dampenedTarget =
280
+ currentSize + (int ) Math .copySign (ChannelPoolSettings .MAX_RESIZE_DELTA , delta );
281
+ }
282
+
283
+ // Only resize the pool when thresholds are crossed
284
+ if (localEntries .size () < minChannels ) {
285
+ LOG .fine (
286
+ String .format (
287
+ "Detected throughput peak of %d, expanding channel pool size: %d -> %d." ,
288
+ actualOutstandingRpcs , currentSize , dampenedTarget ));
289
+
290
+ expand (dampenedTarget );
291
+ } else if (localEntries .size () > maxChannels ) {
292
+ LOG .fine (
293
+ String .format (
294
+ "Detected throughput drop to %d, shrinking channel pool size: %d -> %d." ,
295
+ actualOutstandingRpcs , currentSize , dampenedTarget ));
296
+
297
+ shrink (dampenedTarget );
298
+ }
299
+ }
300
+
301
+ /** Not threadsafe, must be called under the entryWriteLock monitor */
302
+ private void shrink (int desiredSize ) {
303
+ ImmutableList <Entry > localEntries = entries .get ();
304
+ Preconditions .checkState (
305
+ localEntries .size () >= desiredSize , "current size is already smaller than the desired" );
306
+
307
+ // Set the new list
308
+ entries .set (localEntries .subList (0 , desiredSize ));
309
+ // clean up removed entries
310
+ List <Entry > removed = localEntries .subList (desiredSize , localEntries .size ());
311
+ removed .forEach (Entry ::requestShutdown );
312
+ }
313
+
314
+ /** Not threadsafe, must be called under the entryWriteLock monitor */
315
+ private void expand (int desiredSize ) {
316
+ List <Entry > localEntries = entries .get ();
317
+ Preconditions .checkState (
318
+ localEntries .size () <= desiredSize , "current size is already bigger than the desired" );
319
+
320
+ ImmutableList .Builder <Entry > newEntries = ImmutableList .<Entry >builder ().addAll (localEntries );
321
+
322
+ for (int i = 0 ; i < desiredSize - localEntries .size (); i ++) {
323
+ try {
324
+ newEntries .add (new Entry (channelFactory .createSingleChannel ()));
325
+ } catch (IOException e ) {
326
+ LOG .log (Level .WARNING , "Failed to add channel" , e );
327
+ }
328
+ }
329
+
330
+ entries .set (newEntries .build ());
331
+ }
332
+
333
+ private void refreshSafely () {
334
+ try {
335
+ refresh ();
336
+ } catch (Exception e ) {
337
+ LOG .log (Level .WARNING , "Failed to pre-emptively refresh channnels" , e );
338
+ }
271
339
}
272
340
273
341
/**
@@ -341,13 +409,15 @@ private Entry getEntry(int affinity) {
341
409
List <Entry > localEntries = entries .get ();
342
410
343
411
int index = Math .abs (affinity % localEntries .size ());
412
+
344
413
return localEntries .get (index );
345
414
}
346
415
347
416
/** Bundles a gRPC {@link ManagedChannel} with some usage accounting. */
348
417
private static class Entry {
349
418
private final ManagedChannel channel ;
350
419
private final AtomicInteger outstandingRpcs = new AtomicInteger (0 );
420
+ private final AtomicInteger maxOutstanding = new AtomicInteger ();
351
421
352
422
// Flag that the channel should be closed once all of the outstanding RPC complete.
353
423
private final AtomicBoolean shutdownRequested = new AtomicBoolean ();
@@ -358,6 +428,10 @@ private Entry(ManagedChannel channel) {
358
428
this .channel = channel ;
359
429
}
360
430
431
+ int getAndResetMaxOutstanding () {
432
+ return maxOutstanding .getAndSet (outstandingRpcs .get ());
433
+ }
434
+
361
435
/**
362
436
* Try to increment the outstanding RPC count. The method will return false if the channel is
363
437
* closing and the caller should pick a different channel. If the method returned true, the
@@ -366,7 +440,13 @@ private Entry(ManagedChannel channel) {
366
440
*/
367
441
private boolean retain () {
368
442
// register desire to start RPC
369
- outstandingRpcs .incrementAndGet ();
443
+ int currentOutstanding = outstandingRpcs .incrementAndGet ();
444
+
445
+ // Rough book keeping
446
+ int prevMax = maxOutstanding .get ();
447
+ if (currentOutstanding > prevMax ) {
448
+ maxOutstanding .incrementAndGet ();
449
+ }
370
450
371
451
// abort if the channel is closing
372
452
if (shutdownRequested .get ()) {
0 commit comments