• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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