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.checkState; 20 import static io.grpc.internal.GrpcUtil.CONTENT_TYPE_KEY; 21 import static io.grpc.internal.TransportFrameUtil.toHttp2Headers; 22 import static io.grpc.internal.TransportFrameUtil.toRawSerializedHeaders; 23 import static io.netty.channel.ChannelOption.SO_LINGER; 24 import static io.netty.channel.ChannelOption.SO_TIMEOUT; 25 import static io.netty.util.CharsetUtil.UTF_8; 26 27 import com.google.common.annotations.VisibleForTesting; 28 import com.google.common.base.Preconditions; 29 import io.grpc.InternalChannelz; 30 import io.grpc.InternalMetadata; 31 import io.grpc.Metadata; 32 import io.grpc.Status; 33 import io.grpc.internal.GrpcUtil; 34 import io.grpc.internal.SharedResourceHolder.Resource; 35 import io.grpc.internal.TransportTracer; 36 import io.grpc.netty.GrpcHttp2HeadersUtils.GrpcHttp2InboundHeaders; 37 import io.grpc.netty.NettySocketSupport.NativeSocketOptions; 38 import io.netty.buffer.ByteBufAllocator; 39 import io.netty.buffer.PooledByteBufAllocator; 40 import io.netty.channel.Channel; 41 import io.netty.channel.ChannelConfig; 42 import io.netty.channel.ChannelFactory; 43 import io.netty.channel.ChannelOption; 44 import io.netty.channel.EventLoopGroup; 45 import io.netty.channel.ReflectiveChannelFactory; 46 import io.netty.channel.ServerChannel; 47 import io.netty.channel.nio.NioEventLoopGroup; 48 import io.netty.channel.socket.nio.NioServerSocketChannel; 49 import io.netty.channel.socket.nio.NioSocketChannel; 50 import io.netty.handler.codec.DecoderException; 51 import io.netty.handler.codec.http2.Http2Connection; 52 import io.netty.handler.codec.http2.Http2Exception; 53 import io.netty.handler.codec.http2.Http2FlowController; 54 import io.netty.handler.codec.http2.Http2Headers; 55 import io.netty.handler.codec.http2.Http2Stream; 56 import io.netty.util.AsciiString; 57 import io.netty.util.NettyRuntime; 58 import io.netty.util.concurrent.DefaultThreadFactory; 59 import java.io.IOException; 60 import java.lang.reflect.Constructor; 61 import java.nio.channels.ClosedChannelException; 62 import java.nio.channels.UnresolvedAddressException; 63 import java.util.Map; 64 import java.util.concurrent.ThreadFactory; 65 import java.util.concurrent.TimeUnit; 66 import java.util.logging.Level; 67 import java.util.logging.Logger; 68 import javax.annotation.CheckReturnValue; 69 import javax.annotation.Nullable; 70 import javax.net.ssl.SSLException; 71 72 /** 73 * Common utility methods. 74 */ 75 class Utils { 76 private static final Logger logger = Logger.getLogger(Utils.class.getName()); 77 78 public static final AsciiString STATUS_OK = AsciiString.of("200"); 79 public static final AsciiString HTTP_METHOD = AsciiString.of(GrpcUtil.HTTP_METHOD); 80 public static final AsciiString HTTP_GET_METHOD = AsciiString.of("GET"); 81 public static final AsciiString HTTPS = AsciiString.of("https"); 82 public static final AsciiString HTTP = AsciiString.of("http"); 83 public static final AsciiString CONTENT_TYPE_HEADER = AsciiString.of(CONTENT_TYPE_KEY.name()); 84 public static final AsciiString CONTENT_TYPE_GRPC = AsciiString.of(GrpcUtil.CONTENT_TYPE_GRPC); 85 public static final AsciiString TE_HEADER = AsciiString.of(GrpcUtil.TE_HEADER.name()); 86 public static final AsciiString TE_TRAILERS = AsciiString.of(GrpcUtil.TE_TRAILERS); 87 public static final AsciiString USER_AGENT = AsciiString.of(GrpcUtil.USER_AGENT_KEY.name()); 88 public static final Resource<EventLoopGroup> NIO_BOSS_EVENT_LOOP_GROUP 89 = new DefaultEventLoopGroupResource(1, "grpc-nio-boss-ELG", EventLoopGroupType.NIO); 90 public static final Resource<EventLoopGroup> NIO_WORKER_EVENT_LOOP_GROUP 91 = new DefaultEventLoopGroupResource(0, "grpc-nio-worker-ELG", EventLoopGroupType.NIO); 92 93 public static final Resource<EventLoopGroup> DEFAULT_BOSS_EVENT_LOOP_GROUP; 94 public static final Resource<EventLoopGroup> DEFAULT_WORKER_EVENT_LOOP_GROUP; 95 96 // This class is initialized on first use, thus provides delayed allocator creation. 97 private static final class ByteBufAllocatorPreferDirectHolder { 98 private static final ByteBufAllocator allocator = createByteBufAllocator(true); 99 } 100 101 // This class is initialized on first use, thus provides delayed allocator creation. 102 private static final class ByteBufAllocatorPreferHeapHolder { 103 private static final ByteBufAllocator allocator = createByteBufAllocator(false); 104 } 105 106 public static final ChannelFactory<? extends ServerChannel> DEFAULT_SERVER_CHANNEL_FACTORY; 107 public static final Class<? extends Channel> DEFAULT_CLIENT_CHANNEL_TYPE; 108 public static final Class<? extends Channel> EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE; 109 110 @Nullable 111 private static final Constructor<? extends EventLoopGroup> EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR; 112 113 static { 114 // Decide default channel types and EventLoopGroup based on Epoll availability 115 if (isEpollAvailable()) { 116 DEFAULT_CLIENT_CHANNEL_TYPE = epollChannelType(); 117 EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = epollDomainSocketChannelType(); 118 DEFAULT_SERVER_CHANNEL_FACTORY = new ReflectiveChannelFactory<>(epollServerChannelType()); 119 EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = epollEventLoopGroupConstructor(); 120 DEFAULT_BOSS_EVENT_LOOP_GROUP 121 = new DefaultEventLoopGroupResource(1, "grpc-default-boss-ELG", EventLoopGroupType.EPOLL); 122 DEFAULT_WORKER_EVENT_LOOP_GROUP 123 = new DefaultEventLoopGroupResource(0,"grpc-default-worker-ELG", EventLoopGroupType.EPOLL); 124 } else { logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause())125 logger.log(Level.FINE, "Epoll is not available, using Nio.", getEpollUnavailabilityCause()); 126 DEFAULT_SERVER_CHANNEL_FACTORY = nioServerChannelFactory(); 127 DEFAULT_CLIENT_CHANNEL_TYPE = NioSocketChannel.class; 128 EPOLL_DOMAIN_CLIENT_CHANNEL_TYPE = null; 129 DEFAULT_BOSS_EVENT_LOOP_GROUP = NIO_BOSS_EVENT_LOOP_GROUP; 130 DEFAULT_WORKER_EVENT_LOOP_GROUP = NIO_WORKER_EVENT_LOOP_GROUP; 131 EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = null; 132 } 133 } 134 getByteBufAllocator(boolean forceHeapBuffer)135 public static ByteBufAllocator getByteBufAllocator(boolean forceHeapBuffer) { 136 if (Boolean.parseBoolean( 137 System.getProperty("io.grpc.netty.useCustomAllocator", "true"))) { 138 boolean defaultPreferDirect = PooledByteBufAllocator.defaultPreferDirect(); 139 logger.log( 140 Level.FINE, 141 String.format( 142 "Using custom allocator: forceHeapBuffer=%s, defaultPreferDirect=%s", 143 forceHeapBuffer, 144 defaultPreferDirect)); 145 if (forceHeapBuffer || !defaultPreferDirect) { 146 return ByteBufAllocatorPreferHeapHolder.allocator; 147 } else { 148 return ByteBufAllocatorPreferDirectHolder.allocator; 149 } 150 } else { 151 logger.log(Level.FINE, "Using default allocator"); 152 return ByteBufAllocator.DEFAULT; 153 } 154 } 155 createByteBufAllocator(boolean preferDirect)156 private static ByteBufAllocator createByteBufAllocator(boolean preferDirect) { 157 int maxOrder; 158 logger.log(Level.FINE, "Creating allocator, preferDirect=" + preferDirect); 159 if (System.getProperty("io.netty.allocator.maxOrder") == null) { 160 // See the implementation of PooledByteBufAllocator. DEFAULT_MAX_ORDER in there is 161 // 11, which makes chunk size to be 8192 << 11 = 16 MiB. We want the chunk size to be 162 // 2MiB, thus reducing the maxOrder to 8. 163 maxOrder = 8; 164 logger.log(Level.FINE, "Forcing maxOrder=" + maxOrder); 165 } else { 166 maxOrder = PooledByteBufAllocator.defaultMaxOrder(); 167 logger.log(Level.FINE, "Using default maxOrder=" + maxOrder); 168 } 169 return new PooledByteBufAllocator( 170 preferDirect, 171 PooledByteBufAllocator.defaultNumHeapArena(), 172 // Assuming neither gRPC nor netty are using allocator.directBuffer() to request 173 // specifically for direct buffers, which is true as I just checked, setting arenas to 0 174 // will make sure no direct buffer is ever created. 175 preferDirect ? PooledByteBufAllocator.defaultNumDirectArena() : 0, 176 PooledByteBufAllocator.defaultPageSize(), 177 maxOrder, 178 PooledByteBufAllocator.defaultSmallCacheSize(), 179 PooledByteBufAllocator.defaultNormalCacheSize(), 180 PooledByteBufAllocator.defaultUseCacheForAllThreads()); 181 } 182 convertHeaders(Http2Headers http2Headers)183 public static Metadata convertHeaders(Http2Headers http2Headers) { 184 if (http2Headers instanceof GrpcHttp2InboundHeaders) { 185 GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers; 186 return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues()); 187 } 188 return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers)); 189 } 190 191 @CheckReturnValue convertHeadersToArray(Http2Headers http2Headers)192 private static byte[][] convertHeadersToArray(Http2Headers http2Headers) { 193 // The Netty AsciiString class is really just a wrapper around a byte[] and supports 194 // arbitrary binary data, not just ASCII. 195 byte[][] headerValues = new byte[http2Headers.size() * 2][]; 196 int i = 0; 197 for (Map.Entry<CharSequence, CharSequence> entry : http2Headers) { 198 headerValues[i++] = bytes(entry.getKey()); 199 headerValues[i++] = bytes(entry.getValue()); 200 } 201 return toRawSerializedHeaders(headerValues); 202 } 203 bytes(CharSequence seq)204 private static byte[] bytes(CharSequence seq) { 205 if (seq instanceof AsciiString) { 206 // Fast path - sometimes copy. 207 AsciiString str = (AsciiString) seq; 208 return str.isEntireArrayUsed() ? str.array() : str.toByteArray(); 209 } 210 // Slow path - copy. 211 return seq.toString().getBytes(UTF_8); 212 } 213 convertClientHeaders(Metadata headers, AsciiString scheme, AsciiString defaultPath, AsciiString authority, AsciiString method, AsciiString userAgent)214 public static Http2Headers convertClientHeaders(Metadata headers, 215 AsciiString scheme, 216 AsciiString defaultPath, 217 AsciiString authority, 218 AsciiString method, 219 AsciiString userAgent) { 220 Preconditions.checkNotNull(defaultPath, "defaultPath"); 221 Preconditions.checkNotNull(authority, "authority"); 222 Preconditions.checkNotNull(method, "method"); 223 224 // Discard any application supplied duplicates of the reserved headers 225 headers.discardAll(CONTENT_TYPE_KEY); 226 headers.discardAll(GrpcUtil.TE_HEADER); 227 headers.discardAll(GrpcUtil.USER_AGENT_KEY); 228 229 return GrpcHttp2OutboundHeaders.clientRequestHeaders( 230 toHttp2Headers(headers), 231 authority, 232 defaultPath, 233 method, 234 scheme, 235 userAgent); 236 } 237 convertServerHeaders(Metadata headers)238 public static Http2Headers convertServerHeaders(Metadata headers) { 239 // Discard any application supplied duplicates of the reserved headers 240 headers.discardAll(CONTENT_TYPE_KEY); 241 headers.discardAll(GrpcUtil.TE_HEADER); 242 headers.discardAll(GrpcUtil.USER_AGENT_KEY); 243 244 return GrpcHttp2OutboundHeaders.serverResponseHeaders(toHttp2Headers(headers)); 245 } 246 convertTrailers(Http2Headers http2Headers)247 public static Metadata convertTrailers(Http2Headers http2Headers) { 248 if (http2Headers instanceof GrpcHttp2InboundHeaders) { 249 GrpcHttp2InboundHeaders h = (GrpcHttp2InboundHeaders) http2Headers; 250 return InternalMetadata.newMetadata(h.numHeaders(), h.namesAndValues()); 251 } 252 return InternalMetadata.newMetadata(convertHeadersToArray(http2Headers)); 253 } 254 convertTrailers(Metadata trailers, boolean headersSent)255 public static Http2Headers convertTrailers(Metadata trailers, boolean headersSent) { 256 if (!headersSent) { 257 return convertServerHeaders(trailers); 258 } 259 return GrpcHttp2OutboundHeaders.serverResponseTrailers(toHttp2Headers(trailers)); 260 } 261 statusFromThrowable(Throwable t)262 public static Status statusFromThrowable(Throwable t) { 263 Status s = Status.fromThrowable(t); 264 if (s.getCode() != Status.Code.UNKNOWN) { 265 return s; 266 } 267 if (t instanceof ClosedChannelException) { 268 // ClosedChannelException is used any time the Netty channel is closed. Proper error 269 // processing requires remembering the error that occurred before this one and using it 270 // instead. 271 // 272 // Netty uses an exception that has no stack trace, while we would never hope to show this to 273 // users, if it happens having the extra information may provide a small hint of where to 274 // look. 275 ClosedChannelException extraT = new ClosedChannelException(); 276 extraT.initCause(t); 277 return Status.UNKNOWN.withDescription("channel closed").withCause(extraT); 278 } 279 if (t instanceof DecoderException && t.getCause() instanceof SSLException) { 280 return Status.UNAVAILABLE.withDescription("ssl exception").withCause(t); 281 } 282 if (t instanceof IOException) { 283 return Status.UNAVAILABLE.withDescription("io exception").withCause(t); 284 } 285 if (t instanceof UnresolvedAddressException) { 286 return Status.UNAVAILABLE.withDescription("unresolved address").withCause(t); 287 } 288 if (t instanceof Http2Exception) { 289 return Status.INTERNAL.withDescription("http2 exception").withCause(t); 290 } 291 return s; 292 } 293 294 @VisibleForTesting isEpollAvailable()295 static boolean isEpollAvailable() { 296 try { 297 return (boolean) (Boolean) 298 Class 299 .forName("io.netty.channel.epoll.Epoll") 300 .getDeclaredMethod("isAvailable") 301 .invoke(null); 302 } catch (ClassNotFoundException e) { 303 // this is normal if netty-epoll runtime dependency doesn't exist. 304 return false; 305 } catch (Exception e) { 306 throw new RuntimeException("Exception while checking Epoll availability", e); 307 } 308 } 309 getEpollUnavailabilityCause()310 private static Throwable getEpollUnavailabilityCause() { 311 try { 312 return (Throwable) 313 Class 314 .forName("io.netty.channel.epoll.Epoll") 315 .getDeclaredMethod("unavailabilityCause") 316 .invoke(null); 317 } catch (Exception e) { 318 return e; 319 } 320 } 321 322 // Must call when epoll is available epollChannelType()323 private static Class<? extends Channel> epollChannelType() { 324 try { 325 Class<? extends Channel> channelType = Class 326 .forName("io.netty.channel.epoll.EpollSocketChannel").asSubclass(Channel.class); 327 return channelType; 328 } catch (ClassNotFoundException e) { 329 throw new RuntimeException("Cannot load EpollSocketChannel", e); 330 } 331 } 332 333 // Must call when epoll is available epollDomainSocketChannelType()334 private static Class<? extends Channel> epollDomainSocketChannelType() { 335 try { 336 Class<? extends Channel> channelType = Class 337 .forName("io.netty.channel.epoll.EpollDomainSocketChannel").asSubclass(Channel.class); 338 return channelType; 339 } catch (ClassNotFoundException e) { 340 throw new RuntimeException("Cannot load EpollDomainSocketChannel", e); 341 } 342 } 343 344 // Must call when epoll is available epollEventLoopGroupConstructor()345 private static Constructor<? extends EventLoopGroup> epollEventLoopGroupConstructor() { 346 try { 347 return Class 348 .forName("io.netty.channel.epoll.EpollEventLoopGroup").asSubclass(EventLoopGroup.class) 349 .getConstructor(Integer.TYPE, ThreadFactory.class); 350 } catch (ClassNotFoundException e) { 351 throw new RuntimeException("Cannot load EpollEventLoopGroup", e); 352 } catch (NoSuchMethodException e) { 353 throw new RuntimeException("EpollEventLoopGroup constructor not found", e); 354 } 355 } 356 357 // Must call when epoll is available epollServerChannelType()358 private static Class<? extends ServerChannel> epollServerChannelType() { 359 try { 360 Class<? extends ServerChannel> serverSocketChannel = 361 Class 362 .forName("io.netty.channel.epoll.EpollServerSocketChannel") 363 .asSubclass(ServerChannel.class); 364 return serverSocketChannel; 365 } catch (ClassNotFoundException e) { 366 throw new RuntimeException("Cannot load EpollServerSocketChannel", e); 367 } 368 } 369 createEpollEventLoopGroup( int parallelism, ThreadFactory threadFactory)370 private static EventLoopGroup createEpollEventLoopGroup( 371 int parallelism, 372 ThreadFactory threadFactory) { 373 checkState(EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR != null, "Epoll is not available"); 374 375 try { 376 return EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR 377 .newInstance(parallelism, threadFactory); 378 } catch (Exception e) { 379 throw new RuntimeException("Cannot create Epoll EventLoopGroup", e); 380 } 381 } 382 nioServerChannelFactory()383 private static ChannelFactory<ServerChannel> nioServerChannelFactory() { 384 return new ChannelFactory<ServerChannel>() { 385 @Override 386 public ServerChannel newChannel() { 387 return new NioServerSocketChannel(); 388 } 389 }; 390 } 391 392 /** 393 * Returns TCP_USER_TIMEOUT channel option for Epoll channel if Epoll is available, otherwise 394 * null. 395 */ 396 @Nullable 397 static ChannelOption<Integer> maybeGetTcpUserTimeoutOption() { 398 return getEpollChannelOption("TCP_USER_TIMEOUT"); 399 } 400 401 @Nullable 402 @SuppressWarnings("unchecked") 403 private static <T> ChannelOption<T> getEpollChannelOption(String optionName) { 404 if (isEpollAvailable()) { 405 try { 406 return 407 (ChannelOption<T>) Class.forName("io.netty.channel.epoll.EpollChannelOption") 408 .getField(optionName) 409 .get(null); 410 } catch (Exception e) { 411 throw new RuntimeException("ChannelOption(" + optionName + ") is not available", e); 412 } 413 } 414 return null; 415 } 416 417 private static final class DefaultEventLoopGroupResource implements Resource<EventLoopGroup> { 418 private final String name; 419 private final int numEventLoops; 420 private final EventLoopGroupType eventLoopGroupType; 421 422 DefaultEventLoopGroupResource( 423 int numEventLoops, String name, EventLoopGroupType eventLoopGroupType) { 424 this.name = name; 425 // See the implementation of MultithreadEventLoopGroup. DEFAULT_EVENT_LOOP_THREADS there 426 // defaults to NettyRuntime.availableProcessors() * 2. We don't think we need that many 427 // threads. The overhead of a thread includes file descriptors and at least one chunk 428 // allocation from PooledByteBufAllocator. Here we reduce the default number of threads by 429 // half. 430 if (numEventLoops == 0 && System.getProperty("io.netty.eventLoopThreads") == null) { 431 this.numEventLoops = NettyRuntime.availableProcessors(); 432 } else { 433 this.numEventLoops = numEventLoops; 434 } 435 this.eventLoopGroupType = eventLoopGroupType; 436 } 437 438 @Override 439 public EventLoopGroup create() { 440 // Use Netty's DefaultThreadFactory in order to get the benefit of FastThreadLocal. 441 ThreadFactory threadFactory = new DefaultThreadFactory(name, /* daemon= */ true); 442 switch (eventLoopGroupType) { 443 case NIO: 444 return new NioEventLoopGroup(numEventLoops, threadFactory); 445 case EPOLL: 446 return createEpollEventLoopGroup(numEventLoops, threadFactory); 447 default: 448 throw new AssertionError("Unknown/Unsupported EventLoopGroupType: " + eventLoopGroupType); 449 } 450 } 451 452 @Override 453 public void close(EventLoopGroup instance) { 454 instance.shutdownGracefully(0, 0, TimeUnit.SECONDS); 455 } 456 457 @Override 458 public String toString() { 459 return name; 460 } 461 } 462 463 static final class FlowControlReader implements TransportTracer.FlowControlReader { 464 private final Http2Stream connectionStream; 465 private final Http2FlowController local; 466 private final Http2FlowController remote; 467 468 FlowControlReader(Http2Connection connection) { 469 // 'local' in Netty is the _controller_ that controls inbound data. 'local' in Channelz is 470 // the _present window_ provided by the remote that allows data to be sent. They are 471 // opposites. 472 local = connection.remote().flowController(); 473 remote = connection.local().flowController(); 474 connectionStream = connection.connectionStream(); 475 } 476 477 @Override 478 public TransportTracer.FlowControlWindows read() { 479 return new TransportTracer.FlowControlWindows( 480 local.windowSize(connectionStream), 481 remote.windowSize(connectionStream)); 482 } 483 } 484 485 static InternalChannelz.SocketOptions getSocketOptions(Channel channel) { 486 ChannelConfig config = channel.config(); 487 InternalChannelz.SocketOptions.Builder b = new InternalChannelz.SocketOptions.Builder(); 488 489 // The API allows returning null but not sure if it can happen in practice. 490 // Let's be paranoid and do null checking just in case. 491 Integer lingerSeconds = config.getOption(SO_LINGER); 492 if (lingerSeconds != null) { 493 b.setSocketOptionLingerSeconds(lingerSeconds); 494 } 495 496 Integer timeoutMillis = config.getOption(SO_TIMEOUT); 497 if (timeoutMillis != null) { 498 // in java, SO_TIMEOUT only applies to receiving 499 b.setSocketOptionTimeoutMillis(timeoutMillis); 500 } 501 502 for (Map.Entry<ChannelOption<?>, Object> opt : config.getOptions().entrySet()) { 503 ChannelOption<?> key = opt.getKey(); 504 // Constants are pooled, so there should only be one instance of each constant 505 if (key.equals(SO_LINGER) || key.equals(SO_TIMEOUT)) { 506 continue; 507 } 508 Object value = opt.getValue(); 509 // zpencer: Can a netty option be null? 510 b.addOption(key.name(), String.valueOf(value)); 511 } 512 513 NativeSocketOptions nativeOptions 514 = NettySocketSupport.getNativeSocketOptions(channel); 515 if (nativeOptions != null) { 516 b.setTcpInfo(nativeOptions.tcpInfo); // may be null 517 for (Map.Entry<String, String> entry : nativeOptions.otherInfo.entrySet()) { 518 b.addOption(entry.getKey(), entry.getValue()); 519 } 520 } 521 return b.build(); 522 } 523 524 private enum EventLoopGroupType { 525 NIO, 526 EPOLL 527 } 528 529 private Utils() { 530 // Prevents instantiation 531 } 532 } 533