1 /* 2 * Copyright 2014 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.netty; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkState; 21 import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIMEOUT_NANOS; 22 import static io.grpc.internal.GrpcUtil.DEFAULT_KEEPALIVE_TIME_NANOS; 23 import static io.grpc.internal.GrpcUtil.KEEPALIVE_TIME_NANOS_DISABLED; 24 25 import com.google.common.annotations.VisibleForTesting; 26 import com.google.common.base.Preconditions; 27 import com.google.errorprone.annotations.CanIgnoreReturnValue; 28 import io.grpc.Attributes; 29 import io.grpc.EquivalentAddressGroup; 30 import io.grpc.ExperimentalApi; 31 import io.grpc.Internal; 32 import io.grpc.NameResolver; 33 import io.grpc.internal.AbstractManagedChannelImplBuilder; 34 import io.grpc.internal.AtomicBackoff; 35 import io.grpc.internal.ClientTransportFactory; 36 import io.grpc.internal.ConnectionClientTransport; 37 import io.grpc.internal.GrpcUtil; 38 import io.grpc.internal.KeepAliveManager; 39 import io.grpc.internal.ProxyParameters; 40 import io.grpc.internal.SharedResourceHolder; 41 import io.grpc.internal.TransportTracer; 42 import io.netty.channel.Channel; 43 import io.netty.channel.ChannelOption; 44 import io.netty.channel.EventLoopGroup; 45 import io.netty.channel.socket.nio.NioSocketChannel; 46 import io.netty.handler.ssl.SslContext; 47 import java.net.InetSocketAddress; 48 import java.net.SocketAddress; 49 import java.util.HashMap; 50 import java.util.Map; 51 import java.util.concurrent.ScheduledExecutorService; 52 import java.util.concurrent.TimeUnit; 53 import javax.annotation.CheckReturnValue; 54 import javax.annotation.Nullable; 55 import javax.net.ssl.SSLException; 56 57 /** 58 * A builder to help simplify construction of channels using the Netty transport. 59 */ 60 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1784") 61 @CanIgnoreReturnValue 62 public final class NettyChannelBuilder 63 extends AbstractManagedChannelImplBuilder<NettyChannelBuilder> { 64 public static final int DEFAULT_FLOW_CONTROL_WINDOW = 1048576; // 1MiB 65 66 private static final long AS_LARGE_AS_INFINITE = TimeUnit.DAYS.toNanos(1000L); 67 68 private final Map<ChannelOption<?>, Object> channelOptions = 69 new HashMap<ChannelOption<?>, Object>(); 70 71 private NegotiationType negotiationType = NegotiationType.TLS; 72 private OverrideAuthorityChecker authorityChecker; 73 private Class<? extends Channel> channelType = NioSocketChannel.class; 74 75 @Nullable 76 private EventLoopGroup eventLoopGroup; 77 private SslContext sslContext; 78 private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW; 79 private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; 80 private long keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED; 81 private long keepAliveTimeoutNanos = DEFAULT_KEEPALIVE_TIMEOUT_NANOS; 82 private boolean keepAliveWithoutCalls; 83 private ProtocolNegotiatorFactory protocolNegotiatorFactory; 84 private LocalSocketPicker localSocketPicker; 85 86 /** 87 * Creates a new builder with the given server address. This factory method is primarily intended 88 * for using Netty Channel types other than SocketChannel. {@link #forAddress(String, int)} should 89 * generally be preferred over this method, since that API permits delaying DNS lookups and 90 * noticing changes to DNS. 91 */ 92 @CheckReturnValue forAddress(SocketAddress serverAddress)93 public static NettyChannelBuilder forAddress(SocketAddress serverAddress) { 94 return new NettyChannelBuilder(serverAddress); 95 } 96 97 /** 98 * Creates a new builder with the given host and port. 99 */ 100 @CheckReturnValue forAddress(String host, int port)101 public static NettyChannelBuilder forAddress(String host, int port) { 102 return new NettyChannelBuilder(host, port); 103 } 104 105 /** 106 * Creates a new builder with the given target string that will be resolved by 107 * {@link io.grpc.NameResolver}. 108 */ 109 @CheckReturnValue forTarget(String target)110 public static NettyChannelBuilder forTarget(String target) { 111 return new NettyChannelBuilder(target); 112 } 113 114 @CheckReturnValue NettyChannelBuilder(String host, int port)115 NettyChannelBuilder(String host, int port) { 116 this(GrpcUtil.authorityFromHostAndPort(host, port)); 117 } 118 119 @CheckReturnValue NettyChannelBuilder(String target)120 NettyChannelBuilder(String target) { 121 super(target); 122 } 123 124 @CheckReturnValue NettyChannelBuilder(SocketAddress address)125 NettyChannelBuilder(SocketAddress address) { 126 super(address, getAuthorityFromAddress(address)); 127 } 128 129 @CheckReturnValue getAuthorityFromAddress(SocketAddress address)130 private static String getAuthorityFromAddress(SocketAddress address) { 131 if (address instanceof InetSocketAddress) { 132 InetSocketAddress inetAddress = (InetSocketAddress) address; 133 return GrpcUtil.authorityFromHostAndPort(inetAddress.getHostString(), inetAddress.getPort()); 134 } else { 135 return address.toString(); 136 } 137 } 138 139 /** 140 * Specifies the channel type to use, by default we use {@link NioSocketChannel}. 141 */ channelType(Class<? extends Channel> channelType)142 public NettyChannelBuilder channelType(Class<? extends Channel> channelType) { 143 this.channelType = Preconditions.checkNotNull(channelType, "channelType"); 144 return this; 145 } 146 147 /** 148 * Specifies a channel option. As the underlying channel as well as network implementation may 149 * ignore this value applications should consider it a hint. 150 */ withOption(ChannelOption<T> option, T value)151 public <T> NettyChannelBuilder withOption(ChannelOption<T> option, T value) { 152 channelOptions.put(option, value); 153 return this; 154 } 155 156 /** 157 * Sets the negotiation type for the HTTP/2 connection. 158 * 159 * <p>Default: <code>TLS</code> 160 */ negotiationType(NegotiationType type)161 public NettyChannelBuilder negotiationType(NegotiationType type) { 162 negotiationType = type; 163 return this; 164 } 165 166 /** 167 * Provides an EventGroupLoop to be used by the netty transport. 168 * 169 * <p>It's an optional parameter. If the user has not provided an EventGroupLoop when the channel 170 * is built, the builder will use the default one which is static. 171 * 172 * <p>The channel won't take ownership of the given EventLoopGroup. It's caller's responsibility 173 * to shut it down when it's desired. 174 */ eventLoopGroup(@ullable EventLoopGroup eventLoopGroup)175 public NettyChannelBuilder eventLoopGroup(@Nullable EventLoopGroup eventLoopGroup) { 176 this.eventLoopGroup = eventLoopGroup; 177 return this; 178 } 179 180 /** 181 * SSL/TLS context to use instead of the system default. It must have been configured with {@link 182 * GrpcSslContexts}, but options could have been overridden. 183 */ sslContext(SslContext sslContext)184 public NettyChannelBuilder sslContext(SslContext sslContext) { 185 if (sslContext != null) { 186 checkArgument(sslContext.isClient(), 187 "Server SSL context can not be used for client channel"); 188 GrpcSslContexts.ensureAlpnAndH2Enabled(sslContext.applicationProtocolNegotiator()); 189 } 190 this.sslContext = sslContext; 191 return this; 192 } 193 194 /** 195 * Sets the flow control window in bytes. If not called, the default value 196 * is {@link #DEFAULT_FLOW_CONTROL_WINDOW}). 197 */ flowControlWindow(int flowControlWindow)198 public NettyChannelBuilder flowControlWindow(int flowControlWindow) { 199 checkArgument(flowControlWindow > 0, "flowControlWindow must be positive"); 200 this.flowControlWindow = flowControlWindow; 201 return this; 202 } 203 204 /** 205 * Sets the max message size. 206 * 207 * @deprecated Use {@link #maxInboundMessageSize} instead 208 */ 209 @Deprecated maxMessageSize(int maxMessageSize)210 public NettyChannelBuilder maxMessageSize(int maxMessageSize) { 211 maxInboundMessageSize(maxMessageSize); 212 return this; 213 } 214 215 /** 216 * Sets the maximum size of header list allowed to be received. This is cumulative size of the 217 * headers with some overhead, as defined for 218 * <a href="http://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2"> 219 * HTTP/2's SETTINGS_MAX_HEADER_LIST_SIZE</a>. The default is 8 KiB. 220 */ maxHeaderListSize(int maxHeaderListSize)221 public NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) { 222 checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be > 0"); 223 this.maxHeaderListSize = maxHeaderListSize; 224 return this; 225 } 226 227 /** 228 * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT} or 229 * {@code PLAINTEXT_UPGRADE}. 230 * 231 * @deprecated use {@link #usePlaintext()} instead. 232 */ 233 @Override 234 @Deprecated usePlaintext(boolean skipNegotiation)235 public NettyChannelBuilder usePlaintext(boolean skipNegotiation) { 236 if (skipNegotiation) { 237 negotiationType(NegotiationType.PLAINTEXT); 238 } else { 239 negotiationType(NegotiationType.PLAINTEXT_UPGRADE); 240 } 241 return this; 242 } 243 244 /** 245 * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code PLAINTEXT}. 246 */ 247 @Override usePlaintext()248 public NettyChannelBuilder usePlaintext() { 249 negotiationType(NegotiationType.PLAINTEXT); 250 return this; 251 } 252 253 /** 254 * Equivalent to using {@link #negotiationType(NegotiationType)} with {@code TLS}. 255 */ 256 @Override useTransportSecurity()257 public NettyChannelBuilder useTransportSecurity() { 258 negotiationType(NegotiationType.TLS); 259 return this; 260 } 261 262 /** 263 * Enable keepalive with default delay and timeout. 264 * 265 * @deprecated Please use {@link #keepAliveTime} and {@link #keepAliveTimeout} instead 266 */ 267 @Deprecated enableKeepAlive(boolean enable)268 public final NettyChannelBuilder enableKeepAlive(boolean enable) { 269 if (enable) { 270 return keepAliveTime(DEFAULT_KEEPALIVE_TIME_NANOS, TimeUnit.NANOSECONDS); 271 } 272 return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS); 273 } 274 275 /** 276 * Enable keepalive with custom delay and timeout. 277 * 278 * @deprecated Please use {@link #keepAliveTime} and {@link #keepAliveTimeout} instead 279 */ 280 @Deprecated enableKeepAlive(boolean enable, long keepAliveTime, TimeUnit delayUnit, long keepAliveTimeout, TimeUnit timeoutUnit)281 public final NettyChannelBuilder enableKeepAlive(boolean enable, long keepAliveTime, 282 TimeUnit delayUnit, long keepAliveTimeout, TimeUnit timeoutUnit) { 283 if (enable) { 284 return keepAliveTime(keepAliveTime, delayUnit) 285 .keepAliveTimeout(keepAliveTimeout, timeoutUnit); 286 } 287 return keepAliveTime(KEEPALIVE_TIME_NANOS_DISABLED, TimeUnit.NANOSECONDS); 288 } 289 290 /** 291 * {@inheritDoc} 292 * 293 * @since 1.3.0 294 */ 295 @Override keepAliveTime(long keepAliveTime, TimeUnit timeUnit)296 public NettyChannelBuilder keepAliveTime(long keepAliveTime, TimeUnit timeUnit) { 297 checkArgument(keepAliveTime > 0L, "keepalive time must be positive"); 298 keepAliveTimeNanos = timeUnit.toNanos(keepAliveTime); 299 keepAliveTimeNanos = KeepAliveManager.clampKeepAliveTimeInNanos(keepAliveTimeNanos); 300 if (keepAliveTimeNanos >= AS_LARGE_AS_INFINITE) { 301 // Bump keepalive time to infinite. This disables keepalive. 302 keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED; 303 } 304 return this; 305 } 306 307 /** 308 * {@inheritDoc} 309 * 310 * @since 1.3.0 311 */ 312 @Override keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit)313 public NettyChannelBuilder keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) { 314 checkArgument(keepAliveTimeout > 0L, "keepalive timeout must be positive"); 315 keepAliveTimeoutNanos = timeUnit.toNanos(keepAliveTimeout); 316 keepAliveTimeoutNanos = KeepAliveManager.clampKeepAliveTimeoutInNanos(keepAliveTimeoutNanos); 317 return this; 318 } 319 320 /** 321 * {@inheritDoc} 322 * 323 * @since 1.3.0 324 */ 325 @Override keepAliveWithoutCalls(boolean enable)326 public NettyChannelBuilder keepAliveWithoutCalls(boolean enable) { 327 keepAliveWithoutCalls = enable; 328 return this; 329 } 330 331 332 /** 333 * If non-{@code null}, attempts to create connections bound to a local port. 334 */ localSocketPicker(@ullable LocalSocketPicker localSocketPicker)335 public NettyChannelBuilder localSocketPicker(@Nullable LocalSocketPicker localSocketPicker) { 336 this.localSocketPicker = localSocketPicker; 337 return this; 338 } 339 340 /** 341 * This class is meant to be overriden with a custom implementation of 342 * {@link #createSocketAddress}. The default implementation is a no-op. 343 * 344 * @since 1.16.0 345 */ 346 @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4917") 347 public static class LocalSocketPicker { 348 349 /** 350 * Called by gRPC to pick local socket to bind to. This may be called multiple times. 351 * Subclasses are expected to override this method. 352 * 353 * @param remoteAddress the remote address to connect to. 354 * @param attrs the Attributes present on the {@link io.grpc.EquivalentAddressGroup} associated 355 * with the address. 356 * @return a {@link SocketAddress} suitable for binding, or else {@code null}. 357 * @since 1.16.0 358 */ 359 @Nullable createSocketAddress( SocketAddress remoteAddress, @EquivalentAddressGroup.Attr Attributes attrs)360 public SocketAddress createSocketAddress( 361 SocketAddress remoteAddress, @EquivalentAddressGroup.Attr Attributes attrs) { 362 return null; 363 } 364 } 365 366 @Override 367 @CheckReturnValue 368 @Internal buildTransportFactory()369 protected ClientTransportFactory buildTransportFactory() { 370 ProtocolNegotiator negotiator; 371 if (protocolNegotiatorFactory != null) { 372 negotiator = protocolNegotiatorFactory.buildProtocolNegotiator(); 373 } else { 374 SslContext localSslContext = sslContext; 375 if (negotiationType == NegotiationType.TLS && localSslContext == null) { 376 try { 377 localSslContext = GrpcSslContexts.forClient().build(); 378 } catch (SSLException ex) { 379 throw new RuntimeException(ex); 380 } 381 } 382 negotiator = createProtocolNegotiatorByType(negotiationType, localSslContext); 383 } 384 return new NettyTransportFactory( 385 negotiator, channelType, channelOptions, 386 eventLoopGroup, flowControlWindow, maxInboundMessageSize(), 387 maxHeaderListSize, keepAliveTimeNanos, keepAliveTimeoutNanos, keepAliveWithoutCalls, 388 transportTracerFactory.create(), localSocketPicker); 389 } 390 391 @Override 392 @CheckReturnValue getNameResolverParams()393 protected Attributes getNameResolverParams() { 394 int defaultPort; 395 switch (negotiationType) { 396 case PLAINTEXT: 397 case PLAINTEXT_UPGRADE: 398 defaultPort = GrpcUtil.DEFAULT_PORT_PLAINTEXT; 399 break; 400 case TLS: 401 defaultPort = GrpcUtil.DEFAULT_PORT_SSL; 402 break; 403 default: 404 throw new AssertionError(negotiationType + " not handled"); 405 } 406 return Attributes.newBuilder() 407 .set(NameResolver.Factory.PARAMS_DEFAULT_PORT, defaultPort).build(); 408 } 409 overrideAuthorityChecker(@ullable OverrideAuthorityChecker authorityChecker)410 void overrideAuthorityChecker(@Nullable OverrideAuthorityChecker authorityChecker) { 411 this.authorityChecker = authorityChecker; 412 } 413 414 @VisibleForTesting 415 @CheckReturnValue createProtocolNegotiatorByType( NegotiationType negotiationType, SslContext sslContext)416 static ProtocolNegotiator createProtocolNegotiatorByType( 417 NegotiationType negotiationType, 418 SslContext sslContext) { 419 switch (negotiationType) { 420 case PLAINTEXT: 421 return ProtocolNegotiators.plaintext(); 422 case PLAINTEXT_UPGRADE: 423 return ProtocolNegotiators.plaintextUpgrade(); 424 case TLS: 425 return ProtocolNegotiators.tls(sslContext); 426 default: 427 throw new IllegalArgumentException("Unsupported negotiationType: " + negotiationType); 428 } 429 } 430 431 @CheckReturnValue 432 interface OverrideAuthorityChecker { checkAuthority(String authority)433 String checkAuthority(String authority); 434 } 435 436 @Override 437 @CheckReturnValue 438 @Internal checkAuthority(String authority)439 protected String checkAuthority(String authority) { 440 if (authorityChecker != null) { 441 return authorityChecker.checkAuthority(authority); 442 } 443 return super.checkAuthority(authority); 444 } 445 protocolNegotiatorFactory(ProtocolNegotiatorFactory protocolNegotiatorFactory)446 void protocolNegotiatorFactory(ProtocolNegotiatorFactory protocolNegotiatorFactory) { 447 this.protocolNegotiatorFactory 448 = Preconditions.checkNotNull(protocolNegotiatorFactory, "protocolNegotiatorFactory"); 449 } 450 451 @Override setTracingEnabled(boolean value)452 protected void setTracingEnabled(boolean value) { 453 super.setTracingEnabled(value); 454 } 455 456 @Override setStatsEnabled(boolean value)457 protected void setStatsEnabled(boolean value) { 458 super.setStatsEnabled(value); 459 } 460 461 @Override setStatsRecordStartedRpcs(boolean value)462 protected void setStatsRecordStartedRpcs(boolean value) { 463 super.setStatsRecordStartedRpcs(value); 464 } 465 466 @VisibleForTesting setTransportTracerFactory(TransportTracer.Factory transportTracerFactory)467 NettyChannelBuilder setTransportTracerFactory(TransportTracer.Factory transportTracerFactory) { 468 this.transportTracerFactory = transportTracerFactory; 469 return this; 470 } 471 472 interface ProtocolNegotiatorFactory { 473 /** 474 * Returns a ProtocolNegotatior instance configured for this Builder. This method is called 475 * during {@code ManagedChannelBuilder#build()}. 476 */ buildProtocolNegotiator()477 ProtocolNegotiator buildProtocolNegotiator(); 478 } 479 480 /** 481 * Creates Netty transports. Exposed for internal use, as it should be private. 482 */ 483 @CheckReturnValue 484 private static final class NettyTransportFactory implements ClientTransportFactory { 485 private final ProtocolNegotiator protocolNegotiator; 486 private final Class<? extends Channel> channelType; 487 private final Map<ChannelOption<?>, ?> channelOptions; 488 private final EventLoopGroup group; 489 private final boolean usingSharedGroup; 490 private final int flowControlWindow; 491 private final int maxMessageSize; 492 private final int maxHeaderListSize; 493 private final AtomicBackoff keepAliveTimeNanos; 494 private final long keepAliveTimeoutNanos; 495 private final boolean keepAliveWithoutCalls; 496 private final TransportTracer transportTracer; 497 private final LocalSocketPicker localSocketPicker; 498 499 private boolean closed; 500 NettyTransportFactory(ProtocolNegotiator protocolNegotiator, Class<? extends Channel> channelType, Map<ChannelOption<?>, ?> channelOptions, EventLoopGroup group, int flowControlWindow, int maxMessageSize, int maxHeaderListSize, long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls, TransportTracer transportTracer, LocalSocketPicker localSocketPicker)501 NettyTransportFactory(ProtocolNegotiator protocolNegotiator, 502 Class<? extends Channel> channelType, Map<ChannelOption<?>, ?> channelOptions, 503 EventLoopGroup group, int flowControlWindow, int maxMessageSize, int maxHeaderListSize, 504 long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls, 505 TransportTracer transportTracer, LocalSocketPicker localSocketPicker) { 506 this.protocolNegotiator = protocolNegotiator; 507 this.channelType = channelType; 508 this.channelOptions = new HashMap<ChannelOption<?>, Object>(channelOptions); 509 this.flowControlWindow = flowControlWindow; 510 this.maxMessageSize = maxMessageSize; 511 this.maxHeaderListSize = maxHeaderListSize; 512 this.keepAliveTimeNanos = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos); 513 this.keepAliveTimeoutNanos = keepAliveTimeoutNanos; 514 this.keepAliveWithoutCalls = keepAliveWithoutCalls; 515 this.transportTracer = transportTracer; 516 this.localSocketPicker = 517 localSocketPicker != null ? localSocketPicker : new LocalSocketPicker(); 518 519 usingSharedGroup = group == null; 520 if (usingSharedGroup) { 521 // The group was unspecified, using the shared group. 522 this.group = SharedResourceHolder.get(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP); 523 } else { 524 this.group = group; 525 } 526 } 527 528 @Override newClientTransport( SocketAddress serverAddress, ClientTransportOptions options)529 public ConnectionClientTransport newClientTransport( 530 SocketAddress serverAddress, ClientTransportOptions options) { 531 checkState(!closed, "The transport factory is closed."); 532 533 ProtocolNegotiator localNegotiator = protocolNegotiator; 534 ProxyParameters proxyParams = options.getProxyParameters(); 535 if (proxyParams != null) { 536 localNegotiator = ProtocolNegotiators.httpProxy( 537 proxyParams.proxyAddress, proxyParams.username, proxyParams.password, 538 protocolNegotiator); 539 } 540 541 final AtomicBackoff.State keepAliveTimeNanosState = keepAliveTimeNanos.getState(); 542 Runnable tooManyPingsRunnable = new Runnable() { 543 @Override 544 public void run() { 545 keepAliveTimeNanosState.backoff(); 546 } 547 }; 548 549 NettyClientTransport transport = new NettyClientTransport( 550 serverAddress, channelType, channelOptions, group, 551 localNegotiator, flowControlWindow, 552 maxMessageSize, maxHeaderListSize, keepAliveTimeNanosState.get(), keepAliveTimeoutNanos, 553 keepAliveWithoutCalls, options.getAuthority(), options.getUserAgent(), 554 tooManyPingsRunnable, transportTracer, options.getEagAttributes(), 555 localSocketPicker); 556 return transport; 557 } 558 559 @Override getScheduledExecutorService()560 public ScheduledExecutorService getScheduledExecutorService() { 561 return group; 562 } 563 564 @Override close()565 public void close() { 566 if (closed) { 567 return; 568 } 569 closed = true; 570 571 protocolNegotiator.close(); 572 if (usingSharedGroup) { 573 SharedResourceHolder.release(Utils.DEFAULT_WORKER_EVENT_LOOP_GROUP, group); 574 } 575 } 576 } 577 } 578