netty5/common/src/main/java/io/netty/util/concurrent/NonStickyEventExecutorGroup.java
Norman Maurer 3d6e6136a9
Decouple EventLoop details from the IO handling for each transport to… (#8680)
* Decouble EventLoop details from the IO handling for each transport to allow easy re-use of code and customization

Motiviation:

As today extending EventLoop implementations to add custom logic / metrics / instrumentations is only possible in a very limited way if at all. This is due the fact that most implementations are final or even package-private. That said even if these would be public there are the ability to do something useful with these is very limited as the IO processing and task processing are very tightly coupled. All of the mentioned things are a big pain point in netty 4.x and need improvement.

Modifications:

This changeset decoubled the IO processing logic from the task processing logic for the main transport (NIO, Epoll, KQueue) by introducing the concept of an IoHandler. The IoHandler itself is responsible to wait for IO readiness and process these IO events. The execution of the IoHandler itself is done by the SingleThreadEventLoop as part of its EventLoop processing. This allows to use the same EventLoopGroup (MultiThreadEventLoupGroup) for all the mentioned transports by just specify a different IoHandlerFactory during construction.

Beside this core API change this changeset also allows to easily extend SingleThreadEventExecutor / SingleThreadEventLoop to add custom logic to it which then can be reused by all the transports. The ideas are very similar to what is provided by ScheduledThreadPoolExecutor (that is part of the JDK). This allows for example things like:

  * Adding instrumentation / metrics:
    * how many Channels are registered on an SingleThreadEventLoop
    * how many Channels were handled during the IO processing in an EventLoop run
    * how many task were handled during the last EventLoop / EventExecutor run
    * how many outstanding tasks we have
    ...
    ...
  * Implementing custom strategies for choosing the next EventExecutor / EventLoop to use based on these metrics.
  * Use different Promise / Future / ScheduledFuture implementations
  * decorate Runnable / Callables when submitted to the EventExecutor / EventLoop

As a lot of functionalities are folded into the MultiThreadEventLoopGroup and SingleThreadEventLoopGroup this changeset also removes:

  * AbstractEventLoop
  * AbstractEventLoopGroup
  * EventExecutorChooser
  * EventExecutorChooserFactory
  * DefaultEventLoopGroup
  * DefaultEventExecutor
  * DefaultEventExecutorGroup

Result:

Fixes https://github.com/netty/netty/issues/8514 .
2019-01-23 08:32:05 +01:00

366 lines
13 KiB
Java

/*
* Copyright 2016 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.util.concurrent;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.UnstableApi;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* {@link EventExecutorGroup} which will preserve {@link Runnable} execution order but makes no guarantees about what
* {@link EventExecutor} (and therefore {@link Thread}) will be used to execute the {@link Runnable}s.
*
* <p>The {@link EventExecutorGroup#next()} for the wrapped {@link EventExecutorGroup} must <strong>NOT</strong> return
* executors of type {@link OrderedEventExecutor}.
*/
@UnstableApi
public final class NonStickyEventExecutorGroup implements EventExecutorGroup {
private final EventExecutorGroup group;
private final int maxTaskExecutePerRun;
/**
* Creates a new instance. Be aware that the given {@link EventExecutorGroup} <strong>MUST NOT</strong> contain
* any {@link OrderedEventExecutor}s.
*/
public NonStickyEventExecutorGroup(EventExecutorGroup group) {
this(group, 1024);
}
/**
* Creates a new instance. Be aware that the given {@link EventExecutorGroup} <strong>MUST NOT</strong> contain
* any {@link OrderedEventExecutor}s.
*/
public NonStickyEventExecutorGroup(EventExecutorGroup group, int maxTaskExecutePerRun) {
this.group = verify(group);
this.maxTaskExecutePerRun = ObjectUtil.checkPositive(maxTaskExecutePerRun, "maxTaskExecutePerRun");
}
private static EventExecutorGroup verify(EventExecutorGroup group) {
Iterator<EventExecutor> executors = ObjectUtil.checkNotNull(group, "group").iterator();
while (executors.hasNext()) {
EventExecutor executor = executors.next();
if (executor instanceof OrderedEventExecutor) {
throw new IllegalArgumentException("EventExecutorGroup " + group
+ " contains OrderedEventExecutors: " + executor);
}
}
return group;
}
private NonStickyOrderedEventExecutor newExecutor(EventExecutor executor) {
return new NonStickyOrderedEventExecutor(executor, maxTaskExecutePerRun);
}
@Override
public boolean isShuttingDown() {
return group.isShuttingDown();
}
@Override
public Future<?> shutdownGracefully() {
return group.shutdownGracefully();
}
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
return group.shutdownGracefully(quietPeriod, timeout, unit);
}
@Override
public Future<?> terminationFuture() {
return group.terminationFuture();
}
@SuppressWarnings("deprecation")
@Override
public void shutdown() {
group.shutdown();
}
@SuppressWarnings("deprecation")
@Override
public List<Runnable> shutdownNow() {
return group.shutdownNow();
}
@Override
public EventExecutor next() {
return newExecutor(group.next());
}
@Override
public Iterator<EventExecutor> iterator() {
final Iterator<EventExecutor> itr = group.iterator();
return new Iterator<EventExecutor>() {
@Override
public boolean hasNext() {
return itr.hasNext();
}
@Override
public EventExecutor next() {
return newExecutor(itr.next());
}
@Override
public void remove() {
itr.remove();
}
};
}
@Override
public Future<?> submit(Runnable task) {
return group.submit(task);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return group.submit(task, result);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return group.submit(task);
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
return group.schedule(command, delay, unit);
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
return group.schedule(callable, delay, unit);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
return group.scheduleAtFixedRate(command, initialDelay, period, unit);
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
return group.scheduleWithFixedDelay(command, initialDelay, delay, unit);
}
@Override
public boolean isShutdown() {
return group.isShutdown();
}
@Override
public boolean isTerminated() {
return group.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return group.awaitTermination(timeout, unit);
}
@Override
public <T> List<java.util.concurrent.Future<T>> invokeAll(
Collection<? extends Callable<T>> tasks) throws InterruptedException {
return group.invokeAll(tasks);
}
@Override
public <T> List<java.util.concurrent.Future<T>> invokeAll(
Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
return group.invokeAll(tasks, timeout, unit);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
return group.invokeAny(tasks);
}
@Override
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
return group.invokeAny(tasks, timeout, unit);
}
@Override
public void execute(Runnable command) {
group.execute(command);
}
private static final class NonStickyOrderedEventExecutor extends AbstractEventExecutor
implements Runnable, OrderedEventExecutor {
private final EventExecutor executor;
private final Queue<Runnable> tasks = PlatformDependent.newMpscQueue();
private static final int NONE = 0;
private static final int SUBMITTED = 1;
private static final int RUNNING = 2;
private final AtomicInteger state = new AtomicInteger();
private final int maxTaskExecutePerRun;
NonStickyOrderedEventExecutor(EventExecutor executor, int maxTaskExecutePerRun) {
this.executor = executor;
this.maxTaskExecutePerRun = maxTaskExecutePerRun;
}
@Override
public void run() {
if (!state.compareAndSet(SUBMITTED, RUNNING)) {
return;
}
for (;;) {
int i = 0;
try {
for (; i < maxTaskExecutePerRun; i++) {
Runnable task = tasks.poll();
if (task == null) {
break;
}
safeExecute(task);
}
} finally {
if (i == maxTaskExecutePerRun) {
try {
state.set(SUBMITTED);
executor.execute(this);
return; // done
} catch (Throwable ignore) {
// Reset the state back to running as we will keep on executing tasks.
state.set(RUNNING);
// if an error happened we should just ignore it and let the loop run again as there is not
// much else we can do. Most likely this was triggered by a full task queue. In this case
// we just will run more tasks and try again later.
}
} else {
state.set(NONE);
// After setting the state to NONE, look at the tasks queue one more time.
// If it is empty, then we can return from this method.
// Otherwise, it means the producer thread has called execute(Runnable)
// and enqueued a task in between the tasks.poll() above and the state.set(NONE) here.
// There are two possible scenarios when this happen
//
// 1. The producer thread sees state == NONE, hence the compareAndSet(NONE, SUBMITTED)
// is successfully setting the state to SUBMITTED. This mean the producer
// will call / has called executor.execute(this). In this case, we can just return.
// 2. The producer thread don't see the state change, hence the compareAndSet(NONE, SUBMITTED)
// returns false. In this case, the producer thread won't call executor.execute.
// In this case, we need to change the state to RUNNING and keeps running.
//
// The above cases can be distinguished by performing a
// compareAndSet(NONE, RUNNING). If it returns "false", it is case 1; otherwise it is case 2.
if (tasks.peek() == null || !state.compareAndSet(NONE, RUNNING)) {
return; // done
}
}
}
}
}
@Override
public boolean inEventLoop(Thread thread) {
return false;
}
@Override
public boolean isShuttingDown() {
return executor.isShutdown();
}
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
return executor.shutdownGracefully(quietPeriod, timeout, unit);
}
@Override
public Future<?> terminationFuture() {
return executor.terminationFuture();
}
@Override
public void shutdown() {
executor.shutdown();
}
@Override
public boolean isShutdown() {
return executor.isShutdown();
}
@Override
public boolean isTerminated() {
return executor.isTerminated();
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
return executor.awaitTermination(timeout, unit);
}
@Override
public void execute(Runnable command) {
if (!tasks.offer(command)) {
throw new RejectedExecutionException();
}
if (state.compareAndSet(NONE, SUBMITTED)) {
// Actually it could happen that the runnable was picked up in between but we not care to much and just
// execute ourself. At worst this will be a NOOP when run() is called.
try {
executor.execute(this);
} catch (Throwable e) {
// Not reset the state as some other Runnable may be added to the queue already in the meantime.
tasks.remove(command);
PlatformDependent.throwException(e);
}
}
}
@Override
public ScheduledFuture<?> schedule(Runnable command, long delay,
TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command, long initialDelay, long period, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command, long initialDelay, long delay, TimeUnit unit) {
throw new UnsupportedOperationException();
}
}
}