View Javadoc
1   /*
2    * Copyright 2016 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.channel.kqueue;
17  
18  import io.netty.channel.Channel;
19  import io.netty.channel.DefaultSelectStrategyFactory;
20  import io.netty.channel.IoHandle;
21  import io.netty.channel.IoHandler;
22  import io.netty.channel.IoHandlerContext;
23  import io.netty.channel.IoHandlerFactory;
24  import io.netty.channel.IoOps;
25  import io.netty.channel.IoRegistration;
26  import io.netty.channel.SelectStrategy;
27  import io.netty.channel.SelectStrategyFactory;
28  import io.netty.channel.unix.FileDescriptor;
29  import io.netty.util.IntSupplier;
30  import io.netty.util.collection.LongObjectHashMap;
31  import io.netty.util.collection.LongObjectMap;
32  import io.netty.util.concurrent.ThreadAwareExecutor;
33  import io.netty.util.internal.ObjectUtil;
34  import io.netty.util.internal.StringUtil;
35  import io.netty.util.internal.logging.InternalLogger;
36  import io.netty.util.internal.logging.InternalLoggerFactory;
37  
38  import java.io.IOException;
39  import java.util.ArrayDeque;
40  import java.util.ArrayList;
41  import java.util.Collections;
42  import java.util.List;
43  import java.util.Queue;
44  import java.util.concurrent.atomic.AtomicBoolean;
45  import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
46  
47  import static java.lang.Math.min;
48  
49  /**
50   * {@link IoHandler} which uses kqueue under the covers. Only works on BSD!
51   */
52  public final class KQueueIoHandler implements IoHandler {
53      private static final InternalLogger logger = InternalLoggerFactory.getInstance(KQueueIoHandler.class);
54      private static final AtomicIntegerFieldUpdater<KQueueIoHandler> WAKEN_UP_UPDATER =
55              AtomicIntegerFieldUpdater.newUpdater(KQueueIoHandler.class, "wakenUp");
56      private static final int KQUEUE_WAKE_UP_IDENT = 0;
57      // `kqueue()` may return EINVAL when a large number such as Integer.MAX_VALUE is specified as timeout.
58      // 24 hours would be a large enough value.
59      // https://man.freebsd.org/cgi/man.cgi?query=kevent&apropos=0&sektion=0&manpath=FreeBSD+6.1-RELEASE&format=html#end
60      private static final int KQUEUE_MAX_TIMEOUT_SECONDS = 86399; // 24 hours - 1 second
61  
62      {
63          KQueue.ensureAvailability();
64      }
65  
66      private final boolean allowGrowing;
67      private final FileDescriptor kqueueFd;
68      private final KQueueEventArray changeList;
69      private final KQueueEventArray eventList;
70      private final SelectStrategy selectStrategy;
71      private final NativeArrays nativeArrays;
72      private final IntSupplier selectNowSupplier = new IntSupplier() {
73          @Override
74          public int get() throws Exception {
75              return kqueueWaitNow();
76          }
77      };
78      private final ThreadAwareExecutor executor;
79      private final Queue<DefaultKqueueIoRegistration> cancelledRegistrations = new ArrayDeque<>();
80      private final LongObjectMap<DefaultKqueueIoRegistration> registrations = new LongObjectHashMap<>(4096);
81      private int numChannels;
82      private long nextId;
83  
84      private volatile int wakenUp;
85  
86      private long generateNextId() {
87          boolean reset = false;
88          for (;;) {
89              if (nextId == Long.MAX_VALUE) {
90                  if (reset) {
91                      throw new IllegalStateException("All possible ids in use");
92                  }
93                  reset = true;
94              }
95              nextId++;
96              if (nextId == KQUEUE_WAKE_UP_IDENT) {
97                  continue;
98              }
99              if (!registrations.containsKey(nextId)) {
100                 return nextId;
101             }
102         }
103     }
104 
105     /**
106      * Returns a new {@link IoHandlerFactory} that creates {@link KQueueIoHandler} instances.
107      */
108     public static IoHandlerFactory newFactory() {
109         return newFactory(0, DefaultSelectStrategyFactory.INSTANCE);
110     }
111 
112     /**
113      * Returns a new {@link IoHandlerFactory} that creates {@link KQueueIoHandler} instances.
114      */
115     public static IoHandlerFactory newFactory(final int maxEvents,
116                                               final SelectStrategyFactory selectStrategyFactory) {
117         KQueue.ensureAvailability();
118         ObjectUtil.checkPositiveOrZero(maxEvents, "maxEvents");
119         ObjectUtil.checkNotNull(selectStrategyFactory, "selectStrategyFactory");
120         return new IoHandlerFactory() {
121             @Override
122             public IoHandler newHandler(ThreadAwareExecutor executor) {
123                 return new KQueueIoHandler(executor, maxEvents, selectStrategyFactory.newSelectStrategy());
124             }
125 
126             @Override
127             public boolean isChangingThreadSupported() {
128                 return true;
129             }
130         };
131     }
132 
133     private KQueueIoHandler(ThreadAwareExecutor executor, int maxEvents, SelectStrategy strategy) {
134         this.executor = ObjectUtil.checkNotNull(executor, "executor");
135         this.selectStrategy = ObjectUtil.checkNotNull(strategy, "strategy");
136         this.kqueueFd = Native.newKQueue();
137         if (maxEvents == 0) {
138             allowGrowing = true;
139             maxEvents = 4096;
140         } else {
141             allowGrowing = false;
142         }
143         this.changeList = new KQueueEventArray(maxEvents);
144         this.eventList = new KQueueEventArray(maxEvents);
145         nativeArrays = new NativeArrays();
146         int result = Native.keventAddUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT);
147         if (result < 0) {
148             destroy();
149             throw new IllegalStateException("kevent failed to add user event with errno: " + (-result));
150         }
151     }
152 
153     @Override
154     public void wakeup() {
155         if (!executor.isExecutorThread(Thread.currentThread())
156                 && WAKEN_UP_UPDATER.compareAndSet(this, 0, 1)) {
157             wakeup0();
158         }
159     }
160 
161     private void wakeup0() {
162         Native.keventTriggerUserEvent(kqueueFd.intValue(), KQUEUE_WAKE_UP_IDENT);
163         // Note that the result may return an error (e.g. errno = EBADF after the event loop has been shutdown).
164         // So it is not very practical to assert the return value is always >= 0.
165     }
166 
167     private int kqueueWait(IoHandlerContext context, boolean oldWakeup) throws IOException {
168         // If a task was submitted when wakenUp value was 1, the task didn't get a chance to produce wakeup event.
169         // So we need to check task queue again before calling kqueueWait. If we don't, the task might be pended
170         // until kqueueWait was timed out. It might be pended until idle timeout if IdleStateHandler existed
171         // in pipeline.
172         if (oldWakeup && !context.canBlock()) {
173             return kqueueWaitNow();
174         }
175 
176         long totalDelay = context.delayNanos(System.nanoTime());
177         int delaySeconds = (int) min(totalDelay / 1000000000L, KQUEUE_MAX_TIMEOUT_SECONDS);
178         int delayNanos = (int) (totalDelay % 1000000000L);
179         return kqueueWait(delaySeconds, delayNanos);
180     }
181 
182     private int kqueueWaitNow() throws IOException {
183         return kqueueWait(0, 0);
184     }
185 
186     private int kqueueWait(int timeoutSec, int timeoutNs) throws IOException {
187         int numEvents = Native.keventWait(kqueueFd.intValue(), changeList, eventList, timeoutSec, timeoutNs);
188         changeList.clear();
189         return numEvents;
190     }
191 
192     private int processReady(int ready) {
193         int ioCount = 0;
194         for (int i = 0; i < ready; ++i) {
195             final short filter = eventList.filter(i);
196             final short flags = eventList.flags(i);
197             final int ident = eventList.ident(i);
198             if (filter == Native.EVFILT_USER || (flags & Native.EV_ERROR) != 0) {
199                 // EV_ERROR is returned if the FD is closed synchronously (which removes from kqueue) and then
200                 // we later attempt to delete the filters from kqueue.
201                 assert filter != Native.EVFILT_USER ||
202                         (filter == Native.EVFILT_USER && ident == KQUEUE_WAKE_UP_IDENT);
203                 continue;
204             }
205 
206             ioCount++;
207             long id = eventList.udata(i);
208             DefaultKqueueIoRegistration registration = registrations.get(id);
209             if (registration == null) {
210                 // This may happen if the channel has already been closed, and it will be removed from kqueue anyways.
211                 // We also handle EV_ERROR above to skip this even early if it is a result of a referencing a closed and
212                 // thus removed from kqueue FD.
213                 logger.warn("events[{}]=[{}, {}, {}] had no registration!", i, ident, id, filter);
214                 continue;
215             }
216             registration.handle(ident, filter, flags, eventList.fflags(i), eventList.data(i), id);
217         }
218         return ioCount;
219     }
220 
221     @Override
222     public int run(IoHandlerContext context) {
223         int handled = 0;
224         try {
225             int strategy = selectStrategy.calculateStrategy(selectNowSupplier, !context.canBlock());
226             switch (strategy) {
227                 case SelectStrategy.CONTINUE:
228                     if (context.shouldReportActiveIoTime()) {
229                         context.reportActiveIoTime(0); // Report zero as we did no I/O.
230                     }
231                     return 0;
232 
233                 case SelectStrategy.BUSY_WAIT:
234                     // fall-through to SELECT since the busy-wait is not supported with kqueue
235 
236                 case SelectStrategy.SELECT:
237                     strategy = kqueueWait(context, WAKEN_UP_UPDATER.getAndSet(this, 0) == 1);
238 
239                     // 'wakenUp.compareAndSet(false, true)' is always evaluated
240                     // before calling 'selector.wakeup()' to reduce the wake-up
241                     // overhead. (Selector.wakeup() is an expensive operation.)
242                     //
243                     // However, there is a race condition in this approach.
244                     // The race condition is triggered when 'wakenUp' is set to
245                     // true too early.
246                     //
247                     // 'wakenUp' is set to true too early if:
248                     // 1) Selector is waken up between 'wakenUp.set(false)' and
249                     //    'selector.select(...)'. (BAD)
250                     // 2) Selector is waken up between 'selector.select(...)' and
251                     //    'if (wakenUp.get()) { ... }'. (OK)
252                     //
253                     // In the first case, 'wakenUp' is set to true and the
254                     // following 'selector.select(...)' will wake up immediately.
255                     // Until 'wakenUp' is set to false again in the next round,
256                     // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
257                     // any attempt to wake up the Selector will fail, too, causing
258                     // the following 'selector.select(...)' call to block
259                     // unnecessarily.
260                     //
261                     // To fix this problem, we wake up the selector again if wakenUp
262                     // is true immediately after selector.select(...).
263                     // It is inefficient in that it wakes up the selector for both
264                     // the first case (BAD - wake-up required) and the second case
265                     // (OK - no wake-up required).
266 
267                     if (wakenUp == 1) {
268                         wakeup0();
269                     }
270                     // fall-through
271                 default:
272             }
273 
274             if (strategy > 0) {
275                 if (context.shouldReportActiveIoTime()) {
276                     long activeIoStartTimeNanos = System.nanoTime();
277                     handled = processReady(strategy);
278                     long activeIoEndTimeNanos = System.nanoTime();
279                     context.reportActiveIoTime(activeIoEndTimeNanos - activeIoStartTimeNanos);
280                 } else {
281                     handled = processReady(strategy);
282                 }
283             } else if (context.shouldReportActiveIoTime()) {
284                 context.reportActiveIoTime(0);
285             }
286 
287             if (allowGrowing && strategy == eventList.capacity()) {
288                 //increase the size of the array as we needed the whole space for the events
289                 eventList.realloc(false);
290             }
291         } catch (Error e) {
292             throw e;
293         } catch (Throwable t) {
294             handleLoopException(t);
295         } finally {
296             processCancelledRegistrations();
297         }
298         return handled;
299     }
300 
301     // Process all previous cannceld registrations and remove them from the registration map.
302     private void processCancelledRegistrations() {
303         for (;;) {
304             DefaultKqueueIoRegistration cancelledRegistration = cancelledRegistrations.poll();
305             if (cancelledRegistration == null) {
306                 return;
307             }
308             DefaultKqueueIoRegistration removed = registrations.remove(cancelledRegistration.id);
309             assert removed == cancelledRegistration;
310             if (removed.isHandleForChannel()) {
311                 numChannels--;
312             }
313             removed.handle.unregistered();
314         }
315     }
316 
317     int numRegisteredChannels() {
318         return numChannels;
319     }
320 
321     List<Channel> registeredChannelsList() {
322         LongObjectMap<DefaultKqueueIoRegistration> ch = registrations;
323         if (ch.isEmpty()) {
324             return Collections.emptyList();
325         }
326 
327         List<Channel> channels = new ArrayList<>(ch.size());
328 
329         for (DefaultKqueueIoRegistration registration : ch.values()) {
330             if (registration.handle instanceof AbstractKQueueChannel.AbstractKQueueUnsafe) {
331                 channels.add(((AbstractKQueueChannel.AbstractKQueueUnsafe) registration.handle).channel());
332             }
333         }
334         return Collections.unmodifiableList(channels);
335     }
336 
337     private static void handleLoopException(Throwable t) {
338         logger.warn("Unexpected exception in the selector loop.", t);
339 
340         // Prevent possible consecutive immediate failures that lead to
341         // excessive CPU consumption.
342         try {
343             Thread.sleep(1000);
344         } catch (InterruptedException e) {
345             // Ignore.
346         }
347     }
348 
349     @Override
350     public void prepareToDestroy() {
351         try {
352             kqueueWaitNow();
353         } catch (IOException e) {
354             // ignore on close
355         }
356 
357         // Using the intermediate collection to prevent ConcurrentModificationException.
358         // In the `close()` method, the channel is deleted from `channels` map.
359         DefaultKqueueIoRegistration[] copy = registrations.values().toArray(new DefaultKqueueIoRegistration[0]);
360 
361         for (DefaultKqueueIoRegistration reg: copy) {
362             reg.close();
363         }
364 
365         processCancelledRegistrations();
366     }
367 
368     @Override
369     public void destroy() {
370         try {
371             try {
372                 kqueueFd.close();
373             } catch (IOException e) {
374                 logger.warn("Failed to close the kqueue fd.", e);
375             }
376         } finally {
377             // Cleanup all native memory!
378             nativeArrays.free();
379             changeList.free();
380             eventList.free();
381         }
382     }
383 
384     @Override
385     public IoRegistration register(IoHandle handle) {
386         final KQueueIoHandle kqueueHandle = cast(handle);
387         if (kqueueHandle.ident() == KQUEUE_WAKE_UP_IDENT) {
388             throw new IllegalArgumentException("ident " + KQUEUE_WAKE_UP_IDENT + " is reserved for internal usage");
389         }
390 
391         DefaultKqueueIoRegistration registration = new DefaultKqueueIoRegistration(
392                 executor, kqueueHandle);
393         DefaultKqueueIoRegistration old = registrations.put(registration.id, registration);
394         if (old != null) {
395             // This should never happen but just in case.
396             registrations.put(old.id, old);
397             throw new IllegalStateException();
398         }
399         if (registration.isHandleForChannel()) {
400             numChannels++;
401         }
402         handle.registered();
403         return registration;
404     }
405 
406     private static KQueueIoHandle cast(IoHandle handle) {
407         if (handle instanceof KQueueIoHandle) {
408             return (KQueueIoHandle) handle;
409         }
410         throw new IllegalArgumentException("IoHandle of type " + StringUtil.simpleClassName(handle) + " not supported");
411     }
412 
413     private static KQueueIoOps cast(IoOps ops) {
414         if (ops instanceof KQueueIoOps) {
415             return (KQueueIoOps) ops;
416         }
417         throw new IllegalArgumentException("IoOps of type " + StringUtil.simpleClassName(ops) + " not supported");
418     }
419 
420     @Override
421     public boolean isCompatible(Class<? extends IoHandle> handleType) {
422         return KQueueIoHandle.class.isAssignableFrom(handleType);
423     }
424 
425     private final class DefaultKqueueIoRegistration implements IoRegistration {
426         private boolean cancellationPending;
427         private final AtomicBoolean canceled = new AtomicBoolean();
428         private final KQueueIoEvent event = new KQueueIoEvent();
429 
430         final KQueueIoHandle handle;
431         final long id;
432         private final ThreadAwareExecutor executor;
433 
434         DefaultKqueueIoRegistration(ThreadAwareExecutor executor, KQueueIoHandle handle) {
435             this.executor = executor;
436             this.handle = handle;
437             id = generateNextId();
438         }
439 
440         boolean isHandleForChannel() {
441             return handle instanceof AbstractKQueueChannel.AbstractKQueueUnsafe;
442         }
443 
444         @SuppressWarnings("unchecked")
445         @Override
446         public <T> T attachment() {
447             return (T) nativeArrays;
448         }
449 
450         @Override
451         public long submit(IoOps ops) {
452             KQueueIoOps kQueueIoOps = cast(ops);
453             if (!isValid()) {
454                 return -1;
455             }
456             short filter = kQueueIoOps.filter();
457             short flags = kQueueIoOps.flags();
458             int fflags = kQueueIoOps.fflags();
459             long data = kQueueIoOps.data();
460             if (executor.isExecutorThread(Thread.currentThread())) {
461                 evSet(filter, flags, fflags, data);
462             } else {
463                 executor.execute(() -> evSet(filter, flags, fflags, data));
464             }
465             return 0;
466         }
467 
468         void handle(int ident, short filter, short flags, int fflags, long data, long udata) {
469             if (cancellationPending) {
470                 // This registration was already cancelled but not removed from the map yet, just ignore.
471                 return;
472             }
473             event.update(ident, filter, flags, fflags, data, udata);
474             handle.handle(this, event);
475         }
476 
477         private void evSet(short filter, short flags, int fflags, long data) {
478             if (cancellationPending) {
479                 // This registration was already cancelled but not removed from the map yet, just ignore.
480                 return;
481             }
482             changeList.evSet(handle.ident(), filter, flags, fflags, data, id);
483         }
484 
485         @Override
486         public boolean isValid() {
487             return !canceled.get();
488         }
489 
490         @Override
491         public boolean cancel() {
492             if (!canceled.compareAndSet(false, true)) {
493                 return false;
494             }
495             if (executor.isExecutorThread(Thread.currentThread())) {
496                 cancel0();
497             } else {
498                 executor.execute(this::cancel0);
499             }
500             return true;
501         }
502 
503         private void cancel0() {
504             // Let's add the registration to our cancelledRegistrations queue so we will process it
505             // after we processed all events. This is needed as otherwise we might end up removing it
506             // from the registration map while we still have some unprocessed events.
507             cancellationPending = true;
508             cancelledRegistrations.offer(this);
509         }
510 
511         void close() {
512             cancel();
513             try {
514                 handle.close();
515             } catch (Exception e) {
516                 logger.debug("Exception during closing " + handle, e);
517             }
518         }
519     }
520 }