From 6fd62571ba41b05d7c05f8b4be05beb4486b5010 Mon Sep 17 00:00:00 2001 From: "injae.kim" Date: Fri, 20 Oct 2023 16:55:44 +0900 Subject: [PATCH 1/2] Make ScheduledEventExecutor task scheduler pluggable --- .../AbstractScheduledEventExecutor.java | 18 +- .../util/internal/DefaultPriorityQueue.java | 4 +- .../util/internal/ScheduledTaskQueue.java | 40 +++++ .../AbstractScheduledEventExecutorTest.java | 159 ++++++++++++++++++ 4 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java diff --git a/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java b/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java index e40dd05cd5d..4a4bc13555e 100644 --- a/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java +++ b/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java @@ -16,13 +16,14 @@ package io.netty5.util.concurrent; import io.netty5.util.internal.DefaultPriorityQueue; -import io.netty5.util.internal.PriorityQueue; import io.netty5.util.internal.PriorityQueueNode; +import io.netty5.util.internal.ScheduledTaskQueue; import java.util.Comparator; import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import static java.util.Objects.requireNonNull; import static java.util.concurrent.Executors.callable; @@ -37,11 +38,17 @@ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecut private static final RunnableScheduledFutureNode[] EMPTY_RUNNABLE_SCHEDULED_FUTURE_NODES = new RunnableScheduledFutureNode[0]; - private PriorityQueue> scheduledTaskQueue; + private ScheduledTaskQueue> scheduledTaskQueue; + private Supplier>> scheduledTaskQueueSupplier; protected AbstractScheduledEventExecutor() { } + protected AbstractScheduledEventExecutor( + Supplier>> scheduledTaskQueueSupplier) { + this.scheduledTaskQueueSupplier = requireNonNull(scheduledTaskQueueSupplier, "scheduledTaskQueueSupplier"); + } + /** * Return the {@link Ticker} that provides the time source. */ @@ -55,7 +62,10 @@ static long deadlineNanos(long nanoTime, long delay) { return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos; } - PriorityQueue> scheduledTaskQueue() { + ScheduledTaskQueue> scheduledTaskQueue() { + if (scheduledTaskQueue == null && scheduledTaskQueueSupplier != null) { + scheduledTaskQueue = scheduledTaskQueueSupplier.get(); + } if (scheduledTaskQueue == null) { scheduledTaskQueue = new DefaultPriorityQueue<>( SCHEDULED_FUTURE_TASK_COMPARATOR, @@ -76,7 +86,7 @@ private static boolean isNullOrEmpty(Queue> queue */ protected final void cancelScheduledTasks() { assert inEventLoop(); - PriorityQueue> scheduledTaskQueue = this.scheduledTaskQueue; + ScheduledTaskQueue> scheduledTaskQueue = this.scheduledTaskQueue; if (isNullOrEmpty(scheduledTaskQueue)) { return; } diff --git a/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java b/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java index 4579776aa2a..d3ea8482518 100644 --- a/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java +++ b/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java @@ -29,8 +29,8 @@ * {@link PriorityQueueNode} for the purpose of maintaining the index in the priority queue. * @param The object that is maintained in the queue. */ -public final class DefaultPriorityQueue extends AbstractQueue - implements PriorityQueue { +public final class DefaultPriorityQueue + extends AbstractQueue implements PriorityQueue, ScheduledTaskQueue { private static final PriorityQueueNode[] EMPTY_ARRAY = new PriorityQueueNode[0]; private final Comparator comparator; private T[] queue; diff --git a/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java b/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java new file mode 100644 index 00000000000..77ac6c62cd3 --- /dev/null +++ b/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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.netty5.util.internal; + +import io.netty5.util.concurrent.EventExecutor; + +import java.util.Queue; + +/** + * A task scheduler for {@link EventExecutor}s that want to support task scheduling. + * @param The object that is scheduled in this {@link ScheduledTaskQueue}. + */ +public interface ScheduledTaskQueue extends Queue { + + /** + * Same as {@link #remove(Object)} but typed using generics. + */ + boolean removeTyped(T task); + + /** + * Removes all of the elements from this {@link ScheduledTaskQueue} without explicitly removing references + * to them to allow them to be garbage collected. This should only be used when it is certain that + * the nodes will not be re-inserted into this or any other {@link ScheduledTaskQueue} and it is known that + * the {@link ScheduledTaskQueue} itself will be garbage collected after this call. + */ + void clearIgnoringIndexes(); +} diff --git a/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java b/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java index 781f7c5314e..37e8815f58c 100644 --- a/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java +++ b/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java @@ -15,11 +15,18 @@ */ package io.netty5.util.concurrent; +import io.netty5.util.concurrent.AbstractScheduledEventExecutor.RunnableScheduledFutureNode; +import io.netty5.util.internal.ScheduledTaskQueue; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import java.util.Collection; +import java.util.Iterator; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -68,6 +75,42 @@ public void testScheduleCallableNegative() { assertNull(executor.pollScheduledTask()); } + @Test + public void testScheduledTaskQueueSupplier_scheduleRunnableZero() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + Future future = executor.schedule(TEST_RUNNABLE, 0, TimeUnit.NANOSECONDS); + assertEquals(0, getDelay(future)); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); + } + + @Test + public void testScheduledTaskQueueSupplier_testScheduleRunnableNegative() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + Future future = executor.schedule(TEST_RUNNABLE, -1, TimeUnit.NANOSECONDS); + assertEquals(0, getDelay(future)); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); + } + + @Test + public void testScheduledTaskQueueSupplier_testScheduleCallableZero() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + Future future = executor.schedule(TEST_CALLABLE, 0, TimeUnit.NANOSECONDS); + assertEquals(0, getDelay(future)); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); + } + + @Test + public void testScheduledTaskQueueSupplier_testScheduleCallableNegative() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + Future future = executor.schedule(TEST_CALLABLE, -1, TimeUnit.NANOSECONDS); + assertEquals(0, getDelay(future)); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); + } + private static long getDelay(Future future) { return ((RunnableScheduledFuture) future).delayNanos(); } @@ -107,6 +150,14 @@ public void testDeadlineNanosNotOverflow() { } private static final class TestScheduledEventExecutor extends AbstractScheduledEventExecutor { + private TestScheduledEventExecutor() { + } + + private TestScheduledEventExecutor( + Supplier>> scheduledTaskQueueSupplier) { + super(scheduledTaskQueueSupplier); + } + @Override public boolean isShuttingDown() { return false; @@ -147,4 +198,112 @@ public void execute(Runnable task) { throw new UnsupportedOperationException(); } } + + private static final class TestScheduledTaskQueue implements ScheduledTaskQueue> { + private final LinkedBlockingQueue> scheduledTaskQueue + = new LinkedBlockingQueue<>(); + + @Override + public boolean removeTyped(RunnableScheduledFutureNode task) { + return scheduledTaskQueue.remove(task); + } + + @Override + public void clearIgnoringIndexes() { + scheduledTaskQueue.clear(); + } + + @Override + public int size() { + return scheduledTaskQueue.size(); + } + + @Override + public boolean isEmpty() { + return scheduledTaskQueue.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return scheduledTaskQueue.contains(o); + } + + @NotNull + @Override + public Iterator> iterator() { + return scheduledTaskQueue.iterator(); + } + + @NotNull + @Override + public Object[] toArray() { + return scheduledTaskQueue.toArray(); + } + + @NotNull + @Override + public T[] toArray(@NotNull T[] a) { + return scheduledTaskQueue.toArray(a); + } + + @Override + public boolean add(RunnableScheduledFutureNode task) { + return scheduledTaskQueue.add(task); + } + + @Override + public boolean remove(Object o) { + return scheduledTaskQueue.remove(o); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + return scheduledTaskQueue.containsAll(c); + } + + @Override + public boolean addAll(@NotNull Collection> c) { + return scheduledTaskQueue.addAll(c); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + return scheduledTaskQueue.removeAll(c); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + return scheduledTaskQueue.retainAll(c); + } + + @Override + public void clear() { + scheduledTaskQueue.clear(); + } + + @Override + public boolean offer(RunnableScheduledFutureNode task) { + return scheduledTaskQueue.offer(task); + } + + @Override + public RunnableScheduledFutureNode remove() { + return scheduledTaskQueue.remove(); + } + + @Override + public RunnableScheduledFutureNode poll() { + return scheduledTaskQueue.poll(); + } + + @Override + public RunnableScheduledFutureNode element() { + return scheduledTaskQueue.element(); + } + + @Override + public RunnableScheduledFutureNode peek() { + return scheduledTaskQueue.peek(); + } + } } From 7995b22dc8dfd93e8532b49764cb391b6f43fb73 Mon Sep 17 00:00:00 2001 From: "injae.kim" Date: Mon, 23 Oct 2023 19:04:52 +0900 Subject: [PATCH 2/2] Introduce TaskScheduler, TaskSchedulerFactory interface --- .../AbstractScheduledEventExecutor.java | 286 ++----------- .../concurrent/AbstractTaskScheduler.java | 126 ++++++ .../util/concurrent/DefaultTaskScheduler.java | 221 ++++++++++ .../DefaultTaskSchedulerFactory.java | 32 ++ .../util/concurrent/GlobalEventExecutor.java | 13 +- .../concurrent/SingleThreadEventExecutor.java | 10 +- .../netty5/util/concurrent/TaskScheduler.java | 92 +++++ .../util/concurrent/TaskSchedulerFactory.java | 28 ++ .../util/internal/DefaultPriorityQueue.java | 4 +- .../util/internal/ScheduledTaskQueue.java | 40 -- .../AbstractScheduledEventExecutorTest.java | 209 +++++----- .../concurrent/DefaultTaskSchedulerTest.java | 385 ++++++++++++++++++ 12 files changed, 1037 insertions(+), 409 deletions(-) create mode 100644 common/src/main/java/io/netty5/util/concurrent/AbstractTaskScheduler.java create mode 100644 common/src/main/java/io/netty5/util/concurrent/DefaultTaskScheduler.java create mode 100644 common/src/main/java/io/netty5/util/concurrent/DefaultTaskSchedulerFactory.java create mode 100644 common/src/main/java/io/netty5/util/concurrent/TaskScheduler.java create mode 100644 common/src/main/java/io/netty5/util/concurrent/TaskSchedulerFactory.java delete mode 100644 common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java create mode 100644 common/src/test/java/io/netty5/util/concurrent/DefaultTaskSchedulerTest.java diff --git a/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java b/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java index 4a4bc13555e..8cc993377ea 100644 --- a/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java +++ b/common/src/main/java/io/netty5/util/concurrent/AbstractScheduledEventExecutor.java @@ -15,38 +15,26 @@ */ package io.netty5.util.concurrent; -import io.netty5.util.internal.DefaultPriorityQueue; import io.netty5.util.internal.PriorityQueueNode; -import io.netty5.util.internal.ScheduledTaskQueue; -import java.util.Comparator; -import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static java.util.Objects.requireNonNull; -import static java.util.concurrent.Executors.callable; /** * Abstract base class for {@link EventExecutor}s that want to support scheduling. */ public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor { - private static final Comparator> SCHEDULED_FUTURE_TASK_COMPARATOR = - Comparable::compareTo; - private static final RunnableScheduledFutureNode[] - EMPTY_RUNNABLE_SCHEDULED_FUTURE_NODES = new RunnableScheduledFutureNode[0]; - - private ScheduledTaskQueue> scheduledTaskQueue; - private Supplier>> scheduledTaskQueueSupplier; + private TaskScheduler taskScheduler; + private TaskSchedulerFactory taskSchedulerFactory; protected AbstractScheduledEventExecutor() { } - protected AbstractScheduledEventExecutor( - Supplier>> scheduledTaskQueueSupplier) { - this.scheduledTaskQueueSupplier = requireNonNull(scheduledTaskQueueSupplier, "scheduledTaskQueueSupplier"); + protected AbstractScheduledEventExecutor(TaskSchedulerFactory taskSchedulerFactory) { + this.taskSchedulerFactory = requireNonNull(taskSchedulerFactory, "taskSchedulerFactory"); } /** @@ -62,21 +50,18 @@ static long deadlineNanos(long nanoTime, long delay) { return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos; } - ScheduledTaskQueue> scheduledTaskQueue() { - if (scheduledTaskQueue == null && scheduledTaskQueueSupplier != null) { - scheduledTaskQueue = scheduledTaskQueueSupplier.get(); + TaskScheduler taskScheduler() { + if (taskScheduler == null && taskSchedulerFactory != null) { + taskScheduler = taskSchedulerFactory.newTaskScheduler(this); } - if (scheduledTaskQueue == null) { - scheduledTaskQueue = new DefaultPriorityQueue<>( - SCHEDULED_FUTURE_TASK_COMPARATOR, - // Use same initial capacity as java.util.PriorityQueue - 11); + if (taskScheduler == null) { + taskScheduler = DefaultTaskSchedulerFactory.ISTANCE.newTaskScheduler(this); } - return scheduledTaskQueue; + return taskScheduler; } - private static boolean isNullOrEmpty(Queue> queue) { - return queue == null || queue.isEmpty(); + private static boolean isNullOrEmpty(TaskScheduler taskScheduler) { + return taskScheduler == null || taskScheduler.isEmpty(); } /** @@ -86,19 +71,11 @@ private static boolean isNullOrEmpty(Queue> queue */ protected final void cancelScheduledTasks() { assert inEventLoop(); - ScheduledTaskQueue> scheduledTaskQueue = this.scheduledTaskQueue; - if (isNullOrEmpty(scheduledTaskQueue)) { + TaskScheduler taskScheduler = this.taskScheduler; + if (isNullOrEmpty(taskScheduler)) { return; } - - final RunnableScheduledFutureNode[] scheduledTasks = - scheduledTaskQueue.toArray(EMPTY_RUNNABLE_SCHEDULED_FUTURE_NODES); - - for (RunnableScheduledFutureNode task : scheduledTasks) { - task.cancel(); - } - - scheduledTaskQueue.clearIgnoringIndexes(); + taskScheduler.cancelScheduledTasks(); } /** @@ -116,18 +93,8 @@ protected final RunnableScheduledFuture pollScheduledTask() { */ protected final RunnableScheduledFuture pollScheduledTask(long nanoTime) { assert inEventLoop(); - - Queue> scheduledTaskQueue = this.scheduledTaskQueue; - RunnableScheduledFutureNode scheduledTask = scheduledTaskQueue == null? null : scheduledTaskQueue.peek(); - if (scheduledTask == null) { - return null; - } - - if (scheduledTask.deadlineNanos() <= nanoTime) { - scheduledTaskQueue.remove(); - return scheduledTask; - } - return null; + TaskScheduler taskScheduler = this.taskScheduler; + return taskScheduler == null? null : taskScheduler.pollScheduledTask(nanoTime); } /** @@ -136,8 +103,8 @@ protected final RunnableScheduledFuture pollScheduledTask(long nanoTime) { * This method MUST be called only when {@link #inEventLoop()} is {@code true}. */ protected final long nextScheduledTaskNano() { - Queue> scheduledTaskQueue = this.scheduledTaskQueue; - RunnableScheduledFutureNode scheduledTask = scheduledTaskQueue == null? null : scheduledTaskQueue.peek(); + assert inEventLoop(); + RunnableScheduledFuture scheduledTask = peekScheduledTask(); if (scheduledTask == null) { return -1; } @@ -145,11 +112,8 @@ protected final long nextScheduledTaskNano() { } final RunnableScheduledFuture peekScheduledTask() { - Queue> scheduledTaskQueue = this.scheduledTaskQueue; - if (scheduledTaskQueue == null) { - return null; - } - return scheduledTaskQueue.peek(); + TaskScheduler taskScheduler = this.taskScheduler; + return taskScheduler == null? null : taskScheduler.peekScheduledTask(); } /** @@ -159,235 +123,45 @@ final RunnableScheduledFuture peekScheduledTask() { */ protected final boolean hasScheduledTasks() { assert inEventLoop(); - Queue> scheduledTaskQueue = this.scheduledTaskQueue; - RunnableScheduledFutureNode scheduledTask = scheduledTaskQueue == null? null : scheduledTaskQueue.peek(); + RunnableScheduledFuture scheduledTask = peekScheduledTask(); return scheduledTask != null && scheduledTask.deadlineNanos() <= ticker().nanoTime(); } @Override public Future schedule(Runnable command, long delay, TimeUnit unit) { - requireNonNull(command, "command"); - requireNonNull(unit, "unit"); - if (delay < 0) { - delay = 0; - } - RunnableScheduledFuture task = newScheduledTaskFor( - callable(command, null), deadlineNanos(ticker().nanoTime(), unit.toNanos(delay)), 0); - return schedule(task); + return taskScheduler().schedule(command, delay, unit); } @Override public Future schedule(Callable callable, long delay, TimeUnit unit) { - requireNonNull(callable, "callable"); - requireNonNull(unit, "unit"); - if (delay < 0) { - delay = 0; - } - RunnableScheduledFuture task = newScheduledTaskFor( - callable, deadlineNanos(ticker().nanoTime(), unit.toNanos(delay)), 0); - return schedule(task); + return taskScheduler().schedule(callable, delay, unit); } @Override public Future scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { - requireNonNull(command, "command"); - requireNonNull(unit, "unit"); - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (period <= 0) { - throw new IllegalArgumentException( - String.format("period: %d (expected: > 0)", period)); - } - - RunnableScheduledFuture task = newScheduledTaskFor( - callable(command, null), - deadlineNanos(ticker().nanoTime(), unit.toNanos(initialDelay)), unit.toNanos(period)); - return schedule(task); + return taskScheduler().scheduleAtFixedRate(command, initialDelay, period, unit); } @Override public Future scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { - requireNonNull(command, "command"); - requireNonNull(unit, "unit"); - if (initialDelay < 0) { - throw new IllegalArgumentException( - String.format("initialDelay: %d (expected: >= 0)", initialDelay)); - } - if (delay <= 0) { - throw new IllegalArgumentException( - String.format("delay: %d (expected: > 0)", delay)); - } - - RunnableScheduledFuture task = newScheduledTaskFor( - callable(command, null), - deadlineNanos(ticker().nanoTime(), unit.toNanos(initialDelay)), -unit.toNanos(delay)); - return schedule(task); + return taskScheduler().scheduleWithFixedDelay(command, initialDelay, delay, unit); } /** - * Add the {@link RunnableScheduledFuture} for execution. + * Schedule the {@link RunnableScheduledFuture} for execution. */ protected final Future schedule(final RunnableScheduledFuture task) { - if (inEventLoop()) { - add0(task); - } else { - execute(() -> add0(task)); - } - return task; - } - - private void add0(RunnableScheduledFuture task) { - final RunnableScheduledFutureNode node; - if (task instanceof RunnableScheduledFutureNode) { - node = (RunnableScheduledFutureNode) task; - } else { - node = new DefaultRunnableScheduledFutureNode<>(task); - } - scheduledTaskQueue().add(node); + return taskScheduler().schedule(task); } - final void removeScheduled(final RunnableScheduledFutureNode task) { + final void removeScheduled(final RunnableScheduledFuture task) { if (inEventLoop()) { - scheduledTaskQueue().removeTyped(task); + taskScheduler().removeScheduled(task); } else { execute(() -> removeScheduled(task)); } } - /** - * Returns a new {@link RunnableFuture} build on top of the given {@link Promise} and {@link Callable}. - *

- * This can be used if you want to override {@link #newScheduledTaskFor(Callable, long, long)} and return a - * different {@link RunnableFuture}. - */ - protected static RunnableScheduledFuture newRunnableScheduledFuture( - AbstractScheduledEventExecutor executor, Promise promise, Callable task, - long deadlineNanos, long periodNanos) { - return new RunnableScheduledFutureAdapter<>(executor, promise, task, deadlineNanos, periodNanos); - } - - /** - * Returns a {@code RunnableScheduledFuture} for the given values. - */ - protected RunnableScheduledFuture newScheduledTaskFor( - Callable callable, long deadlineNanos, long period) { - return newRunnableScheduledFuture(this, newPromise(), callable, deadlineNanos, period); - } - interface RunnableScheduledFutureNode extends PriorityQueueNode, RunnableScheduledFuture { } - - private static final class DefaultRunnableScheduledFutureNode implements RunnableScheduledFutureNode { - private final RunnableScheduledFuture future; - private int queueIndex = INDEX_NOT_IN_QUEUE; - - DefaultRunnableScheduledFutureNode(RunnableScheduledFuture future) { - this.future = future; - } - - @Override - public EventExecutor executor() { - return future.executor(); - } - - @Override - public long deadlineNanos() { - return future.deadlineNanos(); - } - - @Override - public long delayNanos() { - return future.delayNanos(); - } - - @Override - public long delayNanos(long currentTimeNanos) { - return future.delayNanos(currentTimeNanos); - } - - @Override - public RunnableScheduledFuture addListener(FutureListener listener) { - future.addListener(listener); - return this; - } - - @Override - public RunnableScheduledFuture addListener( - C context, FutureContextListener listener) { - future.addListener(context, listener); - return this; - } - - @Override - public boolean isPeriodic() { - return future.isPeriodic(); - } - - @Override - public int priorityQueueIndex(DefaultPriorityQueue queue) { - return queueIndex; - } - - @Override - public void priorityQueueIndex(DefaultPriorityQueue queue, int i) { - queueIndex = i; - } - - @Override - public void run() { - future.run(); - } - - @Override - public boolean cancel() { - return future.cancel(); - } - - @Override - public boolean isCancelled() { - return future.isCancelled(); - } - - @Override - public boolean isDone() { - return future.isDone(); - } - - @Override - public FutureCompletionStage asStage() { - return future.asStage(); - } - - @Override - public int compareTo(RunnableScheduledFuture o) { - return future.compareTo(o); - } - - @Override - public boolean isSuccess() { - return future.isSuccess(); - } - - @Override - public boolean isFailed() { - return future.isFailed(); - } - - @Override - public boolean isCancellable() { - return future.isCancellable(); - } - - @Override - public Throwable cause() { - return future.cause(); - } - - @Override - public V getNow() { - return future.getNow(); - } - } } diff --git a/common/src/main/java/io/netty5/util/concurrent/AbstractTaskScheduler.java b/common/src/main/java/io/netty5/util/concurrent/AbstractTaskScheduler.java new file mode 100644 index 00000000000..5b611b5f1f4 --- /dev/null +++ b/common/src/main/java/io/netty5/util/concurrent/AbstractTaskScheduler.java @@ -0,0 +1,126 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static io.netty5.util.concurrent.AbstractScheduledEventExecutor.deadlineNanos; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.callable; + +/** + * Abstract base class for {@link TaskScheduler} that want to support scheduling. + * + * @see AbstractScheduledEventExecutor + */ +public abstract class AbstractTaskScheduler implements TaskScheduler { + + /** + * Returns a {@link RunnableScheduledFuture} for the given values. + */ + protected abstract RunnableScheduledFuture newScheduledTaskFor(Callable callable, + long deadlineNanos, long period); + + protected abstract Ticker ticker(); + + @Override + public Future schedule(Runnable command, long delay, TimeUnit unit) { + requireNonNull(command, "command"); + requireNonNull(unit, "unit"); + if (delay < 0) { + delay = 0; + } + RunnableScheduledFuture task = newScheduledTaskFor( + callable(command, null), deadlineNanos(ticker().nanoTime(), unit.toNanos(delay)), 0); + return schedule(task); + } + + @Override + public Future schedule(Callable callable, long delay, TimeUnit unit) { + requireNonNull(callable, "callable"); + requireNonNull(unit, "unit"); + if (delay < 0) { + delay = 0; + } + RunnableScheduledFuture task = newScheduledTaskFor( + callable, deadlineNanos(ticker().nanoTime(), unit.toNanos(delay)), 0); + return schedule(task); + } + + @Override + public Future scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + requireNonNull(command, "command"); + requireNonNull(unit, "unit"); + if (initialDelay < 0) { + throw new IllegalArgumentException( + String.format("initialDelay: %d (expected: >= 0)", initialDelay)); + } + if (period <= 0) { + throw new IllegalArgumentException( + String.format("period: %d (expected: > 0)", period)); + } + + RunnableScheduledFuture task = newScheduledTaskFor( + callable(command, null), + deadlineNanos(ticker().nanoTime(), unit.toNanos(initialDelay)), unit.toNanos(period)); + return schedule(task); + } + + @Override + public Future scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + requireNonNull(command, "command"); + requireNonNull(unit, "unit"); + if (initialDelay < 0) { + throw new IllegalArgumentException( + String.format("initialDelay: %d (expected: >= 0)", initialDelay)); + } + if (delay <= 0) { + throw new IllegalArgumentException( + String.format("delay: %d (expected: > 0)", delay)); + } + + RunnableScheduledFuture task = newScheduledTaskFor( + callable(command, null), + deadlineNanos(ticker().nanoTime(), unit.toNanos(initialDelay)), -unit.toNanos(delay)); + return schedule(task); + } + + @Override + public RunnableScheduledFuture pollScheduledTask() { + return pollScheduledTask(ticker().nanoTime()); + } + + @Override + public RunnableScheduledFuture pollScheduledTask(long nanoTime) { + RunnableScheduledFuture scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { + return null; + } + + if (scheduledTask.deadlineNanos() <= nanoTime) { + removeNextScheduledTask(); + return scheduledTask; + } + return null; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } +} diff --git a/common/src/main/java/io/netty5/util/concurrent/DefaultTaskScheduler.java b/common/src/main/java/io/netty5/util/concurrent/DefaultTaskScheduler.java new file mode 100644 index 00000000000..2c9f10b11a6 --- /dev/null +++ b/common/src/main/java/io/netty5/util/concurrent/DefaultTaskScheduler.java @@ -0,0 +1,221 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +import io.netty5.util.concurrent.AbstractScheduledEventExecutor.RunnableScheduledFutureNode; +import io.netty5.util.internal.DefaultPriorityQueue; +import io.netty5.util.internal.PriorityQueue; + +import java.util.Comparator; +import java.util.concurrent.Callable; + +import static java.util.Objects.requireNonNull; + +/** + * Default task scheduler for {@link AbstractScheduledEventExecutor} that want to support task scheduling. + * Uses {@link DefaultPriorityQueue} to schdeule tasks. + */ +public final class DefaultTaskScheduler extends AbstractTaskScheduler { + + private static final Comparator> SCHEDULED_FUTURE_TASK_COMPARATOR = + Comparable::compareTo; + private static final RunnableScheduledFutureNode[] + EMPTY_RUNNABLE_SCHEDULED_FUTURE_NODES = new RunnableScheduledFutureNode[0]; + + private final AbstractScheduledEventExecutor executor; + private final PriorityQueue> scheduledTaskQueue; + + public DefaultTaskScheduler(AbstractScheduledEventExecutor executor) { + this.executor = requireNonNull(executor, "executor"); + scheduledTaskQueue = new DefaultPriorityQueue<>( + SCHEDULED_FUTURE_TASK_COMPARATOR, + // Use same initial capacity as java.util.PriorityQueue + 11); + } + + @Override + protected RunnableScheduledFuture newScheduledTaskFor(Callable callable, long deadlineNanos, + long period) { + return new RunnableScheduledFutureAdapter<>(executor, executor.newPromise(), callable, deadlineNanos, period); + } + + @Override + protected Ticker ticker() { + return executor.ticker(); + } + + @Override + public Future schedule(RunnableScheduledFuture task) { + if (executor.inEventLoop()) { + final RunnableScheduledFutureNode node; + if (task instanceof RunnableScheduledFutureNode) { + node = (RunnableScheduledFutureNode) task; + } else { + node = new DefaultRunnableScheduledFutureNode<>(task); + } + scheduledTaskQueue.add(node); + return node; + } else { + executor.execute(() -> schedule(task)); + return task; + } + } + + @Override + public RunnableScheduledFuture peekScheduledTask() { + return scheduledTaskQueue.peek(); + } + + @Override + public void removeNextScheduledTask() { + scheduledTaskQueue.remove(); + } + + @Override + public void removeScheduled(RunnableScheduledFuture task) { + scheduledTaskQueue.remove(task); + } + + @Override + public void cancelScheduledTasks() { + final RunnableScheduledFutureNode[] scheduledTasks = + scheduledTaskQueue.toArray(EMPTY_RUNNABLE_SCHEDULED_FUTURE_NODES); + + for (RunnableScheduledFutureNode task : scheduledTasks) { + task.cancel(); + } + + scheduledTaskQueue.clearIgnoringIndexes(); + } + + @Override + public int size() { + return scheduledTaskQueue.size(); + } + + private static final class DefaultRunnableScheduledFutureNode implements RunnableScheduledFutureNode { + private final RunnableScheduledFuture future; + private int queueIndex = INDEX_NOT_IN_QUEUE; + + DefaultRunnableScheduledFutureNode(RunnableScheduledFuture future) { + this.future = future; + } + + @Override + public EventExecutor executor() { + return future.executor(); + } + + @Override + public long deadlineNanos() { + return future.deadlineNanos(); + } + + @Override + public long delayNanos() { + return future.delayNanos(); + } + + @Override + public long delayNanos(long currentTimeNanos) { + return future.delayNanos(currentTimeNanos); + } + + @Override + public RunnableScheduledFuture addListener(FutureListener listener) { + future.addListener(listener); + return this; + } + + @Override + public RunnableScheduledFuture addListener( + C context, FutureContextListener listener) { + future.addListener(context, listener); + return this; + } + + @Override + public boolean isPeriodic() { + return future.isPeriodic(); + } + + @Override + public int priorityQueueIndex(DefaultPriorityQueue queue) { + return queueIndex; + } + + @Override + public void priorityQueueIndex(DefaultPriorityQueue queue, int i) { + queueIndex = i; + } + + @Override + public void run() { + future.run(); + } + + @Override + public boolean cancel() { + return future.cancel(); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public boolean isDone() { + return future.isDone(); + } + + @Override + public FutureCompletionStage asStage() { + return future.asStage(); + } + + @Override + public int compareTo(RunnableScheduledFuture o) { + return future.compareTo(o); + } + + @Override + public boolean isSuccess() { + return future.isSuccess(); + } + + @Override + public boolean isFailed() { + return future.isFailed(); + } + + @Override + public boolean isCancellable() { + return future.isCancellable(); + } + + @Override + public Throwable cause() { + return future.cause(); + } + + @Override + public V getNow() { + return future.getNow(); + } + } +} diff --git a/common/src/main/java/io/netty5/util/concurrent/DefaultTaskSchedulerFactory.java b/common/src/main/java/io/netty5/util/concurrent/DefaultTaskSchedulerFactory.java new file mode 100644 index 00000000000..6673b625318 --- /dev/null +++ b/common/src/main/java/io/netty5/util/concurrent/DefaultTaskSchedulerFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +/** + * Factory that creates a new {@link DefaultTaskScheduler} every time. + */ +public final class DefaultTaskSchedulerFactory implements TaskSchedulerFactory { + + public static final TaskSchedulerFactory ISTANCE = new DefaultTaskSchedulerFactory(); + + private DefaultTaskSchedulerFactory() { } + + @Override + public TaskScheduler newTaskScheduler(AbstractScheduledEventExecutor executor) { + return new DefaultTaskScheduler(executor); + } +} diff --git a/common/src/main/java/io/netty5/util/concurrent/GlobalEventExecutor.java b/common/src/main/java/io/netty5/util/concurrent/GlobalEventExecutor.java index ec83b15e592..3528c9d6734 100644 --- a/common/src/main/java/io/netty5/util/concurrent/GlobalEventExecutor.java +++ b/common/src/main/java/io/netty5/util/concurrent/GlobalEventExecutor.java @@ -25,7 +25,6 @@ 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; @@ -91,7 +90,7 @@ this, newPromise(), Executors.callable(() -> { deadlineNanos(ticker().nanoTime(), SCHEDULE_QUIET_PERIOD_INTERVAL), -SCHEDULE_QUIET_PERIOD_INTERVAL); - scheduledTaskQueue().add(quietPeriodTask); + taskScheduler().schedule(quietPeriodTask); } /** @@ -127,7 +126,7 @@ private Runnable takeTask() { // 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(); + fetchFromTaskScheduler(); task = taskQueue.poll(); } @@ -138,7 +137,7 @@ private Runnable takeTask() { } } - private void fetchFromScheduledTaskQueue() { + private void fetchFromTaskScheduler() { long nanoTime = ticker().nanoTime(); Runnable scheduledTask = pollScheduledTask(nanoTime); while (scheduledTask != null) { @@ -265,16 +264,16 @@ public void run() { } } - Queue> scheduledTaskQueue = scheduledTaskQueue(); + TaskScheduler taskScheduler = taskScheduler(); // Terminate if there is no task in the queue (except the noop task). - if (taskQueue.isEmpty() && scheduledTaskQueue.size() <= 1) { + if (taskQueue.isEmpty() && taskScheduler.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 + // Do not check task scheduler 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 diff --git a/common/src/main/java/io/netty5/util/concurrent/SingleThreadEventExecutor.java b/common/src/main/java/io/netty5/util/concurrent/SingleThreadEventExecutor.java index bb014844531..b038e3e685e 100644 --- a/common/src/main/java/io/netty5/util/concurrent/SingleThreadEventExecutor.java +++ b/common/src/main/java/io/netty5/util/concurrent/SingleThreadEventExecutor.java @@ -237,7 +237,7 @@ protected final Runnable takeTask() { // 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(); + fetchFromTaskScheduler(); task = taskQueue.poll(); } @@ -248,12 +248,12 @@ protected final Runnable takeTask() { } } - private boolean fetchFromScheduledTaskQueue() { + private boolean fetchFromTaskScheduler() { long nanoTime = ticker().nanoTime(); RunnableScheduledFuture scheduledTask = pollScheduledTask(nanoTime); while (scheduledTask != null) { if (!taskQueue.offer(scheduledTask)) { - // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again. + // No space left in the task queue add it back to the task scheduler so we pick it up again. schedule(scheduledTask); return false; } @@ -314,7 +314,7 @@ protected final boolean removeTask(Runnable task) { private boolean runAllTasks() { boolean fetchedAll; do { - fetchedAll = fetchFromScheduledTaskQueue(); + fetchedAll = fetchFromTaskScheduler(); Runnable task = pollTask(); if (task == null) { return false; @@ -349,7 +349,7 @@ protected int runAllTasks(int maxTasks) { boolean fetchedAll; int processedTasks = 0; do { - fetchedAll = fetchFromScheduledTaskQueue(); + fetchedAll = fetchFromTaskScheduler(); for (; processedTasks < maxTasks; processedTasks++) { Runnable task = pollTask(); if (task == null) { diff --git a/common/src/main/java/io/netty5/util/concurrent/TaskScheduler.java b/common/src/main/java/io/netty5/util/concurrent/TaskScheduler.java new file mode 100644 index 00000000000..def50f119e6 --- /dev/null +++ b/common/src/main/java/io/netty5/util/concurrent/TaskScheduler.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +/** + * A task scheduler for {@link EventExecutor}s that want to support task scheduling. + * + * @see AbstractScheduledEventExecutor + */ +public interface TaskScheduler { + + /** + * Schedule the {@link RunnableScheduledFuture} task for execution. + */ + Future schedule(RunnableScheduledFuture task); + + /** + * Schedule the given {@link Runnable} task for execution after the given delay. + */ + Future schedule(Runnable command, long delay, TimeUnit unit); + + /** + * Schedule the given {@link Callable} task for execution after the given delay. + */ + Future schedule(Callable callable, long delay, TimeUnit unit); + + /** + * Schedule the given {@link Runnable} task for periodic execution. + * The first execution will occur after the given initial delay, and the following repeated executions will occur + * with the given period of time between each execution is started. + * If the task takes longer to complete than the requested period, then the following executions will be delayed, + * rather than allowing multiple instances of the task to run concurrently. + *

+ * The task will be executed repeatedly until it either fails with an exception, or its future is + * {@linkplain Future#cancel() cancelled}. The future thus will never complete successfully. + */ + Future scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); + + /** + * Schedule the given {@link Runnable} task for periodic execution. + * The first execution will occur after the given initial delay, and the following repeated executions will occur + * with the given subsequent delay between one task completing and the next task starting. + * The delay from the completion of one task, to the start of the next, stays unchanged regardless of how long a + * task takes to complete. + *

+ * This is in contrast to {@link #scheduleAtFixedRate(Runnable, long, long, TimeUnit)} which varies the delays + * between the tasks in order to hit a given frequency. + *

+ * The task will be executed repeatedly until it either fails with an exception, or its future is + * {@linkplain Future#cancel() cancelled}. The future thus will never complete successfully. + */ + Future scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); + + RunnableScheduledFuture peekScheduledTask(); + + RunnableScheduledFuture pollScheduledTask(); + + /** + * Return the task which is ready to be executed with the given {@code nanoTime}. + */ + RunnableScheduledFuture pollScheduledTask(long nanoTime); + + void removeNextScheduledTask(); + + void removeScheduled(RunnableScheduledFuture task); + + /** + * Cancel all scheduled tasks. + */ + void cancelScheduledTasks(); + + int size(); + + boolean isEmpty(); +} diff --git a/common/src/main/java/io/netty5/util/concurrent/TaskSchedulerFactory.java b/common/src/main/java/io/netty5/util/concurrent/TaskSchedulerFactory.java new file mode 100644 index 00000000000..b5ca806494b --- /dev/null +++ b/common/src/main/java/io/netty5/util/concurrent/TaskSchedulerFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +/** + * Factory that creates a new {@link TaskScheduler} every time. + */ +public interface TaskSchedulerFactory { + + /** + * Creates a new {@link TaskScheduler}. + */ + TaskScheduler newTaskScheduler(AbstractScheduledEventExecutor executor); +} diff --git a/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java b/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java index d3ea8482518..4579776aa2a 100644 --- a/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java +++ b/common/src/main/java/io/netty5/util/internal/DefaultPriorityQueue.java @@ -29,8 +29,8 @@ * {@link PriorityQueueNode} for the purpose of maintaining the index in the priority queue. * @param The object that is maintained in the queue. */ -public final class DefaultPriorityQueue - extends AbstractQueue implements PriorityQueue, ScheduledTaskQueue { +public final class DefaultPriorityQueue extends AbstractQueue + implements PriorityQueue { private static final PriorityQueueNode[] EMPTY_ARRAY = new PriorityQueueNode[0]; private final Comparator comparator; private T[] queue; diff --git a/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java b/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java deleted file mode 100644 index 77ac6c62cd3..00000000000 --- a/common/src/main/java/io/netty5/util/internal/ScheduledTaskQueue.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 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.netty5.util.internal; - -import io.netty5.util.concurrent.EventExecutor; - -import java.util.Queue; - -/** - * A task scheduler for {@link EventExecutor}s that want to support task scheduling. - * @param The object that is scheduled in this {@link ScheduledTaskQueue}. - */ -public interface ScheduledTaskQueue extends Queue { - - /** - * Same as {@link #remove(Object)} but typed using generics. - */ - boolean removeTyped(T task); - - /** - * Removes all of the elements from this {@link ScheduledTaskQueue} without explicitly removing references - * to them to allow them to be garbage collected. This should only be used when it is certain that - * the nodes will not be re-inserted into this or any other {@link ScheduledTaskQueue} and it is known that - * the {@link ScheduledTaskQueue} itself will be garbage collected after this call. - */ - void clearIgnoringIndexes(); -} diff --git a/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java b/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java index 37e8815f58c..a4ecb50e431 100644 --- a/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java +++ b/common/src/test/java/io/netty5/util/concurrent/AbstractScheduledEventExecutorTest.java @@ -15,18 +15,12 @@ */ package io.netty5.util.concurrent; -import io.netty5.util.concurrent.AbstractScheduledEventExecutor.RunnableScheduledFutureNode; -import io.netty5.util.internal.ScheduledTaskQueue; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; -import java.util.Collection; -import java.util.Iterator; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -76,8 +70,9 @@ public void testScheduleCallableNegative() { } @Test - public void testScheduledTaskQueueSupplier_scheduleRunnableZero() { - TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + public void testTaskScheduler_scheduleRunnableZero() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor)); Future future = executor.schedule(TEST_RUNNABLE, 0, TimeUnit.NANOSECONDS); assertEquals(0, getDelay(future)); assertNotNull(executor.pollScheduledTask()); @@ -85,8 +80,9 @@ public void testScheduledTaskQueueSupplier_scheduleRunnableZero() { } @Test - public void testScheduledTaskQueueSupplier_testScheduleRunnableNegative() { - TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + public void testTaskScheduler_testScheduleRunnableNegative() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor)); Future future = executor.schedule(TEST_RUNNABLE, -1, TimeUnit.NANOSECONDS); assertEquals(0, getDelay(future)); assertNotNull(executor.pollScheduledTask()); @@ -94,8 +90,9 @@ public void testScheduledTaskQueueSupplier_testScheduleRunnableNegative() { } @Test - public void testScheduledTaskQueueSupplier_testScheduleCallableZero() { - TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + public void testTaskScheduler_testScheduleCallableZero() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor)); Future future = executor.schedule(TEST_CALLABLE, 0, TimeUnit.NANOSECONDS); assertEquals(0, getDelay(future)); assertNotNull(executor.pollScheduledTask()); @@ -103,8 +100,9 @@ public void testScheduledTaskQueueSupplier_testScheduleCallableZero() { } @Test - public void testScheduledTaskQueueSupplier_testScheduleCallableNegative() { - TestScheduledEventExecutor executor = new TestScheduledEventExecutor(TestScheduledTaskQueue::new); + public void testTaskScheduler_testScheduleCallableNegative() { + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor)); Future future = executor.schedule(TEST_CALLABLE, -1, TimeUnit.NANOSECONDS); assertEquals(0, getDelay(future)); assertNotNull(executor.pollScheduledTask()); @@ -119,28 +117,79 @@ private static long getDelay(Future future) { public void testScheduleAtFixedRateRunnableZero() { TestScheduledEventExecutor executor = new TestScheduledEventExecutor(); assertThrows(IllegalArgumentException.class, - () -> executor.scheduleAtFixedRate(TEST_RUNNABLE, 0, 0, TimeUnit.DAYS)); + () -> executor.scheduleAtFixedRate(TEST_RUNNABLE, 0, 0, TimeUnit.DAYS)); } @Test public void testScheduleAtFixedRateRunnableNegative() { TestScheduledEventExecutor executor = new TestScheduledEventExecutor(); assertThrows(IllegalArgumentException.class, - () -> executor.scheduleAtFixedRate(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); + () -> executor.scheduleAtFixedRate(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); } @Test public void testScheduleWithFixedDelayZero() { TestScheduledEventExecutor executor = new TestScheduledEventExecutor(); assertThrows(IllegalArgumentException.class, - () -> executor.scheduleWithFixedDelay(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); + () -> executor.scheduleWithFixedDelay(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); } @Test public void testScheduleWithFixedDelayNegative() { TestScheduledEventExecutor executor = new TestScheduledEventExecutor(); assertThrows(IllegalArgumentException.class, - () -> executor.scheduleWithFixedDelay(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); + () -> executor.scheduleWithFixedDelay(TEST_RUNNABLE, 0, -1, TimeUnit.DAYS)); + } + + @Test + public void testTaskScheduler_scheduleAtFixedRate() { + MockTicker ticker = Ticker.newMockTicker(); + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor), ticker); + executor.scheduleAtFixedRate(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(100); + executor.pollScheduledTask().run(); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(150); + executor.pollScheduledTask().run(); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(50); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); + } + + @Test + public void testTaskScheduler_scheduleAtFixedDelay() { + MockTicker ticker = Ticker.newMockTicker(); + TestScheduledEventExecutor executor = new TestScheduledEventExecutor( + excutor -> new TestTaskScheduler(excutor), ticker); + executor.scheduleWithFixedDelay(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(100); + executor.pollScheduledTask().run(); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(150); + executor.pollScheduledTask().run(); + assertNotNull(executor.peekScheduledTask()); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(50); + assertNull(executor.pollScheduledTask()); + + ticker.advanceMillis(50); + assertNotNull(executor.pollScheduledTask()); + assertNull(executor.pollScheduledTask()); } @Test @@ -150,12 +199,24 @@ public void testDeadlineNanosNotOverflow() { } private static final class TestScheduledEventExecutor extends AbstractScheduledEventExecutor { + + private Ticker ticker; + private TestScheduledEventExecutor() { } - private TestScheduledEventExecutor( - Supplier>> scheduledTaskQueueSupplier) { - super(scheduledTaskQueueSupplier); + private TestScheduledEventExecutor(TaskSchedulerFactory taskSchedulerFactory) { + super(taskSchedulerFactory); + } + + private TestScheduledEventExecutor(TaskSchedulerFactory taskSchedulerFactory, Ticker ticker) { + super(taskSchedulerFactory); + this.ticker = ticker; + } + + @Override + protected Ticker ticker() { + return ticker != null ? ticker : super.ticker(); } @Override @@ -199,111 +260,61 @@ public void execute(Runnable task) { } } - private static final class TestScheduledTaskQueue implements ScheduledTaskQueue> { - private final LinkedBlockingQueue> scheduledTaskQueue - = new LinkedBlockingQueue<>(); - - @Override - public boolean removeTyped(RunnableScheduledFutureNode task) { - return scheduledTaskQueue.remove(task); - } - - @Override - public void clearIgnoringIndexes() { - scheduledTaskQueue.clear(); - } - - @Override - public int size() { - return scheduledTaskQueue.size(); - } - - @Override - public boolean isEmpty() { - return scheduledTaskQueue.isEmpty(); - } - - @Override - public boolean contains(Object o) { - return scheduledTaskQueue.contains(o); - } - - @NotNull - @Override - public Iterator> iterator() { - return scheduledTaskQueue.iterator(); - } - - @NotNull - @Override - public Object[] toArray() { - return scheduledTaskQueue.toArray(); - } - - @NotNull - @Override - public T[] toArray(@NotNull T[] a) { - return scheduledTaskQueue.toArray(a); - } + private static final class TestTaskScheduler extends AbstractTaskScheduler { - @Override - public boolean add(RunnableScheduledFutureNode task) { - return scheduledTaskQueue.add(task); - } + private final AbstractScheduledEventExecutor executor; + private final LinkedBlockingQueue> scheduledTaskQueue; - @Override - public boolean remove(Object o) { - return scheduledTaskQueue.remove(o); + private TestTaskScheduler(AbstractScheduledEventExecutor executor) { + this.executor = executor; + scheduledTaskQueue = new LinkedBlockingQueue<>(); } @Override - public boolean containsAll(@NotNull Collection c) { - return scheduledTaskQueue.containsAll(c); + protected RunnableScheduledFuture newScheduledTaskFor(Callable callable, long deadlineNanos, + long period) { + return new RunnableScheduledFutureAdapter<>(executor, executor.newPromise(), callable, + deadlineNanos, period); } @Override - public boolean addAll(@NotNull Collection> c) { - return scheduledTaskQueue.addAll(c); + protected Ticker ticker() { + return executor.ticker(); } @Override - public boolean removeAll(@NotNull Collection c) { - return scheduledTaskQueue.removeAll(c); + public Future schedule(RunnableScheduledFuture task) { + if (executor.inEventLoop()) { + scheduledTaskQueue.add(task); + } else { + executor.execute(() -> schedule(task)); + } + return task; } @Override - public boolean retainAll(@NotNull Collection c) { - return scheduledTaskQueue.retainAll(c); + public RunnableScheduledFuture peekScheduledTask() { + return scheduledTaskQueue.peek(); } @Override - public void clear() { + public void removeNextScheduledTask() { scheduledTaskQueue.clear(); } @Override - public boolean offer(RunnableScheduledFutureNode task) { - return scheduledTaskQueue.offer(task); - } - - @Override - public RunnableScheduledFutureNode remove() { - return scheduledTaskQueue.remove(); - } - - @Override - public RunnableScheduledFutureNode poll() { - return scheduledTaskQueue.poll(); + public void removeScheduled(RunnableScheduledFuture task) { + scheduledTaskQueue.remove(task); } @Override - public RunnableScheduledFutureNode element() { - return scheduledTaskQueue.element(); + public void cancelScheduledTasks() { + scheduledTaskQueue.clear(); } @Override - public RunnableScheduledFutureNode peek() { - return scheduledTaskQueue.peek(); + public int size() { + return scheduledTaskQueue.size(); } } } diff --git a/common/src/test/java/io/netty5/util/concurrent/DefaultTaskSchedulerTest.java b/common/src/test/java/io/netty5/util/concurrent/DefaultTaskSchedulerTest.java new file mode 100644 index 00000000000..98d6f4759ac --- /dev/null +++ b/common/src/test/java/io/netty5/util/concurrent/DefaultTaskSchedulerTest.java @@ -0,0 +1,385 @@ +/* + * Copyright 2023 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.netty5.util.concurrent; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class DefaultTaskSchedulerTest { + + private static final Runnable TEST_RUNNABLE = () -> {}; + + private static final Callable TEST_CALLABLE = Executors.callable(TEST_RUNNABLE); + + @Test + public void schedule() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + int taskCount = 10; + assertEmpty(scheduler); + + for (int i = 1; i <= taskCount; i++) { + long deadlineNanos = executor.ticker().nanoTime() + i * TimeUnit.MILLISECONDS.toNanos(100); + scheduler.schedule(new RunnableScheduledFutureAdapter<>( + executor, executor.newPromise(), TEST_CALLABLE, deadlineNanos, 0)); + } + assertRemainTask(scheduler, taskCount); + + ticker.advanceMillis(500); + for (int i = 0; i < 5; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 5); + + ticker.advanceMillis(300); + for (int i = 0; i < 3; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 2); + + ticker.advanceMillis(200); + for (int i = 0; i < 2; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertEmpty(scheduler); + } + + @Test + public void scheduleWithDelay_runnable() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + int taskCount = 10; + assertEmpty(scheduler); + + for (int i = 1; i <= taskCount; i++) { + scheduler.schedule(TEST_RUNNABLE, i * 100L, TimeUnit.MILLISECONDS); + } + assertRemainTask(scheduler, taskCount); + + ticker.advanceMillis(500); + for (int i = 0; i < 5; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 5); + + ticker.advanceMillis(300); + for (int i = 0; i < 3; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 2); + + ticker.advanceMillis(200); + for (int i = 0; i < 2; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertEmpty(scheduler); + } + + @Test + public void scheduleWithDelay_callable() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + int taskCount = 10; + assertEmpty(scheduler); + + for (int i = 1; i <= taskCount; i++) { + scheduler.schedule(TEST_CALLABLE, i * 100L, TimeUnit.MILLISECONDS); + } + assertRemainTask(scheduler, taskCount); + + ticker.advanceMillis(500); + for (int i = 0; i < 5; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 5); + + ticker.advanceMillis(300); + for (int i = 0; i < 3; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertRemainTask(scheduler, 2); + + ticker.advanceMillis(200); + for (int i = 0; i < 2; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertEmpty(scheduler); + } + + @Test + public void scheduleWithDelay_delayZero() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + scheduler.schedule(TEST_RUNNABLE, 0, TimeUnit.MILLISECONDS); + scheduler.schedule(TEST_CALLABLE, 0, TimeUnit.MILLISECONDS); + assertThat(scheduler.size()).isEqualTo(2); + + for (int i = 0; i < 2; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertEmpty(scheduler); + } + + @Test + public void scheduleWithDelay_delayNegative() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + scheduler.schedule(TEST_RUNNABLE, -1, TimeUnit.MILLISECONDS); + scheduler.schedule(TEST_CALLABLE, -100, TimeUnit.MILLISECONDS); + assertThat(scheduler.size()).isEqualTo(2); + + for (int i = 0; i < 2; i++) { + assertThat(scheduler.pollScheduledTask()).isNotNull(); + } + assertEmpty(scheduler); + } + + @Test + public void scheduleAtFixedRate() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + scheduler.scheduleAtFixedRate(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(100); + scheduler.pollScheduledTask().run(); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(150); + scheduler.pollScheduledTask().run(); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(50); + assertThat(scheduler.pollScheduledTask()).isNotNull(); + assertEmpty(scheduler); + } + + @Test + public void scheduleAtFixedRate_thrown() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + assertThatThrownBy(() -> scheduler + .scheduleAtFixedRate(TEST_RUNNABLE, -1, 100L, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> scheduler + .scheduleAtFixedRate(TEST_RUNNABLE, 100, -1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class); + assertEmpty(scheduler); + } + + @Test + public void scheduleWithFixedDelay() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + scheduler.scheduleWithFixedDelay(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(100); + scheduler.pollScheduledTask().run(); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(150); + scheduler.pollScheduledTask().run(); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(50); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(50); + scheduler.pollScheduledTask().run(); + assertRemainTask(scheduler, 1); + + ticker.advanceMillis(100); + assertThat(scheduler.pollScheduledTask()).isNotNull(); + assertEmpty(scheduler); + } + + @Test + public void scheduleWithFixedDelay_thrown() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + assertEmpty(scheduler); + + assertThatThrownBy(() -> scheduler + .scheduleWithFixedDelay(TEST_RUNNABLE, -1, 100L, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> scheduler + .scheduleWithFixedDelay(TEST_RUNNABLE, 100L, -1, TimeUnit.MILLISECONDS)) + .isInstanceOf(IllegalArgumentException.class); + assertEmpty(scheduler); + } + + + @Test + public void removeNextScheduledTask() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + int taskCount = 10; + assertEmpty(scheduler); + + for (int i = 1; i <= taskCount; i++) { + scheduler.schedule(TEST_RUNNABLE, i * 100L, TimeUnit.MILLISECONDS); + } + assertRemainTask(scheduler, taskCount); + + for (int i = 1; i <= taskCount; i++) { + scheduler.removeNextScheduledTask(); + } + assertEmpty(scheduler); + } + + @Test + public void removeScheduled() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + + scheduler.schedule(TEST_CALLABLE, 100L, TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + scheduler.scheduleWithFixedDelay(TEST_RUNNABLE, 100L, 100L, TimeUnit.MILLISECONDS); + assertRemainTask(scheduler, 3); + + for (int i = 0; i < 3; i++) { + scheduler.removeScheduled(scheduler.peekScheduledTask()); + } + assertEmpty(scheduler); + } + + @Test + public void cancelScheduledTasks() { + MockTicker ticker = Ticker.newMockTicker(); + AbstractScheduledEventExecutor executor = new TestScheduledEventExecutor( + DefaultTaskSchedulerFactory.ISTANCE, ticker); + TaskScheduler scheduler = executor.taskScheduler(); + int taskCount = 10; + assertEmpty(scheduler); + + for (int i = 1; i <= taskCount; i++) { + scheduler.schedule(TEST_RUNNABLE, i * 100L, TimeUnit.MILLISECONDS); + } + assertRemainTask(scheduler, taskCount); + + scheduler.cancelScheduledTasks(); + assertEmpty(scheduler); + } + + private static void assertEmpty(TaskScheduler scheduler) { + assertThat(scheduler.size()).isZero(); + assertThat(scheduler.isEmpty()).isTrue(); + assertThat(scheduler.peekScheduledTask()).isNull();; + assertThat(scheduler.pollScheduledTask()).isNull(); + } + + private static void assertRemainTask(TaskScheduler scheduler, int taskCount) { + assertThat(scheduler.size()).isEqualTo(taskCount); + assertThat(scheduler.isEmpty()).isFalse(); + assertThat(scheduler.peekScheduledTask()).isNotNull(); + assertThat(scheduler.pollScheduledTask()).isNull(); + } + + private static final class TestScheduledEventExecutor extends AbstractScheduledEventExecutor { + + private final Ticker ticker; + + private TestScheduledEventExecutor(TaskSchedulerFactory taskSchedulerFactory, Ticker ticker) { + super(taskSchedulerFactory); + this.ticker = ticker; + } + + @Override + protected Ticker ticker() { + return ticker; + } + + @Override + public boolean isShuttingDown() { + return false; + } + + @Override + public boolean inEventLoop(Thread thread) { + return true; + } + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public Future terminationFuture() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + return false; + } + + @Override + public boolean isTerminated() { + return false; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return false; + } + + @Override + public void execute(Runnable task) { + throw new UnsupportedOperationException(); + } + } +}