netty5/common/src/main/java/io/netty/util/concurrent/GlobalEventExecutor.java
Chris Vest 59275fba52
Netty Future no longer extends JDK Future (#11647)
Motivation:
It is important to avoid blocking method calls in an event loop thread, since that can stall the system.
Netty's Future interface was extending the JDK Future interface, which included a number of blocking methods of questionable use in Netty.
We wish to reduce the number of blocking methods on the Future API in order to discourage their use a little.
Further more, the Netty Future specification of the behaviour of the cancel() and isDone() methods are inconsistent with those of the JDK Future.
If Netty's Future stop extending the JDK Future interface, it will also no longer be bound by its specification.

Modification:
Make Netty's Future no longer extend the JDK Future interface.
Change the EvenExecutorGroup interface to no longer extend ScheduledExecutorService.
The EventExecutorGroup still extends Executor, because Executor does not dictate any return type of the `execute()` method — this is also useful in the DefaultFutureCompletionStage implementation.
The Netty ScheduledFuture interface has been removed since it provided no additional features that were actually used.
Numerous changes to use sites that previously relied on the JDK types.
Remove the `Future.cancel()` method that took a boolean argument — this argument was always ignored in our implementations, which was another spec deviation.
Various `invoke*` and `shutdown*` methods have been removed from the EvenExecutorGroup API since it no longer extends ScheduledExecutorService — these were either not used anywhere, or deprecated with better alternatives available.
Updates to cancellation javadocs.

Result:
Cleaner code, leaner API.
2021-09-08 09:06:28 +02:00

283 lines
11 KiB
Java

/*
* Copyright 2012 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:
*
* https://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 static java.util.Objects.requireNonNull;
import io.netty.util.internal.ThreadExecutorMap;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Queue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Single-thread singleton {@link EventExecutor}. It starts the thread automatically and stops it when there is no
* task pending in the task queue for 1 second. Please note it is not scalable to schedule large number of tasks to
* this executor; use a dedicated executor.
*/
public final class GlobalEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(GlobalEventExecutor.class);
private static final long SCHEDULE_QUIET_PERIOD_INTERVAL = TimeUnit.SECONDS.toNanos(1);
private static final RunnableScheduledFutureAdapter<Void> QUIET_PERIOD_TASK;
public static final GlobalEventExecutor INSTANCE;
static {
INSTANCE = new GlobalEventExecutor();
QUIET_PERIOD_TASK = new RunnableScheduledFutureAdapter<>(
INSTANCE, INSTANCE.newPromise(), Executors.callable(() -> {
// NOOP
}, null), deadlineNanos(SCHEDULE_QUIET_PERIOD_INTERVAL), -SCHEDULE_QUIET_PERIOD_INTERVAL);
INSTANCE.scheduledTaskQueue().add(QUIET_PERIOD_TASK);
}
private final BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
// because the GlobalEventExecutor is a singleton, tasks submitted to it can come from arbitrary threads and this
// can trigger the creation of a thread from arbitrary thread groups; for this reason, the thread factory must not
// be sticky about its thread group
// visible for testing
final ThreadFactory threadFactory;
private final TaskRunner taskRunner = new TaskRunner();
private final AtomicBoolean started = new AtomicBoolean();
volatile Thread thread;
private final Future<Void> terminationFuture = DefaultPromise.<Void>newFailedPromise(
this, new UnsupportedOperationException()).asFuture();
private GlobalEventExecutor() {
threadFactory = ThreadExecutorMap.apply(new DefaultThreadFactory(
DefaultThreadFactory.toPoolName(getClass()), false, Thread.NORM_PRIORITY, null), this);
}
/**
* Take the next {@link Runnable} from the task queue and so will block if no task is currently present.
*
* @return {@code null} if the executor thread has been interrupted or waken up.
*/
private Runnable takeTask() {
BlockingQueue<Runnable> taskQueue = this.taskQueue;
for (;;) {
RunnableScheduledFuture<?> scheduledTask = peekScheduledTask();
if (scheduledTask == null) {
Runnable task = null;
try {
task = taskQueue.take();
} catch (InterruptedException e) {
// Ignore
}
return task;
} else {
long delayNanos = scheduledTask.delayNanos();
Runnable task = null;
if (delayNanos > 0) {
try {
task = taskQueue.poll(delayNanos, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
// Waken up.
return null;
}
}
if (task == null) {
// We need to fetch the scheduled tasks now as otherwise there may be a chance that
// scheduled tasks are never executed if there is always one task in the taskQueue.
// This is for example true for the read task of OIO Transport
// See https://github.com/netty/netty/issues/1614
fetchFromScheduledTaskQueue();
task = taskQueue.poll();
}
if (task != null) {
return task;
}
}
}
}
private void fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) {
taskQueue.add(scheduledTask);
scheduledTask = pollScheduledTask(nanoTime);
}
}
/**
* Return the number of tasks that are pending for processing.
*/
public int pendingTasks() {
return taskQueue.size();
}
/**
* Add a task to the task queue, or throws a {@link RejectedExecutionException} if this instance was shutdown
* before.
*/
private void addTask(Runnable task) {
requireNonNull(task, "task");
taskQueue.add(task);
}
@Override
public boolean inEventLoop(Thread thread) {
return thread == this.thread;
}
@Override
public Future<Void> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
return terminationFuture();
}
@Override
public Future<Void> terminationFuture() {
return terminationFuture;
}
@Override
public boolean isShuttingDown() {
return false;
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) {
return false;
}
/**
* Waits until the worker thread of this executor has no tasks left in its task queue and terminates itself.
* Because a new worker thread will be started again when a new task is submitted, this operation is only useful
* when you want to ensure that the worker thread is terminated <strong>after</strong> your application is shut
* down and there's no chance of submitting a new task afterwards.
*
* @return {@code true} if and only if the worker thread has been terminated
*/
public boolean awaitInactivity(long timeout, TimeUnit unit) throws InterruptedException {
requireNonNull(unit, "unit");
final Thread thread = this.thread;
if (thread == null) {
throw new IllegalStateException("thread was not started");
}
thread.join(unit.toMillis(timeout));
return !thread.isAlive();
}
@Override
public void execute(Runnable task) {
requireNonNull(task, "task");
addTask(task);
if (!inEventLoop()) {
startThread();
}
}
private void startThread() {
if (started.compareAndSet(false, true)) {
final Thread t = threadFactory.newThread(taskRunner);
// Set to null to ensure we not create classloader leaks by holds a strong reference to the inherited
// classloader.
// See:
// - https://github.com/netty/netty/issues/7290
// - https://bugs.openjdk.java.net/browse/JDK-7008595
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
t.setContextClassLoader(null);
return null;
});
// Set the thread before starting it as otherwise inEventLoop() may return false and so produce
// an assert error.
// See https://github.com/netty/netty/issues/4357
thread = t;
t.start();
}
}
final class TaskRunner implements Runnable {
@Override
public void run() {
for (;;) {
Runnable task = takeTask();
if (task != null) {
try {
task.run();
} catch (Throwable t) {
logger.warn("Unexpected exception from the global event executor: ", t);
}
if (task != QUIET_PERIOD_TASK) {
continue;
}
}
Queue<RunnableScheduledFutureNode<?>> scheduledTaskQueue = scheduledTaskQueue();
// Terminate if there is no task in the queue (except the noop task).
if (taskQueue.isEmpty() && scheduledTaskQueue.size() <= 1) {
// Mark the current thread as stopped.
// The following CAS must always success and must be uncontended,
// because only one thread should be running at the same time.
boolean stopped = started.compareAndSet(true, false);
assert stopped;
// Do not check scheduledTaskQueue because it is not thread-safe and can only be mutated from a
// TaskRunner actively running tasks.
if (taskQueue.isEmpty()) {
// A) No new task was added and thus there's nothing to handle
// -> safe to terminate because there's nothing left to do
// B) A new thread started and handled all the new tasks.
// -> safe to terminate the new thread will take care the rest
break;
}
// There are pending tasks added again.
if (!started.compareAndSet(false, true)) {
// startThread() started a new thread and set 'started' to true.
// -> terminate this thread so that the new thread reads from taskQueue exclusively.
break;
}
// New tasks were added, but this worker was faster to set 'started' to true.
// i.e. a new worker thread was not started by startThread().
// -> keep this thread alive to handle the newly added entries.
}
}
}
}
}