View Javadoc
1   /*
2    * Copyright 2024 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;
17  
18  import io.netty.util.concurrent.Future;
19  import io.netty.util.concurrent.Promise;
20  import io.netty.util.concurrent.RejectedExecutionHandler;
21  import io.netty.util.concurrent.SingleThreadEventExecutor;
22  import io.netty.util.internal.ObjectUtil;
23  import io.netty.util.internal.PlatformDependent;
24  import io.netty.util.internal.SystemPropertyUtil;
25  
26  import java.util.Queue;
27  import java.util.concurrent.Executor;
28  import java.util.concurrent.ThreadFactory;
29  import java.util.concurrent.TimeUnit;
30  import java.util.concurrent.atomic.AtomicInteger;
31  
32  /**
33   * {@link IoEventLoop} implementation that execute all its submitted tasks in a single thread using the provided
34   * {@link IoHandler}.
35   */
36  public class SingleThreadIoEventLoop extends SingleThreadEventLoop implements IoEventLoop {
37  
38      // TODO: Is this a sensible default ?
39      private static final long DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS = TimeUnit.MILLISECONDS.toNanos(Math.max(100,
40              SystemPropertyUtil.getInt("io.netty.eventLoop.maxTaskProcessingQuantumMs", 1000)));
41  
42      // 100 preserves the pre-existing behaviour of always using maxTaskProcessingQuantumNs as the task budget.
43      static final int DEFAULT_IO_RATIO = Math.max(1, Math.min(100,
44          SystemPropertyUtil.getInt("io.netty.eventLoop.ioRatio", 100)));
45  
46      private final long maxTaskProcessingQuantumNs;
47      private volatile int ioRatio = DEFAULT_IO_RATIO;
48      private long activeIoTimeNanos = -1;
49      private final IoHandlerContext context = new IoHandlerContext() {
50          @Override
51          public boolean canBlock() {
52              assert inEventLoop();
53              return !hasTasks() && !hasScheduledTasks();
54          }
55  
56          @Override
57          public long delayNanos(long currentTimeNanos) {
58              assert inEventLoop();
59              return SingleThreadIoEventLoop.this.delayNanos(currentTimeNanos);
60          }
61  
62          @Override
63          public long deadlineNanos() {
64              assert inEventLoop();
65              return SingleThreadIoEventLoop.this.deadlineNanos();
66          }
67  
68          @Override
69          public void reportActiveIoTime(long activeNanos) {
70              if (activeNanos >= 0) {
71                  activeIoTimeNanos = activeNanos;
72              }
73              SingleThreadIoEventLoop.this.reportActiveIoTime(activeNanos);
74          }
75  
76          @Override
77          public boolean shouldReportActiveIoTime() {
78              return isSuspensionSupported() || ioRatio != 100;
79          }
80      };
81  
82      private final IoHandler ioHandler;
83  
84      private final AtomicInteger numRegistrations = new AtomicInteger();
85  
86      /**
87       *  Creates a new instance
88       *
89       * @param parent            the parent that holds this {@link IoEventLoop}.
90       * @param threadFactory     the {@link ThreadFactory} that is used to create the underlying {@link Thread}.
91       * @param ioHandlerFactory  the {@link IoHandlerFactory} that should be used to obtain {@link IoHandler} to
92       *                          handle IO.
93       */
94      public SingleThreadIoEventLoop(IoEventLoopGroup parent, ThreadFactory threadFactory,
95                                     IoHandlerFactory ioHandlerFactory) {
96          super(parent, threadFactory, false,
97                  ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported());
98          this.maxTaskProcessingQuantumNs = DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS;
99          this.ioHandler = ioHandlerFactory.newHandler(this);
100     }
101 
102     /**
103      *  Creates a new instance
104      *
105      * @param parent            the parent that holds this {@link IoEventLoop}.
106      * @param executor          the {@link Executor} that is used for dispatching the work.
107      * @param ioHandlerFactory  the {@link IoHandlerFactory} that should be used to obtain {@link IoHandler} to
108      *                          handle IO.
109      */
110     public SingleThreadIoEventLoop(IoEventLoopGroup parent, Executor executor, IoHandlerFactory ioHandlerFactory) {
111         super(parent, executor, false,
112                 ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported());
113         this.maxTaskProcessingQuantumNs = DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS;
114         this.ioHandler = ioHandlerFactory.newHandler(this);
115     }
116 
117     /**
118      *  Creates a new instance
119      *
120      * @param parent                        the parent that holds this {@link IoEventLoop}.
121      * @param threadFactory                 the {@link ThreadFactory} that is used to create the underlying
122      *                                      {@link Thread}.
123      * @param ioHandlerFactory              the {@link IoHandlerFactory} that should be used to obtain {@link IoHandler}
124      *                                      to handle IO.
125      * @param maxPendingTasks               the maximum pending tasks that are allowed before
126      *                                      {@link RejectedExecutionHandler#rejected(Runnable,
127      *                                          SingleThreadEventExecutor)}
128      *                                      is called to handle it.
129      * @param rejectedExecutionHandler      the {@link RejectedExecutionHandler} that handles when more tasks are added
130      *                                      then allowed per {@code maxPendingTasks}.
131      * @param maxTaskProcessingQuantumMs    the maximum number of milliseconds that will be spent to run tasks before
132      *                                      trying to run IO again.
133      */
134     public SingleThreadIoEventLoop(IoEventLoopGroup parent, ThreadFactory threadFactory,
135                                    IoHandlerFactory ioHandlerFactory, int maxPendingTasks,
136                                    RejectedExecutionHandler rejectedExecutionHandler, long maxTaskProcessingQuantumMs) {
137         super(parent, threadFactory, false,
138                 ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported(),
139                 maxPendingTasks, rejectedExecutionHandler);
140         this.maxTaskProcessingQuantumNs =
141                 ObjectUtil.checkPositiveOrZero(maxTaskProcessingQuantumMs, "maxTaskProcessingQuantumMs") == 0 ?
142                         DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS :
143                         TimeUnit.MILLISECONDS.toNanos(maxTaskProcessingQuantumMs);
144         this.ioHandler = ioHandlerFactory.newHandler(this);
145     }
146 
147     /**
148      *  Creates a new instance
149      *
150      * @param parent                        the parent that holds this {@link IoEventLoop}.
151      * @param ioHandlerFactory              the {@link IoHandlerFactory} that should be used to obtain {@link IoHandler}
152      *                                      to handle IO.
153      * @param maxPendingTasks               the maximum pending tasks that are allowed before
154      *                                      {@link RejectedExecutionHandler#rejected(Runnable,
155      *                                          SingleThreadEventExecutor)}
156      *                                      is called to handle it.
157      * @param rejectedExecutionHandler      the {@link RejectedExecutionHandler} that handles when more tasks are added
158      *                                      then allowed per {@code maxPendingTasks}.
159      * @param maxTaskProcessingQuantumMs    the maximum number of milliseconds that will be spent to run tasks before
160      *                                      trying to run IO again.
161      */
162     public SingleThreadIoEventLoop(IoEventLoopGroup parent, Executor executor,
163                                    IoHandlerFactory ioHandlerFactory, int maxPendingTasks,
164                                    RejectedExecutionHandler rejectedExecutionHandler,
165                                    long maxTaskProcessingQuantumMs) {
166         super(parent, executor, false,
167                 ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported(),
168                 maxPendingTasks, rejectedExecutionHandler);
169         this.maxTaskProcessingQuantumNs =
170                 ObjectUtil.checkPositiveOrZero(maxTaskProcessingQuantumMs, "maxTaskProcessingQuantumMs") == 0 ?
171                         DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS :
172                         TimeUnit.MILLISECONDS.toNanos(maxTaskProcessingQuantumMs);
173         this.ioHandler = ioHandlerFactory.newHandler(this);
174     }
175 
176     /**
177      *
178      *  Creates a new instance
179      *
180      * @param parent                    the parent that holds this {@link IoEventLoop}.
181      * @param executor                  the {@link Executor} that is used for dispatching the work.
182      * @param ioHandlerFactory          the {@link IoHandlerFactory} that should be used to obtain {@link IoHandler}
183      *                                  to handle IO.
184      * @param taskQueue                 the {@link Queue} used for storing pending tasks.
185      * @param tailTaskQueue             the {@link Queue} used for storing tail pending tasks.
186      * @param rejectedExecutionHandler  the {@link RejectedExecutionHandler} that handles when more tasks are added
187      *                                  then allowed.
188      */
189     protected SingleThreadIoEventLoop(IoEventLoopGroup parent, Executor executor,
190                                       IoHandlerFactory ioHandlerFactory, Queue<Runnable> taskQueue,
191                                       Queue<Runnable> tailTaskQueue,
192                                       RejectedExecutionHandler rejectedExecutionHandler) {
193         super(parent, executor, false,
194                 ObjectUtil.checkNotNull(ioHandlerFactory, "ioHandlerFactory").isChangingThreadSupported(),
195                 taskQueue, tailTaskQueue, rejectedExecutionHandler);
196         this.maxTaskProcessingQuantumNs = DEFAULT_MAX_TASK_PROCESSING_QUANTUM_NS;
197         this.ioHandler = ioHandlerFactory.newHandler(this);
198     }
199 
200     @Override
201     protected void run() {
202         assert inEventLoop();
203         ioHandler.initialize();
204         do {
205             final int ioRatio = this.ioRatio;
206             final long taskQuantumNs;
207 
208             // reset before calling runIo() which is responsible for calling reportActiveIoTime(...).
209             activeIoTimeNanos = -1;
210             runIo();
211             if (ioRatio == 100 || activeIoTimeNanos == -1) {
212                 taskQuantumNs = maxTaskProcessingQuantumNs;
213             } else {
214                 // Give tasks a budget proportional to the time just spent on IO, still bounded by the
215                 // configured maximum so a burst of IO activity cannot starve the task queue indefinitely.
216                 long quantum = Math.min(maxTaskProcessingQuantumNs, activeIoTimeNanos * (100 - ioRatio) / ioRatio);
217                 if (quantum == 0) {
218                     // Ensure we always use a "deadline"
219                     taskQuantumNs = maxTaskProcessingQuantumNs;
220                 } else {
221                     taskQuantumNs = quantum;
222                 }
223             }
224             if (isShuttingDown()) {
225                 ioHandler.prepareToDestroy();
226             }
227             // Now run tasks for the computed amount of time before trying to run IO again.
228             runAllTasks(taskQuantumNs);
229 
230             // We should continue with our loop until we either confirmed a shutdown or we can suspend it.
231         } while (!confirmShutdown() && !canSuspend());
232     }
233 
234     protected final IoHandler ioHandler() {
235         return ioHandler;
236     }
237 
238     /**
239      * Returns the current IO ratio. This value only affects scheduling behaviour when it's not the default
240      * value of {@code 100}, in which case each iteration's task-processing budget will be capped to the
241      * proportion of time just spent handling IO, in addition to the cap already imposed by
242      * {@code io.netty.eventLoop.maxTaskProcessingQuantumMs}.
243      */
244     public int getIoRatio() {
245         return ioRatio;
246     }
247 
248     /**
249      * Sets the percentage of the desired amount of time spent for I/O in the event loop relative to tasks.
250      * The default value is {@code 100}, which means the event loop will not attempt to balance I/O and tasks,
251      * relying purely on {@code io.netty.eventLoop.maxTaskProcessingQuantumMs} to bound the time spent running
252      * tasks. Setting a value below {@code 100}, e.g. {@code 50}, restores the pre-4.2 behaviour of giving tasks
253      * a per-iteration budget proportional to the time just spent on IO, which helps ensure that a busy task
254      * queue does not delay IO processing (and so response handling) for extended stretches of time.
255      */
256     public void setIoRatio(int ioRatio) {
257         this.ioRatio = ObjectUtil.checkInRange(ioRatio, 1, 100, "ioRatio");
258     }
259 
260     @Override
261     protected boolean canSuspend(int state) {
262         // We should only allow to suspend if there are no registrations on this loop atm.
263         return super.canSuspend(state) && numRegistrations.get() == 0;
264     }
265 
266     /**
267      * Called when IO will be processed for all the {@link IoHandle}s on this {@link SingleThreadIoEventLoop}.
268      * This method returns the number of {@link IoHandle}s for which IO was processed.
269      *
270      * This method must be called from the {@link EventLoop} thread.
271      */
272     protected int runIo() {
273         assert inEventLoop();
274         return ioHandler.run(context);
275     }
276 
277     @Override
278     public IoEventLoop next() {
279         return this;
280     }
281 
282     @Override
283     public final Future<IoRegistration> register(final IoHandle handle) {
284         Promise<IoRegistration> promise = newPromise();
285         if (inEventLoop()) {
286             registerForIo0(handle, promise);
287         } else {
288             execute(() -> registerForIo0(handle, promise));
289         }
290 
291         return promise;
292     }
293 
294     @Override
295     protected int getNumOfRegisteredChannels() {
296         return numRegistrations.get();
297     }
298 
299     private void registerForIo0(final IoHandle handle, Promise<IoRegistration> promise) {
300         assert inEventLoop();
301         final IoRegistration registration;
302         try {
303             registration = ioHandler.register(handle);
304         } catch (Exception e) {
305             promise.setFailure(e);
306             return;
307         }
308         numRegistrations.incrementAndGet();
309         promise.setSuccess(new IoRegistrationWrapper(registration));
310     }
311 
312     @Override
313     protected final void wakeup(boolean inEventLoop) {
314         ioHandler.wakeup();
315     }
316 
317     @Override
318     protected final void cleanup() {
319         assert inEventLoop();
320         ioHandler.destroy();
321     }
322 
323     @Override
324     public boolean isCompatible(Class<? extends IoHandle> handleType) {
325         return ioHandler.isCompatible(handleType);
326     }
327 
328     @Override
329     public boolean isIoType(Class<? extends IoHandler> handlerType) {
330         return ioHandler.getClass().equals(handlerType);
331     }
332 
333     @Override
334     protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
335         return newTaskQueue0(maxPendingTasks);
336     }
337 
338     protected static Queue<Runnable> newTaskQueue0(int maxPendingTasks) {
339         // This event loop never calls takeTask()
340         return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
341                 : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
342     }
343 
344     private final class IoRegistrationWrapper implements IoRegistration {
345         private final IoRegistration registration;
346         IoRegistrationWrapper(IoRegistration registration) {
347             this.registration = registration;
348         }
349 
350         @Override
351         public <T> T attachment() {
352             return registration.attachment();
353         }
354 
355         @Override
356         public long submit(IoOps ops) {
357             return registration.submit(ops);
358         }
359 
360         @Override
361         public boolean isValid() {
362             return registration.isValid();
363         }
364 
365         @Override
366         public boolean cancel() {
367             if (registration.cancel()) {
368                 numRegistrations.decrementAndGet();
369                 return true;
370             }
371             return false;
372         }
373     }
374 }