From 19e0a1aaec97668a552dd62379e810508c1393fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Wed, 26 Apr 2023 17:57:54 +0200 Subject: [PATCH] added the async code from Vert.x and Netty --- NOTICE.txt | 28 +- gradle.properties | 2 +- gradle/ide/idea.gradle | 10 +- src/main/java/module-info.java | 4 +- .../java/org/xbib/event/AbstractFuture.java | 43 + .../java/org/xbib/event/CompleteFuture.java | 135 ++ .../xbib/event/DefaultFutureListeners.java | 71 ++ .../xbib/event/DefaultProgressivePromise.java | 116 ++ .../java/org/xbib/event/DefaultPromise.java | 854 +++++++++++++ .../java/org/xbib/event/EventService.java | 7 - .../java/org/xbib/event/FailedFuture.java | 51 + .../java/org/xbib/event/FileFollowEvent.java | 7 - src/main/java/org/xbib/event/Future.java | 39 + .../java/org/xbib/event/FutureListener.java | 4 + .../org/xbib/event/GenericFutureListener.java | 7 + .../GenericProgressiveFutureListener.java | 12 + .../org/xbib/event/ProgressiveFuture.java | 31 + .../org/xbib/event/ProgressivePromise.java | 50 + src/main/java/org/xbib/event/Promise.java | 29 + src/main/java/org/xbib/event/PromiseTask.java | 175 +++ .../java/org/xbib/event/ScheduledFuture.java | 8 + .../org/xbib/event/ScheduledFutureTask.java | 223 ++++ .../java/org/xbib/event/SucceededFuture.java | 37 + src/main/java/org/xbib/event/async/Async.java | 189 +++ .../org/xbib/event/async/AsyncOptions.java | 447 +++++++ .../org/xbib/event/async/AsyncResult.java | 188 +++ .../java/org/xbib/event/async/Closeable.java | 16 + .../org/xbib/event/async/CompositeFuture.java | 245 ++++ .../java/org/xbib/event/async/Context.java | 212 ++++ .../org/xbib/event/async/EventException.java | 67 + .../java/org/xbib/event/async/Future.java | 414 ++++++ .../java/org/xbib/event/async/Handler.java | 18 + .../event/async/NoStackTraceThrowable.java | 8 + .../java/org/xbib/event/async/Promise.java | 130 ++ .../org/xbib/event/async/TimeoutStream.java | 40 + .../org/xbib/event/async/WorkerExecutor.java | 47 + .../xbib/event/async/impl/AsyncBuilder.java | 110 ++ .../org/xbib/event/async/impl/AsyncImpl.java | 558 +++++++++ .../xbib/event/async/impl/AsyncInternal.java | 52 + .../xbib/event/async/impl/AsyncThread.java | 56 + .../xbib/event/async/impl/CloseFuture.java | 176 +++ .../xbib/event/async/impl/ContextBase.java | 189 +++ .../event/async/impl/ContextInternal.java | 392 ++++++ .../event/async/impl/DuplicatedContext.java | 161 +++ .../event/async/impl/EventLoopContext.java | 92 ++ .../event/async/impl/NestedCloseable.java | 10 + .../org/xbib/event/async/impl/TaskQueue.java | 88 ++ .../org/xbib/event/async/impl/WorkerPool.java | 20 + .../impl/future/CompositeFutureImpl.java | 189 +++ .../event/async/impl/future/Composition.java | 58 + .../event/async/impl/future/Eventually.java | 62 + .../event/async/impl/future/FailedFuture.java | 127 ++ .../event/async/impl/future/FixedMapping.java | 26 + .../async/impl/future/FixedOtherwise.java | 26 + .../event/async/impl/future/FutureBase.java | 119 ++ .../event/async/impl/future/FutureImpl.java | 268 ++++ .../async/impl/future/FutureInternal.java | 23 + .../event/async/impl/future/Listener.java | 21 + .../xbib/event/async/impl/future/Mapping.java | 35 + .../event/async/impl/future/Operation.java | 13 + .../event/async/impl/future/Otherwise.java | 32 + .../event/async/impl/future/PromiseImpl.java | 57 + .../async/impl/future/PromiseInternal.java | 14 + .../async/impl/future/SucceededFuture.java | 118 ++ .../async/impl/future/Transformation.java | 59 + .../event/async/spi/AsyncServiceProvider.java | 17 + .../event/async/spi/AsyncThreadFactory.java | 23 + .../async/spi/ExecutorServiceFactory.java | 45 + .../org/xbib/event/async/streams/Pipe.java | 68 + .../xbib/event/async/streams/ReadStream.java | 102 ++ .../xbib/event/async/streams/StreamBase.java | 17 + .../xbib/event/async/streams/WriteStream.java | 95 ++ .../event/async/streams/impl/PipeImpl.java | 141 +++ .../AbstractAsyncFileReaderLines.java | 2 +- .../event/{async => io}/AddOnComplete.java | 2 +- .../xbib/event/{async => io}/AddOnError.java | 2 +- .../xbib/event/{async => io}/AddOnNext.java | 2 +- .../event/{async => io}/AddOnSubscribe.java | 2 +- .../event/{async => io}/AsyncFileQuery.java | 2 +- .../{async => io}/AsyncFileReaderBytes.java | 2 +- .../{async => io}/AsyncFileReaderLines.java | 2 +- .../event/{async => io}/AsyncFileWriter.java | 2 +- .../xbib/event/{async => io}/AsyncFiles.java | 2 +- .../event/{async => io}/EmptySubscriber.java | 2 +- .../{async => io}/SubscriberBuilder.java | 2 +- .../xbib/event/{async => io}/Subscribers.java | 2 +- .../event/loop/AbstractEventExecutor.java | 181 +++ .../loop/AbstractEventExecutorGroup.java | 85 ++ .../loop/AbstractScheduledEventExecutor.java | 271 ++++ .../loop/BlockingOperationException.java | 24 + .../DefaultEventExecutorChooserFactory.java | 58 + .../org/xbib/event/loop/EventExecutor.java | 63 + .../loop/EventExecutorChooserFactory.java | 23 + .../xbib/event/loop/EventExecutorGroup.java | 82 ++ .../java/org/xbib/event/loop/EventLoop.java | 6 + .../xbib/event/loop/EventLoopException.java | 24 + .../org/xbib/event/loop/EventLoopGroup.java | 7 + .../event/loop/EventLoopTaskQueueFactory.java | 20 + .../xbib/event/loop/GlobalEventExecutor.java | 265 ++++ .../loop/MultithreadEventExecutorGroup.java | 220 ++++ .../event/loop/MultithreadEventLoopGroup.java | 66 + .../xbib/event/loop/OrderedEventExecutor.java | 7 + .../event/loop/RejectedExecutionHandler.java | 13 + .../event/loop/RejectedExecutionHandlers.java | 54 + .../event/loop/SingleThreadEventExecutor.java | 1107 +++++++++++++++++ .../event/loop/SingleThreadEventLoop.java | 116 ++ .../org/xbib/event/loop/nio/NioEventLoop.java | 780 ++++++++++++ .../event/loop/nio/NioEventLoopGroup.java | 176 +++ .../java/org/xbib/event/loop/nio/NioTask.java | 26 + .../loop/nio/SelectedSelectionKeySet.java | 86 ++ .../loop/selector/DefaultSelectStrategy.java | 17 + .../DefaultSelectStrategyFactory.java | 15 + .../event/loop/selector/SelectStrategy.java | 37 + .../loop/selector/SelectStrategyFactory.java | 12 + .../event/thread/DefaultThreadFactory.java | 108 ++ .../xbib/event/thread/FastThreadLocal.java | 258 ++++ .../event/thread/FastThreadLocalRunnable.java | 24 + .../event/thread/FastThreadLocalThread.java | 110 ++ .../event/thread/InternalThreadLocalMap.java | 365 ++++++ .../xbib/event/thread/ThreadExecutorMap.java | 81 ++ .../org/xbib/event/thread/ThreadInfo.java | 15 + .../event/thread/ThreadPerTaskExecutor.java | 18 + .../xbib/event/thread/ThreadProperties.java | 46 + .../event/thread/TypeParameterMatcher.java | 143 +++ .../xbib/event/util/DefaultPriorityQueue.java | 282 +++++ .../java/org/xbib/event/util/IntSupplier.java | 14 + .../org/xbib/event/util/PriorityQueue.java | 31 + .../xbib/event/util/PriorityQueueNode.java | 29 + .../test/AsyncFileReaderTest.java | 6 +- .../test/AsyncFileWriterTest.java | 4 +- .../test/AsyncFilesFailures.java | 6 +- 131 files changed, 12987 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/xbib/event/AbstractFuture.java create mode 100644 src/main/java/org/xbib/event/CompleteFuture.java create mode 100644 src/main/java/org/xbib/event/DefaultFutureListeners.java create mode 100644 src/main/java/org/xbib/event/DefaultProgressivePromise.java create mode 100644 src/main/java/org/xbib/event/DefaultPromise.java delete mode 100644 src/main/java/org/xbib/event/EventService.java create mode 100644 src/main/java/org/xbib/event/FailedFuture.java delete mode 100644 src/main/java/org/xbib/event/FileFollowEvent.java create mode 100644 src/main/java/org/xbib/event/Future.java create mode 100644 src/main/java/org/xbib/event/FutureListener.java create mode 100644 src/main/java/org/xbib/event/GenericFutureListener.java create mode 100644 src/main/java/org/xbib/event/GenericProgressiveFutureListener.java create mode 100644 src/main/java/org/xbib/event/ProgressiveFuture.java create mode 100644 src/main/java/org/xbib/event/ProgressivePromise.java create mode 100644 src/main/java/org/xbib/event/Promise.java create mode 100644 src/main/java/org/xbib/event/PromiseTask.java create mode 100644 src/main/java/org/xbib/event/ScheduledFuture.java create mode 100644 src/main/java/org/xbib/event/ScheduledFutureTask.java create mode 100644 src/main/java/org/xbib/event/SucceededFuture.java create mode 100644 src/main/java/org/xbib/event/async/Async.java create mode 100644 src/main/java/org/xbib/event/async/AsyncOptions.java create mode 100644 src/main/java/org/xbib/event/async/AsyncResult.java create mode 100644 src/main/java/org/xbib/event/async/Closeable.java create mode 100644 src/main/java/org/xbib/event/async/CompositeFuture.java create mode 100644 src/main/java/org/xbib/event/async/Context.java create mode 100644 src/main/java/org/xbib/event/async/EventException.java create mode 100644 src/main/java/org/xbib/event/async/Future.java create mode 100644 src/main/java/org/xbib/event/async/Handler.java create mode 100644 src/main/java/org/xbib/event/async/NoStackTraceThrowable.java create mode 100644 src/main/java/org/xbib/event/async/Promise.java create mode 100644 src/main/java/org/xbib/event/async/TimeoutStream.java create mode 100644 src/main/java/org/xbib/event/async/WorkerExecutor.java create mode 100644 src/main/java/org/xbib/event/async/impl/AsyncBuilder.java create mode 100644 src/main/java/org/xbib/event/async/impl/AsyncImpl.java create mode 100644 src/main/java/org/xbib/event/async/impl/AsyncInternal.java create mode 100644 src/main/java/org/xbib/event/async/impl/AsyncThread.java create mode 100644 src/main/java/org/xbib/event/async/impl/CloseFuture.java create mode 100644 src/main/java/org/xbib/event/async/impl/ContextBase.java create mode 100644 src/main/java/org/xbib/event/async/impl/ContextInternal.java create mode 100644 src/main/java/org/xbib/event/async/impl/DuplicatedContext.java create mode 100644 src/main/java/org/xbib/event/async/impl/EventLoopContext.java create mode 100644 src/main/java/org/xbib/event/async/impl/NestedCloseable.java create mode 100644 src/main/java/org/xbib/event/async/impl/TaskQueue.java create mode 100644 src/main/java/org/xbib/event/async/impl/WorkerPool.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/CompositeFutureImpl.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Composition.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Eventually.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FailedFuture.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FixedMapping.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FixedOtherwise.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FutureBase.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FutureImpl.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/FutureInternal.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Listener.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Mapping.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Operation.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Otherwise.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/PromiseImpl.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/PromiseInternal.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/SucceededFuture.java create mode 100644 src/main/java/org/xbib/event/async/impl/future/Transformation.java create mode 100644 src/main/java/org/xbib/event/async/spi/AsyncServiceProvider.java create mode 100644 src/main/java/org/xbib/event/async/spi/AsyncThreadFactory.java create mode 100644 src/main/java/org/xbib/event/async/spi/ExecutorServiceFactory.java create mode 100644 src/main/java/org/xbib/event/async/streams/Pipe.java create mode 100644 src/main/java/org/xbib/event/async/streams/ReadStream.java create mode 100644 src/main/java/org/xbib/event/async/streams/StreamBase.java create mode 100644 src/main/java/org/xbib/event/async/streams/WriteStream.java create mode 100644 src/main/java/org/xbib/event/async/streams/impl/PipeImpl.java rename src/main/java/org/xbib/event/{async => io}/AbstractAsyncFileReaderLines.java (99%) rename src/main/java/org/xbib/event/{async => io}/AddOnComplete.java (92%) rename src/main/java/org/xbib/event/{async => io}/AddOnError.java (95%) rename src/main/java/org/xbib/event/{async => io}/AddOnNext.java (92%) rename src/main/java/org/xbib/event/{async => io}/AddOnSubscribe.java (94%) rename src/main/java/org/xbib/event/{async => io}/AsyncFileQuery.java (98%) rename src/main/java/org/xbib/event/{async => io}/AsyncFileReaderBytes.java (98%) rename src/main/java/org/xbib/event/{async => io}/AsyncFileReaderLines.java (99%) rename src/main/java/org/xbib/event/{async => io}/AsyncFileWriter.java (99%) rename src/main/java/org/xbib/event/{async => io}/AsyncFiles.java (99%) rename src/main/java/org/xbib/event/{async => io}/EmptySubscriber.java (94%) rename src/main/java/org/xbib/event/{async => io}/SubscriberBuilder.java (97%) rename src/main/java/org/xbib/event/{async => io}/Subscribers.java (87%) create mode 100644 src/main/java/org/xbib/event/loop/AbstractEventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/AbstractEventExecutorGroup.java create mode 100644 src/main/java/org/xbib/event/loop/AbstractScheduledEventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/BlockingOperationException.java create mode 100644 src/main/java/org/xbib/event/loop/DefaultEventExecutorChooserFactory.java create mode 100644 src/main/java/org/xbib/event/loop/EventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/EventExecutorChooserFactory.java create mode 100644 src/main/java/org/xbib/event/loop/EventExecutorGroup.java create mode 100644 src/main/java/org/xbib/event/loop/EventLoop.java create mode 100644 src/main/java/org/xbib/event/loop/EventLoopException.java create mode 100644 src/main/java/org/xbib/event/loop/EventLoopGroup.java create mode 100644 src/main/java/org/xbib/event/loop/EventLoopTaskQueueFactory.java create mode 100644 src/main/java/org/xbib/event/loop/GlobalEventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/MultithreadEventExecutorGroup.java create mode 100644 src/main/java/org/xbib/event/loop/MultithreadEventLoopGroup.java create mode 100644 src/main/java/org/xbib/event/loop/OrderedEventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/RejectedExecutionHandler.java create mode 100644 src/main/java/org/xbib/event/loop/RejectedExecutionHandlers.java create mode 100644 src/main/java/org/xbib/event/loop/SingleThreadEventExecutor.java create mode 100644 src/main/java/org/xbib/event/loop/SingleThreadEventLoop.java create mode 100644 src/main/java/org/xbib/event/loop/nio/NioEventLoop.java create mode 100644 src/main/java/org/xbib/event/loop/nio/NioEventLoopGroup.java create mode 100644 src/main/java/org/xbib/event/loop/nio/NioTask.java create mode 100644 src/main/java/org/xbib/event/loop/nio/SelectedSelectionKeySet.java create mode 100644 src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategy.java create mode 100644 src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategyFactory.java create mode 100644 src/main/java/org/xbib/event/loop/selector/SelectStrategy.java create mode 100644 src/main/java/org/xbib/event/loop/selector/SelectStrategyFactory.java create mode 100644 src/main/java/org/xbib/event/thread/DefaultThreadFactory.java create mode 100644 src/main/java/org/xbib/event/thread/FastThreadLocal.java create mode 100644 src/main/java/org/xbib/event/thread/FastThreadLocalRunnable.java create mode 100644 src/main/java/org/xbib/event/thread/FastThreadLocalThread.java create mode 100644 src/main/java/org/xbib/event/thread/InternalThreadLocalMap.java create mode 100644 src/main/java/org/xbib/event/thread/ThreadExecutorMap.java create mode 100644 src/main/java/org/xbib/event/thread/ThreadInfo.java create mode 100644 src/main/java/org/xbib/event/thread/ThreadPerTaskExecutor.java create mode 100644 src/main/java/org/xbib/event/thread/ThreadProperties.java create mode 100644 src/main/java/org/xbib/event/thread/TypeParameterMatcher.java create mode 100644 src/main/java/org/xbib/event/util/DefaultPriorityQueue.java create mode 100644 src/main/java/org/xbib/event/util/IntSupplier.java create mode 100644 src/main/java/org/xbib/event/util/PriorityQueue.java create mode 100644 src/main/java/org/xbib/event/util/PriorityQueueNode.java rename src/test/java/org/xbib/event/{async => io}/test/AsyncFileReaderTest.java (99%) rename src/test/java/org/xbib/event/{async => io}/test/AsyncFileWriterTest.java (97%) rename src/test/java/org/xbib/event/{async => io}/test/AsyncFilesFailures.java (98%) diff --git a/NOTICE.txt b/NOTICE.txt index 588d9aa..94ae1f0 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,4 +1,30 @@ -The work in org.xbib.event.async is based upon the work in +The work in org.xbib.event.async is based upon io.vertx.core in + +https://github.com/eclipse-vertx/vert.x + +branched as of 26 Apr 2023. + +/* + * Copyright (c) 2011-2019 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + */ + +The work in org.xbib.event, org.xbib.event.thread, org.xbib.event.loop, org.xbib.event.util is based on netty + +https://github.com/netty/netty/ + +branch 4.1 as of 26 Apr 2023. + +Licence: Apache 2.0 + + +The work in org.xbib.event.io is based upon the work in https://github.com/javasync/RxIo diff --git a/gradle.properties b/gradle.properties index 1cef11c..8324a1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group = org.xbib name = event -version = 0.0.1 +version = 0.0.2 org.gradle.warning.mode = ALL diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle index 64e2167..a4ee4a5 100644 --- a/gradle/ide/idea.gradle +++ b/gradle/ide/idea.gradle @@ -5,9 +5,9 @@ idea { outputDir file('build/classes/java/main') testOutputDir file('build/classes/java/test') } -} - -if (project.convention.findPlugin(JavaPluginConvention)) { - //sourceSets.main.output.classesDirs = file("build/classes/java/main") - //sourceSets.test.output.classesDirs = file("build/classes/java/test") + project { + jdkName = '17' + languageLevel = '17' + vcs = 'Git' + } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8fbf381..8d0c6a1 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,12 +1,14 @@ module org.xbib.event { exports org.xbib.event; - exports org.xbib.event.async; exports org.xbib.event.bus; exports org.xbib.event.clock; exports org.xbib.event.persistence; exports org.xbib.event.queue; exports org.xbib.event.syslog; exports org.xbib.event.yield; + exports org.xbib.event.io; + exports org.xbib.event.loop; + exports org.xbib.event.loop.selector; requires org.xbib.datastructures.api; requires org.xbib.datastructures.common; requires org.xbib.datastructures.json.tiny; diff --git a/src/main/java/org/xbib/event/AbstractFuture.java b/src/main/java/org/xbib/event/AbstractFuture.java new file mode 100644 index 0000000..6652544 --- /dev/null +++ b/src/main/java/org/xbib/event/AbstractFuture.java @@ -0,0 +1,43 @@ +package org.xbib.event; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Abstract {@link Future} implementation which does not allow for cancellation. + * + * @param + */ +public abstract class AbstractFuture implements Future { + + @Override + public V get() throws InterruptedException, ExecutionException { + await(); + + Throwable cause = cause(); + if (cause == null) { + return getNow(); + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (await(timeout, unit)) { + Throwable cause = cause(); + if (cause == null) { + return getNow(); + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + throw new TimeoutException(); + } +} diff --git a/src/main/java/org/xbib/event/CompleteFuture.java b/src/main/java/org/xbib/event/CompleteFuture.java new file mode 100644 index 0000000..ad51d5c --- /dev/null +++ b/src/main/java/org/xbib/event/CompleteFuture.java @@ -0,0 +1,135 @@ +package org.xbib.event; + +import org.xbib.event.loop.EventExecutor; + +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * A skeletal {@link Future} implementation which represents a {@link Future} which has been completed already. + */ +public abstract class CompleteFuture extends AbstractFuture { + + private final EventExecutor executor; + + /** + * Creates a new instance. + * + * @param executor the {@link EventExecutor} associated with this future + */ + protected CompleteFuture(EventExecutor executor) { + this.executor = executor; + } + + /** + * Return the {@link EventExecutor} which is used by this {@link CompleteFuture}. + */ + protected EventExecutor executor() { + return executor; + } + + @Override + public Future addListener(GenericFutureListener> listener) { + DefaultPromise.notifyListener(executor(), this, Objects.requireNonNull(listener, "listener")); + return this; + } + + @Override + public Future addListeners(GenericFutureListener>... listeners) { + for (GenericFutureListener> l: + Objects.requireNonNull(listeners, "listeners")) { + + if (l == null) { + break; + } + DefaultPromise.notifyListener(executor(), this, l); + } + return this; + } + + @Override + public Future removeListener(GenericFutureListener> listener) { + // NOOP + return this; + } + + @Override + public Future removeListeners(GenericFutureListener>... listeners) { + // NOOP + return this; + } + + @Override + public Future await() throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + return this; + } + + @Override + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + return true; + } + + @Override + public Future sync() throws InterruptedException { + return this; + } + + @Override + public Future syncUninterruptibly() { + return this; + } + + @Override + public boolean await(long timeoutMillis) throws InterruptedException { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + return true; + } + + @Override + public Future awaitUninterruptibly() { + return this; + } + + @Override + public boolean awaitUninterruptibly(long timeout, TimeUnit unit) { + return true; + } + + @Override + public boolean awaitUninterruptibly(long timeoutMillis) { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public boolean isCancellable() { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + /** + * {@inheritDoc} + * + * @param mayInterruptIfRunning this value has no effect in this implementation. + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } +} diff --git a/src/main/java/org/xbib/event/DefaultFutureListeners.java b/src/main/java/org/xbib/event/DefaultFutureListeners.java new file mode 100644 index 0000000..9e0eb69 --- /dev/null +++ b/src/main/java/org/xbib/event/DefaultFutureListeners.java @@ -0,0 +1,71 @@ +package org.xbib.event; + +import java.util.Arrays; + +final class DefaultFutureListeners { + + private GenericFutureListener>[] listeners; + private int size; + private int progressiveSize; // the number of progressive listeners + + @SuppressWarnings("unchecked") + DefaultFutureListeners( + GenericFutureListener> first, GenericFutureListener> second) { + listeners = new GenericFutureListener[2]; + listeners[0] = first; + listeners[1] = second; + size = 2; + if (first instanceof GenericProgressiveFutureListener) { + progressiveSize ++; + } + if (second instanceof GenericProgressiveFutureListener) { + progressiveSize ++; + } + } + + public void add(GenericFutureListener> l) { + GenericFutureListener>[] listeners = this.listeners; + final int size = this.size; + if (size == listeners.length) { + this.listeners = listeners = Arrays.copyOf(listeners, size << 1); + } + listeners[size] = l; + this.size = size + 1; + + if (l instanceof GenericProgressiveFutureListener) { + progressiveSize ++; + } + } + + public void remove(GenericFutureListener> l) { + final GenericFutureListener>[] listeners = this.listeners; + int size = this.size; + for (int i = 0; i < size; i ++) { + if (listeners[i] == l) { + int listenersToMove = size - i - 1; + if (listenersToMove > 0) { + System.arraycopy(listeners, i + 1, listeners, i, listenersToMove); + } + listeners[-- size] = null; + this.size = size; + + if (l instanceof GenericProgressiveFutureListener) { + progressiveSize --; + } + return; + } + } + } + + public GenericFutureListener>[] listeners() { + return listeners; + } + + public int size() { + return size; + } + + public int progressiveSize() { + return progressiveSize; + } +} diff --git a/src/main/java/org/xbib/event/DefaultProgressivePromise.java b/src/main/java/org/xbib/event/DefaultProgressivePromise.java new file mode 100644 index 0000000..8c0b9e7 --- /dev/null +++ b/src/main/java/org/xbib/event/DefaultProgressivePromise.java @@ -0,0 +1,116 @@ +package org.xbib.event; + +import org.xbib.event.loop.EventExecutor; + +public class DefaultProgressivePromise extends DefaultPromise implements ProgressivePromise { + + /** + * Creates a new instance. + * + * It is preferable to use {@link EventExecutor#newProgressivePromise()} to create a new progressive promise + * + * @param executor + * the {@link EventExecutor} which is used to notify the promise when it progresses or it is complete + */ + public DefaultProgressivePromise(EventExecutor executor) { + super(executor); + } + + protected DefaultProgressivePromise() { /* only for subclasses */ } + + @Override + public ProgressivePromise setProgress(long progress, long total) { + if (total < 0) { + // total unknown + total = -1; // normalize + if (progress < 0) { + throw new IllegalArgumentException("progress must not be less than zero"); + } + } else if (progress < 0 || progress > total) { + throw new IllegalArgumentException( + "progress: " + progress + " (expected: 0 <= progress <= total (" + total + "))"); + } + + if (isDone()) { + throw new IllegalStateException("complete already"); + } + + notifyProgressiveListeners(progress, total); + return this; + } + + @Override + public boolean tryProgress(long progress, long total) { + if (total < 0) { + total = -1; + if (progress < 0 || isDone()) { + return false; + } + } else if (progress < 0 || progress > total || isDone()) { + return false; + } + + notifyProgressiveListeners(progress, total); + return true; + } + + @Override + public ProgressivePromise addListener(GenericFutureListener> listener) { + super.addListener(listener); + return this; + } + + @Override + public ProgressivePromise addListeners(GenericFutureListener>... listeners) { + super.addListeners(listeners); + return this; + } + + @Override + public ProgressivePromise removeListener(GenericFutureListener> listener) { + super.removeListener(listener); + return this; + } + + @Override + public ProgressivePromise removeListeners(GenericFutureListener>... listeners) { + super.removeListeners(listeners); + return this; + } + + @Override + public ProgressivePromise sync() throws InterruptedException { + super.sync(); + return this; + } + + @Override + public ProgressivePromise syncUninterruptibly() { + super.syncUninterruptibly(); + return this; + } + + @Override + public ProgressivePromise await() throws InterruptedException { + super.await(); + return this; + } + + @Override + public ProgressivePromise awaitUninterruptibly() { + super.awaitUninterruptibly(); + return this; + } + + @Override + public ProgressivePromise setSuccess(V result) { + super.setSuccess(result); + return this; + } + + @Override + public ProgressivePromise setFailure(Throwable cause) { + super.setFailure(cause); + return this; + } +} diff --git a/src/main/java/org/xbib/event/DefaultPromise.java b/src/main/java/org/xbib/event/DefaultPromise.java new file mode 100644 index 0000000..ccefa9a --- /dev/null +++ b/src/main/java/org/xbib/event/DefaultPromise.java @@ -0,0 +1,854 @@ +package org.xbib.event; + +import org.xbib.event.loop.BlockingOperationException; +import org.xbib.event.loop.EventExecutor; +import org.xbib.event.thread.InternalThreadLocalMap; + +import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class DefaultPromise extends AbstractFuture implements Promise { + private static final Logger logger = Logger.getLogger(DefaultPromise.class.getName()); + private static final Logger rejectedExecutionLogger = + Logger.getLogger(DefaultPromise.class.getName() + ".rejectedExecution"); + private static final int MAX_LISTENER_STACK_DEPTH = Math.min(8, + Integer.getInteger("org.xbib.defaultPromise.maxListenerStackDepth", 8)); + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater RESULT_UPDATER = + AtomicReferenceFieldUpdater.newUpdater(DefaultPromise.class, Object.class, "result"); + private static final Object SUCCESS = new Object(); + private static final Object UNCANCELLABLE = new Object(); + private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder( + StacklessCancellationException.newInstance(DefaultPromise.class, "cancel(...)")); + private static final StackTraceElement[] CANCELLATION_STACK = CANCELLATION_CAUSE_HOLDER.cause.getStackTrace(); + + private volatile Object result; + private final EventExecutor executor; + /** + * One or more listeners. Can be a {@link GenericFutureListener} or a {@link DefaultFutureListeners}. + * If {@code null}, it means either 1) no listeners were added yet or 2) all listeners were notified. + * + * Threading - synchronized(this). We must support adding listeners when there is no EventExecutor. + */ + private Object listeners; + /** + * Threading - synchronized(this). We are required to hold the monitor to use Java's underlying wait()/notifyAll(). + */ + private short waiters; + + /** + * Threading - synchronized(this). We must prevent concurrent notification and FIFO listener notification if the + * executor changes. + */ + private boolean notifyingListeners; + + /** + * Creates a new instance. + * + * It is preferable to use {@link EventExecutor#newPromise()} to create a new promise + * + * @param executor + * the {@link EventExecutor} which is used to notify the promise once it is complete. + * It is assumed this executor will protect against {@link StackOverflowError} exceptions. + * The executor may be used to avoid {@link StackOverflowError} by executing a {@link Runnable} if the stack + * depth exceeds a threshold. + * + */ + public DefaultPromise(EventExecutor executor) { + this.executor = Objects.requireNonNull(executor, "executor"); + } + + /** + * See {@link #executor()} for expectations of the executor. + */ + protected DefaultPromise() { + // only for subclasses + executor = null; + } + + @Override + public Promise setSuccess(V result) { + if (setSuccess0(result)) { + return this; + } + throw new IllegalStateException("complete already: " + this); + } + + @Override + public boolean trySuccess(V result) { + return setSuccess0(result); + } + + @Override + public Promise setFailure(Throwable cause) { + if (setFailure0(cause)) { + return this; + } + throw new IllegalStateException("complete already: " + this, cause); + } + + @Override + public boolean tryFailure(Throwable cause) { + return setFailure0(cause); + } + + @Override + public boolean setUncancellable() { + if (RESULT_UPDATER.compareAndSet(this, null, UNCANCELLABLE)) { + return true; + } + Object result = this.result; + return !isDone0(result) || !isCancelled0(result); + } + + @Override + public boolean isSuccess() { + Object result = this.result; + return result != null && result != UNCANCELLABLE && !(result instanceof CauseHolder); + } + + @Override + public boolean isCancellable() { + return result == null; + } + + private static final class LeanCancellationException extends CancellationException { + private static final long serialVersionUID = 2794674970981187807L; + + // Suppress a warning since the method doesn't need synchronization + @Override + public Throwable fillInStackTrace() { // lgtm[java/non-sync-override] + setStackTrace(CANCELLATION_STACK); + return this; + } + + @Override + public String toString() { + return CancellationException.class.getName(); + } + } + + @Override + public Throwable cause() { + return cause0(result); + } + + private Throwable cause0(Object result) { + if (!(result instanceof CauseHolder)) { + return null; + } + if (result == CANCELLATION_CAUSE_HOLDER) { + CancellationException ce = new LeanCancellationException(); + if (RESULT_UPDATER.compareAndSet(this, CANCELLATION_CAUSE_HOLDER, new CauseHolder(ce))) { + return ce; + } + result = this.result; + } + return ((CauseHolder) result).cause; + } + + @Override + public Promise addListener(GenericFutureListener> listener) { + Objects.requireNonNull(listener, "listener"); + + synchronized (this) { + addListener0(listener); + } + + if (isDone()) { + notifyListeners(); + } + + return this; + } + + @Override + public Promise addListeners(GenericFutureListener>... listeners) { + Objects.requireNonNull(listeners, "listeners"); + + synchronized (this) { + for (GenericFutureListener> listener : listeners) { + if (listener == null) { + break; + } + addListener0(listener); + } + } + + if (isDone()) { + notifyListeners(); + } + + return this; + } + + @Override + public Promise removeListener(final GenericFutureListener> listener) { + Objects.requireNonNull(listener, "listener"); + + synchronized (this) { + removeListener0(listener); + } + + return this; + } + + @Override + public Promise removeListeners(final GenericFutureListener>... listeners) { + Objects.requireNonNull(listeners, "listeners"); + + synchronized (this) { + for (GenericFutureListener> listener : listeners) { + if (listener == null) { + break; + } + removeListener0(listener); + } + } + + return this; + } + + @Override + public Promise await() throws InterruptedException { + if (isDone()) { + return this; + } + + if (Thread.interrupted()) { + throw new InterruptedException(toString()); + } + + checkDeadLock(); + + synchronized (this) { + while (!isDone()) { + incWaiters(); + try { + wait(); + } finally { + decWaiters(); + } + } + } + return this; + } + + @Override + public Promise awaitUninterruptibly() { + if (isDone()) { + return this; + } + + checkDeadLock(); + + boolean interrupted = false; + synchronized (this) { + while (!isDone()) { + incWaiters(); + try { + wait(); + } catch (InterruptedException e) { + // Interrupted while waiting. + interrupted = true; + } finally { + decWaiters(); + } + } + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + + return this; + } + + @Override + public boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return await0(unit.toNanos(timeout), true); + } + + @Override + public boolean await(long timeoutMillis) throws InterruptedException { + return await0(MILLISECONDS.toNanos(timeoutMillis), true); + } + + @Override + public boolean awaitUninterruptibly(long timeout, TimeUnit unit) { + try { + return await0(unit.toNanos(timeout), false); + } catch (InterruptedException e) { + // Should not be raised at all. + throw new InternalError(); + } + } + + @Override + public boolean awaitUninterruptibly(long timeoutMillis) { + try { + return await0(MILLISECONDS.toNanos(timeoutMillis), false); + } catch (InterruptedException e) { + // Should not be raised at all. + throw new InternalError(); + } + } + + @SuppressWarnings("unchecked") + @Override + public V getNow() { + Object result = this.result; + if (result instanceof CauseHolder || result == SUCCESS || result == UNCANCELLABLE) { + return null; + } + return (V) result; + } + + @SuppressWarnings("unchecked") + @Override + public V get() throws InterruptedException, ExecutionException { + Object result = this.result; + if (!isDone0(result)) { + await(); + result = this.result; + } + if (result == SUCCESS || result == UNCANCELLABLE) { + return null; + } + Throwable cause = cause0(result); + if (cause == null) { + return (V) result; + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + + @SuppressWarnings("unchecked") + @Override + public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + Object result = this.result; + if (!isDone0(result)) { + if (!await(timeout, unit)) { + throw new TimeoutException(); + } + result = this.result; + } + if (result == SUCCESS || result == UNCANCELLABLE) { + return null; + } + Throwable cause = cause0(result); + if (cause == null) { + return (V) result; + } + if (cause instanceof CancellationException) { + throw (CancellationException) cause; + } + throw new ExecutionException(cause); + } + + /** + * {@inheritDoc} + * + * @param mayInterruptIfRunning this value has no effect in this implementation. + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (RESULT_UPDATER.compareAndSet(this, null, CANCELLATION_CAUSE_HOLDER)) { + if (checkNotifyWaiters()) { + notifyListeners(); + } + return true; + } + return false; + } + + @Override + public boolean isCancelled() { + return isCancelled0(result); + } + + @Override + public boolean isDone() { + return isDone0(result); + } + + @Override + public Promise sync() throws InterruptedException { + await(); + rethrowIfFailed(); + return this; + } + + @Override + public Promise syncUninterruptibly() { + awaitUninterruptibly(); + rethrowIfFailed(); + return this; + } + + @Override + public String toString() { + return toStringBuilder().toString(); + } + + protected StringBuilder toStringBuilder() { + StringBuilder buf = new StringBuilder(64) + .append(getClass().getSimpleName()) + .append('@') + .append(Integer.toHexString(hashCode())); + + Object result = this.result; + if (result == SUCCESS) { + buf.append("(success)"); + } else if (result == UNCANCELLABLE) { + buf.append("(uncancellable)"); + } else if (result instanceof CauseHolder) { + buf.append("(failure: ") + .append(((CauseHolder) result).cause) + .append(')'); + } else if (result != null) { + buf.append("(success: ") + .append(result) + .append(')'); + } else { + buf.append("(incomplete)"); + } + + return buf; + } + + /** + * Get the executor used to notify listeners when this promise is complete. + *

+ * It is assumed this executor will protect against {@link StackOverflowError} exceptions. + * The executor may be used to avoid {@link StackOverflowError} by executing a {@link Runnable} if the stack + * depth exceeds a threshold. + * @return The executor used to notify listeners when this promise is complete. + */ + protected EventExecutor executor() { + return executor; + } + + protected void checkDeadLock() { + EventExecutor e = executor(); + if (e != null && e.inEventLoop()) { + throw new BlockingOperationException(toString()); + } + } + + /** + * Notify a listener that a future has completed. + *

+ * This method has a fixed depth of {@link #MAX_LISTENER_STACK_DEPTH} that will limit recursion to prevent + * {@link StackOverflowError} and will stop notifying listeners added after this threshold is exceeded. + * @param eventExecutor the executor to use to notify the listener {@code listener}. + * @param future the future that is complete. + * @param listener the listener to notify. + */ + protected static void notifyListener( + EventExecutor eventExecutor, final Future future, final GenericFutureListener listener) { + notifyListenerWithStackOverFlowProtection( + Objects.requireNonNull(eventExecutor, "eventExecutor"), + Objects.requireNonNull(future, "future"), + Objects.requireNonNull(listener, "listener")); + } + + private void notifyListeners() { + EventExecutor executor = executor(); + if (executor.inEventLoop()) { + final InternalThreadLocalMap threadLocals = InternalThreadLocalMap.get(); + final int stackDepth = threadLocals.futureListenerStackDepth(); + if (stackDepth < MAX_LISTENER_STACK_DEPTH) { + threadLocals.setFutureListenerStackDepth(stackDepth + 1); + try { + notifyListenersNow(); + } finally { + threadLocals.setFutureListenerStackDepth(stackDepth); + } + return; + } + } + + safeExecute(executor, new Runnable() { + @Override + public void run() { + notifyListenersNow(); + } + }); + } + + /** + * The logic in this method should be identical to {@link #notifyListeners()} but + * cannot share code because the listener(s) cannot be cached for an instance of {@link DefaultPromise} since the + * listener(s) may be changed and is protected by a synchronized operation. + */ + private static void notifyListenerWithStackOverFlowProtection(final EventExecutor executor, + final Future future, + final GenericFutureListener listener) { + if (executor.inEventLoop()) { + final InternalThreadLocalMap threadLocals = InternalThreadLocalMap.get(); + final int stackDepth = threadLocals.futureListenerStackDepth(); + if (stackDepth < MAX_LISTENER_STACK_DEPTH) { + threadLocals.setFutureListenerStackDepth(stackDepth + 1); + try { + notifyListener0(future, listener); + } finally { + threadLocals.setFutureListenerStackDepth(stackDepth); + } + return; + } + } + + safeExecute(executor, new Runnable() { + @Override + public void run() { + notifyListener0(future, listener); + } + }); + } + + private void notifyListenersNow() { + Object listeners; + synchronized (this) { + // Only proceed if there are listeners to notify and we are not already notifying listeners. + if (notifyingListeners || this.listeners == null) { + return; + } + notifyingListeners = true; + listeners = this.listeners; + this.listeners = null; + } + for (;;) { + if (listeners instanceof DefaultFutureListeners) { + notifyListeners0((DefaultFutureListeners) listeners); + } else { + notifyListener0(this, (GenericFutureListener) listeners); + } + synchronized (this) { + if (this.listeners == null) { + // Nothing can throw from within this method, so setting notifyingListeners back to false does not + // need to be in a finally block. + notifyingListeners = false; + return; + } + listeners = this.listeners; + this.listeners = null; + } + } + } + + private void notifyListeners0(DefaultFutureListeners listeners) { + GenericFutureListener[] a = listeners.listeners(); + int size = listeners.size(); + for (int i = 0; i < size; i ++) { + notifyListener0(this, a[i]); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void notifyListener0(Future future, GenericFutureListener l) { + try { + l.operationComplete(future); + } catch (Throwable t) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "An exception was thrown by " + l.getClass().getName() + ".operationComplete()", t); + } + } + } + + private void addListener0(GenericFutureListener> listener) { + if (listeners == null) { + listeners = listener; + } else if (listeners instanceof DefaultFutureListeners) { + ((DefaultFutureListeners) listeners).add(listener); + } else { + listeners = new DefaultFutureListeners((GenericFutureListener) listeners, listener); + } + } + + private void removeListener0(GenericFutureListener> listener) { + if (listeners instanceof DefaultFutureListeners) { + ((DefaultFutureListeners) listeners).remove(listener); + } else if (listeners == listener) { + listeners = null; + } + } + + private boolean setSuccess0(V result) { + return setValue0(result == null ? SUCCESS : result); + } + + private boolean setFailure0(Throwable cause) { + return setValue0(new CauseHolder(Objects.requireNonNull(cause, "cause"))); + } + + private boolean setValue0(Object objResult) { + if (RESULT_UPDATER.compareAndSet(this, null, objResult) || + RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) { + if (checkNotifyWaiters()) { + notifyListeners(); + } + return true; + } + return false; + } + + /** + * Check if there are any waiters and if so notify these. + * @return {@code true} if there are any listeners attached to the promise, {@code false} otherwise. + */ + private synchronized boolean checkNotifyWaiters() { + if (waiters > 0) { + notifyAll(); + } + return listeners != null; + } + + private void incWaiters() { + if (waiters == Short.MAX_VALUE) { + throw new IllegalStateException("too many waiters: " + this); + } + ++waiters; + } + + private void decWaiters() { + --waiters; + } + + private void rethrowIfFailed() { + Throwable cause = cause(); + if (cause == null) { + return; + } + throw new RuntimeException(cause); + } + + private boolean await0(long timeoutNanos, boolean interruptable) throws InterruptedException { + if (isDone()) { + return true; + } + + if (timeoutNanos <= 0) { + return isDone(); + } + + if (interruptable && Thread.interrupted()) { + throw new InterruptedException(toString()); + } + + checkDeadLock(); + + long startTime = System.nanoTime(); + long waitTime = timeoutNanos; + boolean interrupted = false; + try { + for (;;) { + synchronized (this) { + if (isDone()) { + return true; + } + incWaiters(); + try { + wait(waitTime / 1000000, (int) (waitTime % 1000000)); + } catch (InterruptedException e) { + if (interruptable) { + throw e; + } else { + interrupted = true; + } + } finally { + decWaiters(); + } + } + if (isDone()) { + return true; + } else { + waitTime = timeoutNanos - (System.nanoTime() - startTime); + if (waitTime <= 0) { + return isDone(); + } + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Notify all progressive listeners. + *

+ * No attempt is made to ensure notification order if multiple calls are made to this method before + * the original invocation completes. + *

+ * This will do an iteration over all listeners to get all of type {@link GenericProgressiveFutureListener}s. + * @param progress the new progress. + * @param total the total progress. + */ + @SuppressWarnings("unchecked") + void notifyProgressiveListeners(final long progress, final long total) { + final Object listeners = progressiveListeners(); + if (listeners == null) { + return; + } + + final ProgressiveFuture self = (ProgressiveFuture) this; + + EventExecutor executor = executor(); + if (executor.inEventLoop()) { + if (listeners instanceof GenericProgressiveFutureListener[]) { + notifyProgressiveListeners0( + self, (GenericProgressiveFutureListener[]) listeners, progress, total); + } else { + notifyProgressiveListener0( + self, (GenericProgressiveFutureListener>) listeners, progress, total); + } + } else { + if (listeners instanceof GenericProgressiveFutureListener[]) { + final GenericProgressiveFutureListener[] array = + (GenericProgressiveFutureListener[]) listeners; + safeExecute(executor, new Runnable() { + @Override + public void run() { + notifyProgressiveListeners0(self, array, progress, total); + } + }); + } else { + final GenericProgressiveFutureListener> l = + (GenericProgressiveFutureListener>) listeners; + safeExecute(executor, new Runnable() { + @Override + public void run() { + notifyProgressiveListener0(self, l, progress, total); + } + }); + } + } + } + + /** + * Returns a {@link GenericProgressiveFutureListener}, an array of {@link GenericProgressiveFutureListener}, or + * {@code null}. + */ + private synchronized Object progressiveListeners() { + Object listeners = this.listeners; + if (listeners == null) { + // No listeners added + return null; + } + + if (listeners instanceof DefaultFutureListeners) { + // Copy DefaultFutureListeners into an array of listeners. + DefaultFutureListeners dfl = (DefaultFutureListeners) listeners; + int progressiveSize = dfl.progressiveSize(); + switch (progressiveSize) { + case 0: + return null; + case 1: + for (GenericFutureListener l: dfl.listeners()) { + if (l instanceof GenericProgressiveFutureListener) { + return l; + } + } + return null; + } + + GenericFutureListener[] array = dfl.listeners(); + GenericProgressiveFutureListener[] copy = new GenericProgressiveFutureListener[progressiveSize]; + for (int i = 0, j = 0; j < progressiveSize; i ++) { + GenericFutureListener l = array[i]; + if (l instanceof GenericProgressiveFutureListener) { + copy[j ++] = (GenericProgressiveFutureListener) l; + } + } + + return copy; + } else if (listeners instanceof GenericProgressiveFutureListener) { + return listeners; + } else { + // Only one listener was added and it's not a progressive listener. + return null; + } + } + + private static void notifyProgressiveListeners0( + ProgressiveFuture future, GenericProgressiveFutureListener[] listeners, long progress, long total) { + for (GenericProgressiveFutureListener l: listeners) { + if (l == null) { + break; + } + notifyProgressiveListener0(future, l, progress, total); + } + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void notifyProgressiveListener0( + ProgressiveFuture future, GenericProgressiveFutureListener l, long progress, long total) { + try { + l.operationProgressed(future, progress, total); + } catch (Throwable t) { + if (logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "An exception was thrown by " + l.getClass().getName() + ".operationProgressed()", t); + } + } + } + + private static boolean isCancelled0(Object result) { + return result instanceof CauseHolder && ((CauseHolder) result).cause instanceof CancellationException; + } + + private static boolean isDone0(Object result) { + return result != null && result != UNCANCELLABLE; + } + + private static final class CauseHolder { + final Throwable cause; + CauseHolder(Throwable cause) { + this.cause = cause; + } + } + + private static void safeExecute(EventExecutor executor, Runnable task) { + try { + executor.execute(task); + } catch (Throwable t) { + rejectedExecutionLogger.log(Level.SEVERE, "Failed to submit a listener notification task. Event loop shut down?", t); + } + } + + private static final class StacklessCancellationException extends CancellationException { + + private StacklessCancellationException() { } + + // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the + // Classloader. + @Override + public Throwable fillInStackTrace() { + return this; + } + + static StacklessCancellationException newInstance(Class clazz, String method) { + return unknownStackTrace(new StacklessCancellationException(), clazz, method); + } + + /** + * Set the {@link StackTraceElement} for the given {@link Throwable}, using the {@link Class} and method name. + */ + private static T unknownStackTrace(T cause, Class clazz, String method) { + cause.setStackTrace(new StackTraceElement[] { new StackTraceElement(clazz.getName(), method, null, -1)}); + return cause; + } + + } +} diff --git a/src/main/java/org/xbib/event/EventService.java b/src/main/java/org/xbib/event/EventService.java deleted file mode 100644 index 35eefc7..0000000 --- a/src/main/java/org/xbib/event/EventService.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.xbib.event; - -public class EventService { - - public EventService() { - } -} diff --git a/src/main/java/org/xbib/event/FailedFuture.java b/src/main/java/org/xbib/event/FailedFuture.java new file mode 100644 index 0000000..7377bc8 --- /dev/null +++ b/src/main/java/org/xbib/event/FailedFuture.java @@ -0,0 +1,51 @@ +package org.xbib.event; + +import org.xbib.event.loop.EventExecutor; + +import java.util.Objects; + +/** + * The {@link CompleteFuture} which is failed already. It is + * recommended to use {@link EventExecutor#newFailedFuture(Throwable)} + * instead of calling the constructor of this future. + */ +public final class FailedFuture extends CompleteFuture { + + private final Throwable cause; + + /** + * Creates a new instance. + * + * @param executor the {@link EventExecutor} associated with this future + * @param cause the cause of failure + */ + public FailedFuture(EventExecutor executor, Throwable cause) { + super(executor); + this.cause = Objects.requireNonNull(cause, "cause"); + } + + @Override + public Throwable cause() { + return cause; + } + + @Override + public boolean isSuccess() { + return false; + } + + @Override + public Future sync() { + throw new RuntimeException(cause); + } + + @Override + public Future syncUninterruptibly() { + throw new RuntimeException(cause); + } + + @Override + public V getNow() { + return null; + } +} diff --git a/src/main/java/org/xbib/event/FileFollowEvent.java b/src/main/java/org/xbib/event/FileFollowEvent.java deleted file mode 100644 index 017ea86..0000000 --- a/src/main/java/org/xbib/event/FileFollowEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.xbib.event; - -public class FileFollowEvent { - - public FileFollowEvent() { - } -} diff --git a/src/main/java/org/xbib/event/Future.java b/src/main/java/org/xbib/event/Future.java new file mode 100644 index 0000000..cbfd227 --- /dev/null +++ b/src/main/java/org/xbib/event/Future.java @@ -0,0 +1,39 @@ +package org.xbib.event; + +import java.util.concurrent.TimeUnit; + +public interface Future extends java.util.concurrent.Future { + boolean isSuccess(); + + boolean isCancellable(); + + Throwable cause(); + + Future addListener(GenericFutureListener> var1); + + Future addListeners(GenericFutureListener>... var1); + + Future removeListener(GenericFutureListener> var1); + + Future removeListeners(GenericFutureListener>... var1); + + Future sync() throws InterruptedException; + + Future syncUninterruptibly(); + + Future await() throws InterruptedException; + + Future awaitUninterruptibly(); + + boolean await(long var1, TimeUnit var3) throws InterruptedException; + + boolean await(long var1) throws InterruptedException; + + boolean awaitUninterruptibly(long var1, TimeUnit var3); + + boolean awaitUninterruptibly(long var1); + + V getNow(); + + boolean cancel(boolean var1); +} diff --git a/src/main/java/org/xbib/event/FutureListener.java b/src/main/java/org/xbib/event/FutureListener.java new file mode 100644 index 0000000..e4b2513 --- /dev/null +++ b/src/main/java/org/xbib/event/FutureListener.java @@ -0,0 +1,4 @@ +package org.xbib.event; + +public interface FutureListener extends GenericFutureListener> { +} diff --git a/src/main/java/org/xbib/event/GenericFutureListener.java b/src/main/java/org/xbib/event/GenericFutureListener.java new file mode 100644 index 0000000..935cf1e --- /dev/null +++ b/src/main/java/org/xbib/event/GenericFutureListener.java @@ -0,0 +1,7 @@ +package org.xbib.event; + +import java.util.EventListener; + +public interface GenericFutureListener> extends EventListener { + void operationComplete(F future) throws Exception; +} diff --git a/src/main/java/org/xbib/event/GenericProgressiveFutureListener.java b/src/main/java/org/xbib/event/GenericProgressiveFutureListener.java new file mode 100644 index 0000000..a1805ed --- /dev/null +++ b/src/main/java/org/xbib/event/GenericProgressiveFutureListener.java @@ -0,0 +1,12 @@ +package org.xbib.event; + +public interface GenericProgressiveFutureListener> extends GenericFutureListener { + /** + * Invoked when the operation has progressed. + * + * @param progress the progress of the operation so far (cumulative) + * @param total the number that signifies the end of the operation when {@code progress} reaches at it. + * {@code -1} if the end of operation is unknown. + */ + void operationProgressed(F future, long progress, long total) throws Exception; +} diff --git a/src/main/java/org/xbib/event/ProgressiveFuture.java b/src/main/java/org/xbib/event/ProgressiveFuture.java new file mode 100644 index 0000000..0e2463a --- /dev/null +++ b/src/main/java/org/xbib/event/ProgressiveFuture.java @@ -0,0 +1,31 @@ +package org.xbib.event; + +/** + * A {@link Future} which is used to indicate the progress of an operation. + */ +public interface ProgressiveFuture extends Future { + + @Override + ProgressiveFuture addListener(GenericFutureListener> listener); + + @Override + ProgressiveFuture addListeners(GenericFutureListener>... listeners); + + @Override + ProgressiveFuture removeListener(GenericFutureListener> listener); + + @Override + ProgressiveFuture removeListeners(GenericFutureListener>... listeners); + + @Override + ProgressiveFuture sync() throws InterruptedException; + + @Override + ProgressiveFuture syncUninterruptibly(); + + @Override + ProgressiveFuture await() throws InterruptedException; + + @Override + ProgressiveFuture awaitUninterruptibly(); +} diff --git a/src/main/java/org/xbib/event/ProgressivePromise.java b/src/main/java/org/xbib/event/ProgressivePromise.java new file mode 100644 index 0000000..40cd0d3 --- /dev/null +++ b/src/main/java/org/xbib/event/ProgressivePromise.java @@ -0,0 +1,50 @@ +package org.xbib.event; + +/** + * Special {@link ProgressiveFuture} which is writable. + */ +public interface ProgressivePromise extends Promise, ProgressiveFuture { + + /** + * Sets the current progress of the operation and notifies the listeners that implement + * {@link GenericProgressiveFutureListener}. + */ + ProgressivePromise setProgress(long progress, long total); + + /** + * Tries to set the current progress of the operation and notifies the listeners that implement + * {@link GenericProgressiveFutureListener}. If the operation is already complete or the progress is out of range, + * this method does nothing but returning {@code false}. + */ + boolean tryProgress(long progress, long total); + + @Override + ProgressivePromise setSuccess(V result); + + @Override + ProgressivePromise setFailure(Throwable cause); + + @Override + ProgressivePromise addListener(GenericFutureListener> listener); + + @Override + ProgressivePromise addListeners(GenericFutureListener>... listeners); + + @Override + ProgressivePromise removeListener(GenericFutureListener> listener); + + @Override + ProgressivePromise removeListeners(GenericFutureListener>... listeners); + + @Override + ProgressivePromise await() throws InterruptedException; + + @Override + ProgressivePromise awaitUninterruptibly(); + + @Override + ProgressivePromise sync() throws InterruptedException; + + @Override + ProgressivePromise syncUninterruptibly(); +} diff --git a/src/main/java/org/xbib/event/Promise.java b/src/main/java/org/xbib/event/Promise.java new file mode 100644 index 0000000..cd13a7a --- /dev/null +++ b/src/main/java/org/xbib/event/Promise.java @@ -0,0 +1,29 @@ +package org.xbib.event; + +public interface Promise extends Future { + Promise setSuccess(V value); + + boolean trySuccess(V value); + + Promise setFailure(Throwable value); + + boolean tryFailure(Throwable value); + + boolean setUncancellable(); + + Promise addListener(GenericFutureListener> value); + + Promise addListeners(GenericFutureListener>... value); + + Promise removeListener(GenericFutureListener> value); + + Promise removeListeners(GenericFutureListener>... value); + + Promise await() throws InterruptedException; + + Promise awaitUninterruptibly(); + + Promise sync() throws InterruptedException; + + Promise syncUninterruptibly(); +} diff --git a/src/main/java/org/xbib/event/PromiseTask.java b/src/main/java/org/xbib/event/PromiseTask.java new file mode 100644 index 0000000..e2c9f38 --- /dev/null +++ b/src/main/java/org/xbib/event/PromiseTask.java @@ -0,0 +1,175 @@ +package org.xbib.event; + +import org.xbib.event.loop.EventExecutor; + +import java.util.concurrent.Callable; +import java.util.concurrent.RunnableFuture; + +public class PromiseTask extends DefaultPromise implements RunnableFuture { + + private static final class RunnableAdapter implements Callable { + final Runnable task; + final T result; + + RunnableAdapter(Runnable task, T result) { + this.task = task; + this.result = result; + } + + @Override + public T call() { + task.run(); + return result; + } + + @Override + public String toString() { + return "Callable(task: " + task + ", result: " + result + ')'; + } + } + + private static final Runnable COMPLETED = new SentinelRunnable("COMPLETED"); + private static final Runnable CANCELLED = new SentinelRunnable("CANCELLED"); + private static final Runnable FAILED = new SentinelRunnable("FAILED"); + + private static class SentinelRunnable implements Runnable { + private final String name; + + SentinelRunnable(String name) { + this.name = name; + } + + @Override + public void run() { } // no-op + + @Override + public String toString() { + return name; + } + } + + // Strictly of type Callable or Runnable + private Object task; + + public PromiseTask(EventExecutor executor, Runnable runnable, V result) { + super(executor); + task = result == null ? runnable : new RunnableAdapter(runnable, result); + } + + public PromiseTask(EventExecutor executor, Runnable runnable) { + super(executor); + task = runnable; + } + + public PromiseTask(EventExecutor executor, Callable callable) { + super(executor); + task = callable; + } + + @Override + public final int hashCode() { + return System.identityHashCode(this); + } + + @Override + public final boolean equals(Object obj) { + return this == obj; + } + + @SuppressWarnings("unchecked") + V runTask() throws Throwable { + final Object task = this.task; + if (task instanceof Callable) { + return ((Callable) task).call(); + } + ((Runnable) task).run(); + return null; + } + + @Override + public void run() { + try { + if (setUncancellableInternal()) { + V result = runTask(); + setSuccessInternal(result); + } + } catch (Throwable e) { + setFailureInternal(e); + } + } + + private boolean clearTaskAfterCompletion(boolean done, Runnable result) { + if (done) { + // The only time where it might be possible for the sentinel task + // to be called is in the case of a periodic ScheduledFutureTask, + // in which case it's a benign race with cancellation and the (null) + // return value is not used. + task = result; + } + return done; + } + + @Override + public final Promise setFailure(Throwable cause) { + throw new IllegalStateException(); + } + + protected final Promise setFailureInternal(Throwable cause) { + super.setFailure(cause); + clearTaskAfterCompletion(true, FAILED); + return this; + } + + @Override + public final boolean tryFailure(Throwable cause) { + return false; + } + + protected final boolean tryFailureInternal(Throwable cause) { + return clearTaskAfterCompletion(super.tryFailure(cause), FAILED); + } + + @Override + public final Promise setSuccess(V result) { + throw new IllegalStateException(); + } + + protected final Promise setSuccessInternal(V result) { + super.setSuccess(result); + clearTaskAfterCompletion(true, COMPLETED); + return this; + } + + @Override + public final boolean trySuccess(V result) { + return false; + } + + protected final boolean trySuccessInternal(V result) { + return clearTaskAfterCompletion(super.trySuccess(result), COMPLETED); + } + + @Override + public final boolean setUncancellable() { + throw new IllegalStateException(); + } + + protected final boolean setUncancellableInternal() { + return super.setUncancellable(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return clearTaskAfterCompletion(super.cancel(mayInterruptIfRunning), CANCELLED); + } + + @Override + protected StringBuilder toStringBuilder() { + StringBuilder buf = super.toStringBuilder(); + buf.setCharAt(buf.length() - 1, ','); + + return buf.append(" task: ") + .append(task) + .append(')'); + } +} diff --git a/src/main/java/org/xbib/event/ScheduledFuture.java b/src/main/java/org/xbib/event/ScheduledFuture.java new file mode 100644 index 0000000..2eb7f0b --- /dev/null +++ b/src/main/java/org/xbib/event/ScheduledFuture.java @@ -0,0 +1,8 @@ +package org.xbib.event; + +/** + * The result of a scheduled asynchronous operation. + */ +@SuppressWarnings("ClassNameSameAsAncestorName") +public interface ScheduledFuture extends Future, java.util.concurrent.ScheduledFuture { +} diff --git a/src/main/java/org/xbib/event/ScheduledFutureTask.java b/src/main/java/org/xbib/event/ScheduledFutureTask.java new file mode 100644 index 0000000..5dbc178 --- /dev/null +++ b/src/main/java/org/xbib/event/ScheduledFutureTask.java @@ -0,0 +1,223 @@ +package org.xbib.event; + +import org.xbib.event.loop.AbstractScheduledEventExecutor; +import org.xbib.event.loop.EventExecutor; +import org.xbib.event.util.DefaultPriorityQueue; +import org.xbib.event.util.PriorityQueueNode; + +import java.util.concurrent.Callable; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("ComparableImplementedButEqualsNotOverridden") +public final class ScheduledFutureTask extends PromiseTask implements ScheduledFuture, PriorityQueueNode { + private static final long START_TIME = System.nanoTime(); + + public static long nanoTime() { + return System.nanoTime() - START_TIME; + } + + public static long deadlineNanos(long delay) { + long deadlineNanos = nanoTime() + delay; + // Guard against overflow + return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos; + } + + public static long initialNanoTime() { + return START_TIME; + } + + // set once when added to priority queue + private long id; + + private long deadlineNanos; + /* 0 - no repeat, >0 - repeat at fixed rate, <0 - repeat with fixed delay */ + private final long periodNanos; + + private int queueIndex = INDEX_NOT_IN_QUEUE; + + public ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Runnable runnable, long nanoTime) { + + super(executor, runnable); + deadlineNanos = nanoTime; + periodNanos = 0; + } + + public ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Runnable runnable, long nanoTime, long period) { + + super(executor, runnable); + deadlineNanos = nanoTime; + periodNanos = validatePeriod(period); + } + + public ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Callable callable, long nanoTime, long period) { + + super(executor, callable); + deadlineNanos = nanoTime; + periodNanos = validatePeriod(period); + } + + public ScheduledFutureTask(AbstractScheduledEventExecutor executor, + Callable callable, long nanoTime) { + + super(executor, callable); + deadlineNanos = nanoTime; + periodNanos = 0; + } + + private static long validatePeriod(long period) { + if (period == 0) { + throw new IllegalArgumentException("period: 0 (expected: != 0)"); + } + return period; + } + + public ScheduledFutureTask setId(long id) { + if (this.id == 0L) { + this.id = id; + } + return this; + } + + @Override + protected EventExecutor executor() { + return super.executor(); + } + + public long deadlineNanos() { + return deadlineNanos; + } + + public void setConsumed() { + // Optimization to avoid checking system clock again + // after deadline has passed and task has been dequeued + if (periodNanos == 0) { + assert nanoTime() >= deadlineNanos; + deadlineNanos = 0L; + } + } + + public long delayNanos() { + return deadlineToDelayNanos(deadlineNanos()); + } + + public static long deadlineToDelayNanos(long deadlineNanos) { + return deadlineNanos == 0L ? 0L : Math.max(0L, deadlineNanos - nanoTime()); + } + + public long delayNanos(long currentTimeNanos) { + return deadlineNanos == 0L ? 0L + : Math.max(0L, deadlineNanos() - (currentTimeNanos - START_TIME)); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(delayNanos(), TimeUnit.NANOSECONDS); + } + + @Override + public int compareTo(Delayed o) { + if (this == o) { + return 0; + } + + ScheduledFutureTask that = (ScheduledFutureTask) o; + long d = deadlineNanos() - that.deadlineNanos(); + if (d < 0) { + return -1; + } else if (d > 0) { + return 1; + } else if (id < that.id) { + return -1; + } else { + assert id != that.id; + return 1; + } + } + + @Override + public void run() { + assert executor().inEventLoop(); + try { + if (delayNanos() > 0L) { + // Not yet expired, need to add or remove from queue + if (isCancelled()) { + scheduledExecutor().scheduledTaskQueue().removeTyped(this); + } else { + scheduledExecutor().scheduleFromEventLoop(this); + } + return; + } + if (periodNanos == 0) { + if (setUncancellableInternal()) { + V result = runTask(); + setSuccessInternal(result); + } + } else { + // check if is done as it may was cancelled + if (!isCancelled()) { + runTask(); + if (!executor().isShutdown()) { + if (periodNanos > 0) { + deadlineNanos += periodNanos; + } else { + deadlineNanos = nanoTime() - periodNanos; + } + if (!isCancelled()) { + scheduledExecutor().scheduledTaskQueue().add(this); + } + } + } + } + } catch (Throwable cause) { + setFailureInternal(cause); + } + } + + private AbstractScheduledEventExecutor scheduledExecutor() { + return (AbstractScheduledEventExecutor) executor(); + } + + /** + * {@inheritDoc} + * + * @param mayInterruptIfRunning this value has no effect in this implementation. + */ + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + boolean canceled = super.cancel(mayInterruptIfRunning); + if (canceled) { + scheduledExecutor().removeScheduled(this); + } + return canceled; + } + + public boolean cancelWithoutRemove(boolean mayInterruptIfRunning) { + return super.cancel(mayInterruptIfRunning); + } + + @Override + protected StringBuilder toStringBuilder() { + StringBuilder buf = super.toStringBuilder(); + buf.setCharAt(buf.length() - 1, ','); + + return buf.append(" deadline: ") + .append(deadlineNanos) + .append(", period: ") + .append(periodNanos) + .append(')'); + } + + @Override + public int priorityQueueIndex(DefaultPriorityQueue queue) { + return queueIndex; + } + + @Override + public void priorityQueueIndex(DefaultPriorityQueue queue, int i) { + queueIndex = i; + } +} diff --git a/src/main/java/org/xbib/event/SucceededFuture.java b/src/main/java/org/xbib/event/SucceededFuture.java new file mode 100644 index 0000000..c4a439d --- /dev/null +++ b/src/main/java/org/xbib/event/SucceededFuture.java @@ -0,0 +1,37 @@ +package org.xbib.event; + +import org.xbib.event.loop.EventExecutor; + +/** + * The {@link CompleteFuture} which is succeeded already. It is + * recommended to use {@link EventExecutor#newSucceededFuture(Object)} instead of + * calling the constructor of this future. + */ +public final class SucceededFuture extends CompleteFuture { + private final V result; + + /** + * Creates a new instance. + * + * @param executor the {@link EventExecutor} associated with this future + */ + public SucceededFuture(EventExecutor executor, V result) { + super(executor); + this.result = result; + } + + @Override + public Throwable cause() { + return null; + } + + @Override + public boolean isSuccess() { + return true; + } + + @Override + public V getNow() { + return result; + } +} diff --git a/src/main/java/org/xbib/event/async/Async.java b/src/main/java/org/xbib/event/async/Async.java new file mode 100644 index 0000000..8e5e772 --- /dev/null +++ b/src/main/java/org/xbib/event/async/Async.java @@ -0,0 +1,189 @@ +package org.xbib.event.async; + +import org.xbib.event.async.impl.ContextInternal; +import org.xbib.event.async.impl.AsyncBuilder; + +/** + * The entry point into the Async API. + * + */ +public interface Async { + + /** + * Creates an instance using default options. + * + * @return the instance + */ + static Async getInstance() { + return getInstance(new AsyncOptions()); + } + + /** + * Creates a non clustered instance using the specified options + * + * @param options the options to use + * @return the instance + */ + static Async getInstance(AsyncOptions options) { + return new AsyncBuilder(options).init().newInstance(); + } + + /** + * Gets the current context + * + * @return The current context or {@code null} if there is no current context + */ + static Context currentContext() { + return ContextInternal.current(); + } + + /** + * Gets the current context, or creates one if there isn't one + * + * @return The current context (created if didn't exist) + */ + Context getOrCreateContext(); + + /** + * Set a one-shot timer to fire after {@code delay} milliseconds, at which point {@code handler} will be called with + * the id of the timer. + * + * @param delay the delay in milliseconds, after which the timer will fire + * @param handler the handler that will be called with the timer ID when the timer fires + * @return the unique ID of the timer + */ + long setTimer(long delay, Handler handler); + + /** + * Returns a one-shot timer as a read stream. + * + * @param delay the delay in milliseconds, after which the timer will fire + * @return the timer stream + */ + TimeoutStream timerStream(long delay); + + /** + * Set a periodic timer to fire every {@code delay} milliseconds, at which point {@code handler} will be called with + * the id of the timer. + * + * @param delay the delay in milliseconds, after which the timer will fire + * @param handler the handler that will be called with the timer ID when the timer fires + * @return the unique ID of the timer + */ + default long setPeriodic(long delay, Handler handler) { + return setPeriodic(delay, delay, handler); + } + + /** + * Set a periodic timer to fire every {@code delay} milliseconds with initial delay, at which point {@code handler} will be called with + * the id of the timer. + * + * @param initialDelay the initial delay in milliseconds + * @param delay the delay in milliseconds, after which the timer will fire + * @param handler the handler that will be called with the timer ID when the timer fires + * @return the unique ID of the timer + */ + long setPeriodic(long initialDelay, long delay, Handler handler); + + /** + * Returns a periodic timer as a read stream. + * + * @param delay the delay in milliseconds, after which the timer will fire + * @return the periodic stream + */ + default TimeoutStream periodicStream(long delay) { + return periodicStream(0, delay); + } + + /** + * Returns a periodic timer as a read stream. + * + * @param initialDelay the initial delay in milliseconds + * @param delay the delay in milliseconds, after which the timer will fire + * @return the periodic stream + */ + TimeoutStream periodicStream(long initialDelay, long delay); + + /** + * Cancels the timer with the specified {@code id}. + * + * @param id The id of the timer to cancel + * @return true if the timer was successfully cancelled, or false if the timer does not exist. + */ + boolean cancelTimer(long id); + + /** + * Puts the handler on the event queue for the current context so it will be run asynchronously ASAP after all + * preceeding events have been handled. + * + * @param action - a handler representing the action to execute + */ + void runOnContext(Handler action); + + /** + * Stop the Async instance and release any resources held by it. + *

+ * The instance cannot be used after it has been closed. + *

+ * The actual close is asynchronous and may not complete until after the call has returned. + * + * @return a future completed with the result + */ + Future close(); + + /** + * Safely execute some blocking code. + *

+ * Executes the blocking code in the handler {@code blockingCodeHandler} using a thread from the worker pool. + *

+ * When the code is complete the handler {@code resultHandler} will be called with the result on the original context + * (e.g. on the original event loop of the caller). + *

+ * A {@code Future} instance is passed into {@code blockingCodeHandler}. When the blocking code successfully completes, + * the handler should call the {@link Promise#complete} or {@link Promise#complete(Object)} method, or the {@link Promise#fail} + * method if it failed. + *

+ * In the {@code blockingCodeHandler} the current context remains the original context and therefore any task + * scheduled in the {@code blockingCodeHandler} will be executed on the this context and not on the worker thread. + *

+ * The blocking code should block for a reasonable amount of time (i.e no more than a few seconds). Long blocking operations + * or polling operations (i.e a thread that spin in a loop polling events in a blocking fashion) are precluded. + *

+ * When the blocking operation lasts more than the 10 seconds, a message will be printed on the console by the + * blocked thread checker. + *

+ * Long blocking operations should use a dedicated thread managed by the application, which can interact with + * subscribers using the event-bus or {@link Context#runOnContext(Handler)} + * + * @param blockingCodeHandler handler representing the blocking code to run + * @param ordered if true then if executeBlocking is called several times on the same context, the executions + * for that context will be executed serially, not in parallel. if false then they will be no ordering + * guarantees + * @param the type of the result + * @return a future completed when the blocking code is complete + */ + default Future executeBlocking(Handler> blockingCodeHandler, boolean ordered) { + Context context = getOrCreateContext(); + return context.executeBlocking(blockingCodeHandler, ordered); + } + + /** + * Like {@link #executeBlocking(Handler, boolean)} called with ordered = true. + */ + default Future executeBlocking(Handler> blockingCodeHandler) { + return executeBlocking(blockingCodeHandler, true); + } + + /** + * Set a default exception handler for {@link Context}, set on {@link Context#exceptionHandler(Handler)} at creation. + * + * @param handler the exception handler + * @return a reference to this, so the API can be used fluently + */ + Async exceptionHandler(Handler handler); + + /** + * @return the current default exception handler + */ + Handler exceptionHandler(); +} diff --git a/src/main/java/org/xbib/event/async/AsyncOptions.java b/src/main/java/org/xbib/event/async/AsyncOptions.java new file mode 100644 index 0000000..ad104a7 --- /dev/null +++ b/src/main/java/org/xbib/event/async/AsyncOptions.java @@ -0,0 +1,447 @@ +package org.xbib.event.async; + +import java.util.concurrent.TimeUnit; + +/** + * Instances of this class are used to configure {@link Async} instances. + */ +public class AsyncOptions { + + private static final String DISABLE_TCCL_PROP_NAME = "org.xbib.disableTCCL"; + + /** + * The default number of event loop threads to be used = 2 * number of cores on the machine + */ + public static final int DEFAULT_EVENT_LOOP_POOL_SIZE = 2 * Runtime.getRuntime().availableProcessors(); + + /** + * The default number of threads in the worker pool = 20 + */ + public static final int DEFAULT_WORKER_POOL_SIZE = 20; + + /** + * The default number of threads in the internal blocking pool (used by some internal operations) = 20 + */ + public static final int DEFAULT_INTERNAL_BLOCKING_POOL_SIZE = 20; + + /** + * The default value of blocked thread check interval = 1000 ms. + */ + public static final long DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL = TimeUnit.SECONDS.toMillis(1); + + /** + * The default value of blocked thread check interval unit = {@link TimeUnit#MILLISECONDS} + */ + public static final TimeUnit DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL_UNIT = TimeUnit.MILLISECONDS; + + /** + * The default value of max event loop execute time = 2000000000 ns (2 seconds) + */ + public static final long DEFAULT_MAX_EVENT_LOOP_EXECUTE_TIME = TimeUnit.SECONDS.toNanos(2); + + /** + * The default value of max event loop execute time unit = {@link TimeUnit#NANOSECONDS} + */ + public static final TimeUnit DEFAULT_MAX_EVENT_LOOP_EXECUTE_TIME_UNIT = TimeUnit.NANOSECONDS; + + /** + * The default value of max worker execute time = 60000000000 ns (60 seconds) + */ + public static final long DEFAULT_MAX_WORKER_EXECUTE_TIME = TimeUnit.SECONDS.toNanos(60); + + /** + * The default value of max worker execute time unit = {@link TimeUnit#NANOSECONDS} + */ + public static final TimeUnit DEFAULT_MAX_WORKER_EXECUTE_TIME_UNIT = TimeUnit.NANOSECONDS; + + /** + * The default value of warning exception time 5000000000 ns (5 seconds) + * If a thread is blocked longer than this threshold, the warning log + * contains a stack trace + */ + private static final long DEFAULT_WARNING_EXCEPTION_TIME = TimeUnit.SECONDS.toNanos(5); + + /** + * The default value of warning exception time unit = {@link TimeUnit#NANOSECONDS} + */ + public static final TimeUnit DEFAULT_WARNING_EXCEPTION_TIME_UNIT = TimeUnit.NANOSECONDS; + + public static final boolean DEFAULT_DISABLE_TCCL = Boolean.getBoolean(DISABLE_TCCL_PROP_NAME); + + /** + * Set default value to false for aligning with the old behavior + * By default, threads are NOT daemons - we want them to prevent JVM exit so embedded user + * doesn't have to explicitly prevent JVM from exiting. + */ + public static final boolean DEFAULT_USE_DAEMON_THREAD = false; + + private int eventLoopPoolSize = DEFAULT_EVENT_LOOP_POOL_SIZE; + private int workerPoolSize = DEFAULT_WORKER_POOL_SIZE; + private int internalBlockingPoolSize = DEFAULT_INTERNAL_BLOCKING_POOL_SIZE; + private long blockedThreadCheckInterval = DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL; + private long maxEventLoopExecuteTime = DEFAULT_MAX_EVENT_LOOP_EXECUTE_TIME; + private long maxWorkerExecuteTime = DEFAULT_MAX_WORKER_EXECUTE_TIME; + private long warningExceptionTime = DEFAULT_WARNING_EXCEPTION_TIME; + private TimeUnit maxEventLoopExecuteTimeUnit = DEFAULT_MAX_EVENT_LOOP_EXECUTE_TIME_UNIT; + private TimeUnit maxWorkerExecuteTimeUnit = DEFAULT_MAX_WORKER_EXECUTE_TIME_UNIT; + private TimeUnit warningExceptionTimeUnit = DEFAULT_WARNING_EXCEPTION_TIME_UNIT; + private TimeUnit blockedThreadCheckIntervalUnit = DEFAULT_BLOCKED_THREAD_CHECK_INTERVAL_UNIT; + private boolean disableTCCL = DEFAULT_DISABLE_TCCL; + private Boolean useDaemonThread = DEFAULT_USE_DAEMON_THREAD; + + /** + * Default constructor + */ + public AsyncOptions() { + } + + /** + * Copy constructor + * + * @param other The other {@code AsyncOptions} to copy when creating this + */ + public AsyncOptions(AsyncOptions other) { + this.eventLoopPoolSize = other.getEventLoopPoolSize(); + this.workerPoolSize = other.getWorkerPoolSize(); + this.blockedThreadCheckInterval = other.getBlockedThreadCheckInterval(); + this.maxEventLoopExecuteTime = other.getMaxEventLoopExecuteTime(); + this.maxWorkerExecuteTime = other.getMaxWorkerExecuteTime(); + this.internalBlockingPoolSize = other.getInternalBlockingPoolSize(); + this.warningExceptionTime = other.warningExceptionTime; + this.maxEventLoopExecuteTimeUnit = other.maxEventLoopExecuteTimeUnit; + this.maxWorkerExecuteTimeUnit = other.maxWorkerExecuteTimeUnit; + this.warningExceptionTimeUnit = other.warningExceptionTimeUnit; + this.blockedThreadCheckIntervalUnit = other.blockedThreadCheckIntervalUnit; + this.disableTCCL = other.disableTCCL; + this.useDaemonThread = other.useDaemonThread; + } + + /** + * Get the number of event loop threads to be used by the instance. + * + * @return the number of threads + */ + public int getEventLoopPoolSize() { + return eventLoopPoolSize; + } + + /** + * Set the number of event loop threads to be used by the instance. + * + * @param eventLoopPoolSize the number of threads + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setEventLoopPoolSize(int eventLoopPoolSize) { + if (eventLoopPoolSize < 1) { + throw new IllegalArgumentException("eventLoopPoolSize must be > 0"); + } + this.eventLoopPoolSize = eventLoopPoolSize; + return this; + } + + /** + * Get the maximum number of worker threads to be used by the instance. + *

+ * Worker threads are used for running blocking code. + * + * @return the maximum number of worker threads + */ + public int getWorkerPoolSize() { + return workerPoolSize; + } + + /** + * Set the maximum number of worker threads to be used by the instance. + * + * @param workerPoolSize the number of threads + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setWorkerPoolSize(int workerPoolSize) { + if (workerPoolSize < 1) { + throw new IllegalArgumentException("workerPoolSize must be > 0"); + } + this.workerPoolSize = workerPoolSize; + return this; + } + + /** + * Get the value of blocked thread check period, in {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit}. + *

+ * This setting determines how often we will check whether event loop threads are executing for too long. + *

+ * The default value of {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit} is {@link TimeUnit#MILLISECONDS}. + * + * @return the value of blocked thread check period, in {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit}. + */ + public long getBlockedThreadCheckInterval() { + return blockedThreadCheckInterval; + } + + /** + * Sets the value of blocked thread check period, in {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit}. + *

+ * The default value of {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit} is {@link TimeUnit#MILLISECONDS} + * + * @param blockedThreadCheckInterval the value of blocked thread check period, in {@link AsyncOptions#setBlockedThreadCheckIntervalUnit blockedThreadCheckIntervalUnit}. + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setBlockedThreadCheckInterval(long blockedThreadCheckInterval) { + if (blockedThreadCheckInterval < 1) { + throw new IllegalArgumentException("blockedThreadCheckInterval must be > 0"); + } + this.blockedThreadCheckInterval = blockedThreadCheckInterval; + return this; + } + + /** + * Get the value of max event loop execute time, in {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit}. + *

+ * We will automatically log a warning if it detects that event loop threads haven't returned within this time. + *

+ * This can be used to detect where the user is blocking an event loop thread, contrary to the Golden Rule of the + * holy Event Loop. + *

+ * The default value of {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit} is {@link TimeUnit#NANOSECONDS} + * + * @return the value of max event loop execute time, in {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit}. + */ + public long getMaxEventLoopExecuteTime() { + return maxEventLoopExecuteTime; + } + + /** + * Sets the value of max event loop execute time, in {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit}. + *

+ * The default value of {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit}is {@link TimeUnit#NANOSECONDS} + * + * @param maxEventLoopExecuteTime the value of max event loop execute time, in {@link AsyncOptions#setMaxEventLoopExecuteTimeUnit maxEventLoopExecuteTimeUnit}. + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setMaxEventLoopExecuteTime(long maxEventLoopExecuteTime) { + if (maxEventLoopExecuteTime < 1) { + throw new IllegalArgumentException("maxEventLoopExecuteTime must be > 0"); + } + this.maxEventLoopExecuteTime = maxEventLoopExecuteTime; + return this; + } + + /** + * Get the value of max worker execute time, in {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit}. + *

+ * We will automatically log a warning if it detects that worker threads haven't returned within this time. + *

+ * This can be used to detect where the user is blocking a worker thread for too long. Although worker threads + * can be blocked longer than event loop threads, they shouldn't be blocked for long periods of time. + *

+ * The default value of {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit} is {@link TimeUnit#NANOSECONDS} + * + * @return The value of max worker execute time, in {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit}. + */ + public long getMaxWorkerExecuteTime() { + return maxWorkerExecuteTime; + } + + /** + * Sets the value of max worker execute time, in {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit}. + *

+ * The default value of {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit} is {@link TimeUnit#NANOSECONDS} + * + * @param maxWorkerExecuteTime the value of max worker execute time, in {@link AsyncOptions#setMaxWorkerExecuteTimeUnit maxWorkerExecuteTimeUnit}. + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setMaxWorkerExecuteTime(long maxWorkerExecuteTime) { + if (maxWorkerExecuteTime < 1) { + throw new IllegalArgumentException("maxWorkerpExecuteTime must be > 0"); + } + this.maxWorkerExecuteTime = maxWorkerExecuteTime; + return this; + } + + /** + * Get the value of internal blocking pool size. + *

+ * We maintain a pool for internal blocking operations. + * + * @return the value of internal blocking pool size + */ + public int getInternalBlockingPoolSize() { + return internalBlockingPoolSize; + } + + /** + * Set the value of internal blocking pool size + * + * @param internalBlockingPoolSize the maximumn number of threads in the internal blocking pool + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setInternalBlockingPoolSize(int internalBlockingPoolSize) { + if (internalBlockingPoolSize < 1) { + throw new IllegalArgumentException("internalBlockingPoolSize must be > 0"); + } + this.internalBlockingPoolSize = internalBlockingPoolSize; + return this; + } + + /** + * Get the threshold value above this, the blocked warning contains a stack trace. in {@link AsyncOptions#setWarningExceptionTimeUnit warningExceptionTimeUnit}. + *

+ * The default value of {@link AsyncOptions#setWarningExceptionTimeUnit warningExceptionTimeUnit} is {@link TimeUnit#NANOSECONDS} + * + * @return the warning exception time threshold + */ + public long getWarningExceptionTime() { + return warningExceptionTime; + } + + /** + * Set the threshold value above this, the blocked warning contains a stack trace. in {@link AsyncOptions#setWarningExceptionTimeUnit warningExceptionTimeUnit}. + * The default value of {@link AsyncOptions#setWarningExceptionTimeUnit warningExceptionTimeUnit} is {@link TimeUnit#NANOSECONDS} + * + * @param warningExceptionTime + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setWarningExceptionTime(long warningExceptionTime) { + if (warningExceptionTime < 1) { + throw new IllegalArgumentException("warningExceptionTime must be > 0"); + } + this.warningExceptionTime = warningExceptionTime; + return this; + } + + /** + * @return the time unit of {@code maxEventLoopExecuteTime} + */ + public TimeUnit getMaxEventLoopExecuteTimeUnit() { + return maxEventLoopExecuteTimeUnit; + } + + /** + * Set the time unit of {@code maxEventLoopExecuteTime}. + * + * @param maxEventLoopExecuteTimeUnit the time unit of {@code maxEventLoopExecuteTime} + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setMaxEventLoopExecuteTimeUnit(TimeUnit maxEventLoopExecuteTimeUnit) { + this.maxEventLoopExecuteTimeUnit = maxEventLoopExecuteTimeUnit; + return this; + } + + /** + * @return the time unit of {@code maxWorkerExecuteTime} + */ + public TimeUnit getMaxWorkerExecuteTimeUnit() { + return maxWorkerExecuteTimeUnit; + } + + /** + * Set the time unit of {@code maxWorkerExecuteTime}. + * + * @param maxWorkerExecuteTimeUnit the time unit of {@code maxWorkerExecuteTime} + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setMaxWorkerExecuteTimeUnit(TimeUnit maxWorkerExecuteTimeUnit) { + this.maxWorkerExecuteTimeUnit = maxWorkerExecuteTimeUnit; + return this; + } + + /** + * @return the time unit of {@code warningExceptionTime} + */ + public TimeUnit getWarningExceptionTimeUnit() { + return warningExceptionTimeUnit; + } + + /** + * Set the time unit of {@code warningExceptionTime}. + * + * @param warningExceptionTimeUnit the time unit of {@code warningExceptionTime} + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setWarningExceptionTimeUnit(TimeUnit warningExceptionTimeUnit) { + this.warningExceptionTimeUnit = warningExceptionTimeUnit; + return this; + } + + /** + * @return the time unit of {@code blockedThreadCheckInterval} + */ + public TimeUnit getBlockedThreadCheckIntervalUnit() { + return blockedThreadCheckIntervalUnit; + } + + /** + * Set the time unit of {@code blockedThreadCheckInterval}. + * + * @param blockedThreadCheckIntervalUnit the time unit of {@code warningExceptionTime} + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setBlockedThreadCheckIntervalUnit(TimeUnit blockedThreadCheckIntervalUnit) { + this.blockedThreadCheckIntervalUnit = blockedThreadCheckIntervalUnit; + return this; + } + + /** + * @return whether we set the {@link Context} classloader as the thread context classloader on actions executed on that {@link Context} + */ + public boolean getDisableTCCL() { + return disableTCCL; + } + + /** + * Configures whether we set the {@link Context} classloader as the thread context classloader on actions executed on that {@link Context}. + * + * When a {@link Context} is created the current thread classloader is captured and associated with this classloader. + * + * This setting overrides the (legacy) system property {@code org.xbib.disableTCCL} and provides control at the + * Async instance level. + * + * @param disableTCCL {@code true} to disable thread context classloader update by Async + * @return a reference to this, so the API can be used fluently + */ + public AsyncOptions setDisableTCCL(boolean disableTCCL) { + this.disableTCCL = disableTCCL; + return this; + } + + /** + * Returns whether we want to use daemon thread. + * + * @return {@code true} means daemon, {@code false} means not daemon(user), {@code null} means do + * not change the daemon option of the created thread. + */ + public Boolean getUseDaemonThread() { + return useDaemonThread; + } + + /** + * Mark the thread as daemon thread or user thread. + *

+ * For keeping the old behavior, the default value is {@code false} instead of {@code null}. + * + * @param daemon {@code true} means daemon, {@code false} means not daemon(user), {@code null} + * means do not change the daemon option of the created thread. + */ + public AsyncOptions setUseDaemonThread(Boolean daemon) { + this.useDaemonThread = daemon; + return this; + } + + @Override + public String toString() { + return "AsyncOptions{" + + "eventLoopPoolSize=" + eventLoopPoolSize + + ", workerPoolSize=" + workerPoolSize + + ", internalBlockingPoolSize=" + internalBlockingPoolSize + + ", blockedThreadCheckIntervalUnit=" + blockedThreadCheckIntervalUnit + + ", blockedThreadCheckInterval=" + blockedThreadCheckInterval + + ", maxEventLoopExecuteTimeUnit=" + maxEventLoopExecuteTimeUnit + + ", maxEventLoopExecuteTime=" + maxEventLoopExecuteTime + + ", maxWorkerExecuteTimeUnit=" + maxWorkerExecuteTimeUnit + + ", maxWorkerExecuteTime=" + maxWorkerExecuteTime + + ", warningExceptionTimeUnit=" + warningExceptionTimeUnit + + ", warningExceptionTime=" + warningExceptionTime + + ", disableTCCL=" + disableTCCL + + ", useDaemonThread=" + useDaemonThread + + '}'; + } +} diff --git a/src/main/java/org/xbib/event/async/AsyncResult.java b/src/main/java/org/xbib/event/async/AsyncResult.java new file mode 100644 index 0000000..76d09c3 --- /dev/null +++ b/src/main/java/org/xbib/event/async/AsyncResult.java @@ -0,0 +1,188 @@ +package org.xbib.event.async; + +import java.util.function.Function; + +/** + * Encapsulates the result of an asynchronous operation. + *

+ * Many operations in Vert.x APIs provide results back by passing an instance of this in a {@link Handler}. + *

+ * The result can either have failed or succeeded. + *

+ * If it failed then the cause of the failure is available with {@link #cause}. + *

+ * If it succeeded then the actual result is available with {@link #result} + * + */ +public interface AsyncResult { + + /** + * The result of the operation. This will be null if the operation failed. + * + * @return the result or null if the operation failed. + */ + T result(); + + /** + * A Throwable describing failure. This will be null if the operation succeeded. + * + * @return the cause or null if the operation succeeded. + */ + Throwable cause(); + + /** + * Did it succeed? + * + * @return true if it succeded or false otherwise + */ + boolean succeeded(); + + /** + * Did it fail? + * + * @return true if it failed or false otherwise + */ + boolean failed(); + + /** + * Apply a {@code mapper} function on this async result.

+ * + * The {@code mapper} is called with the completed value and this mapper returns a value. This value will complete the result returned by this method call.

+ * + * When this async result is failed, the failure will be propagated to the returned async result and the {@code mapper} will not be called. + * + * @param mapper the mapper function + * @return the mapped async result + */ + default AsyncResult map(Function mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + return new AsyncResult() { + @Override + public U result() { + if (succeeded()) { + return mapper.apply(AsyncResult.this.result()); + } else { + return null; + } + } + + @Override + public Throwable cause() { + return AsyncResult.this.cause(); + } + + @Override + public boolean succeeded() { + return AsyncResult.this.succeeded(); + } + + @Override + public boolean failed() { + return AsyncResult.this.failed(); + } + }; + } + + /** + * Map the result of this async result to a specific {@code value}.

+ * + * When this async result succeeds, this {@code value} will succeeed the async result returned by this method call.

+ * + * When this async result fails, the failure will be propagated to the returned async result. + * + * @param value the value that eventually completes the mapped async result + * @return the mapped async result + */ + default AsyncResult map(V value) { + return map(t -> value); + } + + /** + * Map the result of this async result to {@code null}.

+ * + * This is a convenience for {@code asyncResult.map((T) null)} or {@code asyncResult.map((Void) null)}.

+ * + * When this async result succeeds, {@code null} will succeeed the async result returned by this method call.

+ * + * When this async result fails, the failure will be propagated to the returned async result. + * + * @return the mapped async result + */ + default AsyncResult mapEmpty() { + return map((V)null); + } + + /** + * Apply a {@code mapper} function on this async result.

+ * + * The {@code mapper} is called with the failure and this mapper returns a value. This value will complete the result returned by this method call.

+ * + * When this async result is succeeded, the value will be propagated to the returned async result and the {@code mapper} will not be called. + * + * @param mapper the mapper function + * @return the mapped async result + */ + default AsyncResult otherwise(Function mapper) { + if (mapper == null) { + throw new NullPointerException(); + } + return new AsyncResult() { + @Override + public T result() { + if (AsyncResult.this.succeeded()) { + return AsyncResult.this.result(); + } else if (AsyncResult.this.failed()) { + return mapper.apply(AsyncResult.this.cause()); + } else { + return null; + } + } + + @Override + public Throwable cause() { + return null; + } + + @Override + public boolean succeeded() { + return AsyncResult.this.succeeded() || AsyncResult.this.failed(); + } + + @Override + public boolean failed() { + return false; + } + }; + } + + /** + * Map the failure of this async result to a specific {@code value}.

+ * + * When this async result fails, this {@code value} will succeeed the async result returned by this method call.

+ * + * When this async succeeds, the result will be propagated to the returned async result. + * + * @param value the value that eventually completes the mapped async result + * @return the mapped async result + */ + default AsyncResult otherwise(T value) { + return otherwise(err -> value); + } + + /** + * Map the failure of this async result to {@code null}.

+ * + * This is a convenience for {@code asyncResult.otherwise((T) null)}.

+ * + * When this async result fails, the {@code null} will succeeed the async result returned by this method call.

+ * + * When this async succeeds, the result will be propagated to the returned async result. + * + * @return the mapped async result + */ + default AsyncResult otherwiseEmpty() { + return otherwise(err -> null); + } +} diff --git a/src/main/java/org/xbib/event/async/Closeable.java b/src/main/java/org/xbib/event/async/Closeable.java new file mode 100644 index 0000000..39065c4 --- /dev/null +++ b/src/main/java/org/xbib/event/async/Closeable.java @@ -0,0 +1,16 @@ +package org.xbib.event.async; + +/** + * A closeable resource. + *

+ * This interface is mostly used for internal resource management. + */ +public interface Closeable { + + /** + * Close this resource, the {@code completion} promise must be notified when the operation has completed. + * + * @param completion the promise to signal when close has completed + */ + void close(Promise completion); +} diff --git a/src/main/java/org/xbib/event/async/CompositeFuture.java b/src/main/java/org/xbib/event/async/CompositeFuture.java new file mode 100644 index 0000000..14c3a35 --- /dev/null +++ b/src/main/java/org/xbib/event/async/CompositeFuture.java @@ -0,0 +1,245 @@ +package org.xbib.event.async; + +import org.xbib.event.async.impl.future.CompositeFutureImpl; + +import java.util.ArrayList; +import java.util.List; + +/** + * The composite future wraps a list of {@link Future futures}, it is useful when several futures + * needs to be coordinated. + * The handlers set for the coordinated futures are overridden by the handler of the composite future. + * + */ +public interface CompositeFuture extends Future { + + /** + * Return a composite future, succeeded when all futures are succeeded, failed when any future is failed. + *

+ * The returned future fails as soon as one of {@code f1} or {@code f2} fails. + * + * @param f1 future + * @param f2 future + * @return the composite future + */ + static CompositeFuture all(Future f1, Future f2) { + return CompositeFutureImpl.all(f1, f2); + } + + /** + * Like {@link #all(Future, Future)} but with 3 futures. + */ + static CompositeFuture all(Future f1, Future f2, Future f3) { + return CompositeFutureImpl.all(f1, f2, f3); + } + + /** + * Like {@link #all(Future, Future)} but with 4 futures. + */ + static CompositeFuture all(Future f1, Future f2, Future f3, Future f4) { + return CompositeFutureImpl.all(f1, f2, f3, f4); + } + + /** + * Like {@link #all(Future, Future)} but with 5 futures. + */ + static CompositeFuture all(Future f1, Future f2, Future f3, Future f4, Future f5) { + return CompositeFutureImpl.all(f1, f2, f3, f4, f5); + } + + /** + * Like {@link #all(Future, Future)} but with 6 futures. + */ + static CompositeFuture all(Future f1, Future f2, Future f3, Future f4, Future f5, Future f6) { + return CompositeFutureImpl.all(f1, f2, f3, f4, f5, f6); + } + + /** + * Like {@link #all(Future, Future)} but with a list of futures.

+ * + * When the list is empty, the returned future will be already completed. + */ + static CompositeFuture all(List futures) { + return CompositeFutureImpl.all(futures.toArray(new Future[0])); + } + + /** + * Return a composite future, succeeded when any futures is succeeded, failed when all futures are failed. + *

+ * The returned future succeeds as soon as one of {@code f1} or {@code f2} succeeds. + * + * @param f1 future + * @param f2 future + * @return the composite future + */ + static CompositeFuture any(Future f1, Future f2) { + return CompositeFutureImpl.any(f1, f2); + } + + /** + * Like {@link #any(Future, Future)} but with 3 futures. + */ + static CompositeFuture any(Future f1, Future f2, Future f3) { + return CompositeFutureImpl.any(f1, f2, f3); + } + + /** + * Like {@link #any(Future, Future)} but with 4 futures. + */ + static CompositeFuture any(Future f1, Future f2, Future f3, Future f4) { + return CompositeFutureImpl.any(f1, f2, f3, f4); + } + + /** + * Like {@link #any(Future, Future)} but with 5 futures. + */ + static CompositeFuture any(Future f1, Future f2, Future f3, Future f4, Future f5) { + return CompositeFutureImpl.any(f1, f2, f3, f4, f5); + } + + /** + * Like {@link #any(Future, Future)} but with 6 futures. + */ + static CompositeFuture any(Future f1, Future f2, Future f3, Future f4, Future f5, Future f6) { + return CompositeFutureImpl.any(f1, f2, f3, f4, f5, f6); + } + + /** + * Like {@link #any(Future, Future)} but with a list of futures.

+ * + * When the list is empty, the returned future will be already completed. + */ + static CompositeFuture any(List futures) { + return CompositeFutureImpl.any(futures.toArray(new Future[0])); + } + + /** + * Return a composite future, succeeded when all futures are succeeded, failed when any future is failed. + *

+ * It always wait until all its futures are completed and will not fail as soon as one of {@code f1} or {@code f2} fails. + * + * @param f1 future + * @param f2 future + * @return the composite future + */ + static CompositeFuture join(Future f1, Future f2) { + return CompositeFutureImpl.join(f1, f2); + } + + /** + * Like {@link #join(Future, Future)} but with 3 futures. + */ + static CompositeFuture join(Future f1, Future f2, Future f3) { + return CompositeFutureImpl.join(f1, f2, f3); + } + + /** + * Like {@link #join(Future, Future)} but with 4 futures. + */ + static CompositeFuture join(Future f1, Future f2, Future f3, Future f4) { + return CompositeFutureImpl.join(f1, f2, f3, f4); + } + + /** + * Like {@link #join(Future, Future)} but with 5 futures. + */ + static CompositeFuture join(Future f1, Future f2, Future f3, Future f4, Future f5) { + return CompositeFutureImpl.join(f1, f2, f3, f4, f5); + } + + /** + * Like {@link #join(Future, Future)} but with 6 futures. + */ + static CompositeFuture join(Future f1, Future f2, Future f3, Future f4, Future f5, Future f6) { + return CompositeFutureImpl.join(f1, f2, f3, f4, f5, f6); + } + + /** + * Like {@link #join(Future, Future)} but with a list of futures.

+ * + * When the list is empty, the returned future will be already completed. + */ + static CompositeFuture join(List futures) { + return CompositeFutureImpl.join(futures.toArray(new Future[0])); + } + + @Override + CompositeFuture onComplete(Handler> handler); + + @Override + default CompositeFuture onSuccess(Handler handler) { + Future.super.onSuccess(handler); + return this; + } + + @Override + default CompositeFuture onFailure(Handler handler) { + Future.super.onFailure(handler); + return this; + } + + /** + * Returns a cause of a wrapped future + * + * @param index the wrapped future index + */ + Throwable cause(int index); + + /** + * Returns true if a wrapped future is succeeded + * + * @param index the wrapped future index + */ + boolean succeeded(int index); + + /** + * Returns true if a wrapped future is failed + * + * @param index the wrapped future index + */ + boolean failed(int index); + + /** + * Returns true if a wrapped future is completed + * + * @param index the wrapped future index + */ + boolean isComplete(int index); + + /** + * Returns the result of a wrapped future + * + * @param index the wrapped future index + */ + T resultAt(int index); + + /** + * @return the number of wrapped future + */ + int size(); + + /** + * @return a list of the current completed values. If one future is not yet resolved or is failed, {@code} null + * will be used + */ + default List list() { + int size = size(); + ArrayList list = new ArrayList<>(size); + for (int index = 0;index < size;index++) { + list.add(resultAt(index)); + } + return list; + } + + /** + * @return a list of all the eventual failure causes. If no future failed, returns a list of null values. + */ + default List causes() { + int size = size(); + ArrayList list = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + list.add(cause(index)); + } + return list; + } +} diff --git a/src/main/java/org/xbib/event/async/Context.java b/src/main/java/org/xbib/event/async/Context.java new file mode 100644 index 0000000..0ab44cf --- /dev/null +++ b/src/main/java/org/xbib/event/async/Context.java @@ -0,0 +1,212 @@ +package org.xbib.event.async; + +import org.xbib.event.async.impl.AsyncThread; +import org.xbib.event.loop.EventLoop; + +/** + * The execution context of a {@link Handler} execution. + *

+ * When we provide an event to a handler, the execution is associated with a {@code Context}. + *

+ * Usually a context is an *event-loop context* and is tied to a specific event loop thread. So executions for that + * context always occur on that exact same event loop thread. + *

+ * In the case of running inline blocking code a worker context will be associated with the execution + * which will use a thread from the worker thread pool. + *

+ * When a handler is set by a thread associated with a specific context, we will guarantee that when that handler + * is executed, that execution will be associated with the same context. + *

+ * If a handler is set by a thread not associated with a context. Then a new context will + * be created for that handler. + *

+ * In other words, a context is propagated. + *

+ * This class also allows arbitrary data to be {@link #put} and {@link #get} on the context so it can be shared easily + * amongst different handlers. + *

+ * This class also provides {@link #runOnContext} which allows an action to be executed asynchronously using the same context. + * + */ +public interface Context { + + /** + * Is the current thread a worker thread? + *

+ * NOTE! This is not always the same as calling {@link Context#isWorkerContext}. If you are running blocking code + * from an event loop context, then this will return true but {@link Context#isWorkerContext} will return false. + * + * @return true if current thread is a worker thread, false otherwise + */ + static boolean isOnWorkerThread() { + Thread t = Thread.currentThread(); + return t instanceof AsyncThread && ((AsyncThread) t).isWorker(); + } + + /** + * Is the current thread an event thread? + *

+ * NOTE! This is not always the same as calling {@link Context#isEventLoopContext}. If you are running blocking code + * from an event loop context, then this will return false but {@link Context#isEventLoopContext} will return true. + * + * @return true if current thread is an event thread, false otherwise + */ + static boolean isOnEventLoopThread() { + Thread t = Thread.currentThread(); + return t instanceof AsyncThread && !((AsyncThread) t).isWorker(); + } + + /** + * Is the current thread an async thread? That's either a worker thread or an event loop thread + * + * @return true if current thread is an async thread, false otherwise + */ + static boolean isAsyncThread() { + return Thread.currentThread() instanceof AsyncThread; + } + + /** + * Run the specified action asynchronously on the same context, some time after the current execution has completed. + * + * @param action the action to run + */ + void runOnContext(Handler action); + + /** + * Safely execute some blocking code. + *

+ * Executes the blocking code in the handler {@code blockingCodeHandler} using a thread from the worker pool. + *

+ * When the code is complete the handler {@code resultHandler} will be called with the result on the original context + * (e.g. on the original event loop of the caller). + *

+ * A {@code Future} instance is passed into {@code blockingCodeHandler}. When the blocking code successfully completes, + * the handler should call the {@link Promise#complete} or {@link Promise#complete(Object)} method, or the {@link Promise#fail} + * method if it failed. + *

+ * The blocking code should block for a reasonable amount of time (i.e no more than a few seconds). Long blocking operations + * or polling operations (i.e a thread that spin in a loop polling events in a blocking fashion) are precluded. + *

+ * When the blocking operation lasts more than the 10 seconds, a message will be printed on the console by the + * blocked thread checker. + *

+ * Long blocking operations should use a dedicated thread managed by the application, which can interact with + * subscribers using the event-bus or {@link Context#runOnContext(Handler)} + * + * @param blockingCodeHandler handler representing the blocking code to run + * @param ordered if true then if executeBlocking is called several times on the same context, the executions + * for that context will be executed serially, not in parallel. if false then they will be no ordering + * guarantees + * @param the type of the result + * @return a future completed when the blocking code is complete + */ + Future executeBlocking(Handler> blockingCodeHandler, boolean ordered); + + /** + * Invoke {@link #executeBlocking(Handler, boolean)} with order = true. + * @param blockingCodeHandler handler representing the blocking code to run + * @param the type of the result + * @return a future completed when the blocking code is complete + */ + default Future executeBlocking(Handler> blockingCodeHandler) { + return executeBlocking(blockingCodeHandler, true); + } + + /** + * Is the current context an event loop context? + *

+ * NOTE! when running blocking code using {@link Async#executeBlocking(Handler, boolean)}, + * the context will still an event loop context and this {@link this#isEventLoopContext()} + * will return true. + * + * @return true if false otherwise + */ + boolean isEventLoopContext(); + + /** + * Is the current context a worker context? + *

+ * NOTE! when running blocking code using {@link Async#executeBlocking(Handler, boolean)}, + * the context will still an event loop context and this {@link this#isWorkerContext()} + * will return false. + * + * @return true if the current context is a worker context, false otherwise + */ + boolean isWorkerContext(); + + /** + * Get some data from the context. + * + * @param key the key of the data + * @param the type of the data + * @return the data + */ + T get(Object key); + + /** + * Put some data in the context. + *

+ * This can be used to share data between different handlers that share a context + * + * @param key the key of the data + * @param value the data + */ + void put(Object key, Object value); + + /** + * Remove some data from the context. + * + * @param key the key to remove + * @return true if removed successfully, false otherwise + */ + boolean remove(Object key); + + /** + * Get some local data from the context. + * + * @param key the key of the data + * @param the type of the data + * @return the data + */ + T getLocal(Object key); + + /** + * Put some local data in the context. + *

+ * This can be used to share data between different handlers that share a context + * + * @param key the key of the data + * @param value the data + */ + void putLocal(Object key, Object value); + + /** + * Remove some local data from the context. + * + * @param key the key to remove + * @return true if removed successfully, false otherwise + */ + boolean removeLocal(Object key); + + /** + * @return The Async instance that created the context + */ + Async owner(); + + /** + * Set an exception handler called when the context runs an action throwing an uncaught throwable.

+ * + * When this handler is called, {@link Async#currentContext()} will return this context. + * + * @param handler the exception handler + * @return a reference to this, so the API can be used fluently + */ + Context exceptionHandler(Handler handler); + + /** + * @return the current exception handler of this context + */ + Handler exceptionHandler(); + + EventLoop eventLoop(); +} diff --git a/src/main/java/org/xbib/event/async/EventException.java b/src/main/java/org/xbib/event/async/EventException.java new file mode 100644 index 0000000..67ae0d3 --- /dev/null +++ b/src/main/java/org/xbib/event/async/EventException.java @@ -0,0 +1,67 @@ +package org.xbib.event.async; + +/** + * This is a general purpose exception class that is often thrown if things go wrong. + * + */ +public class EventException extends RuntimeException { + + /** + * Create an instance given a message + * + * @param message the message + */ + public EventException(String message) { + super(message); + } + + /** + * Create an instance given a message and a cause + * + * @param message the message + * @param cause the cause + */ + public EventException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Create an instance given a cause + * + * @param cause the cause + */ + public EventException(Throwable cause) { + super(cause); + } + + /** + * Create an instance given a message + * + * @param message the message + * @param noStackTrace disable stack trace capture + */ + public EventException(String message, boolean noStackTrace) { + super(message, null, !noStackTrace, !noStackTrace); + } + + /** + * Create an instance given a message + * + * @param message the message + * @param cause the cause + * @param noStackTrace disable stack trace capture + */ + public EventException(String message, Throwable cause, boolean noStackTrace) { + super(message, cause, !noStackTrace, !noStackTrace); + } + + /** + * Create an instance given a message + * + * @param cause the cause + * @param noStackTrace disable stack trace capture + */ + public EventException(Throwable cause, boolean noStackTrace) { + super(null, cause, !noStackTrace, !noStackTrace); + } +} diff --git a/src/main/java/org/xbib/event/async/Future.java b/src/main/java/org/xbib/event/async/Future.java new file mode 100644 index 0000000..3d894e1 --- /dev/null +++ b/src/main/java/org/xbib/event/async/Future.java @@ -0,0 +1,414 @@ +package org.xbib.event.async; + +import org.xbib.event.async.impl.ContextInternal; +import org.xbib.event.async.impl.future.FailedFuture; +import org.xbib.event.async.impl.future.SucceededFuture; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +/** + * Represents the result of an action that may, or may not, have occurred yet. + */ +public interface Future extends AsyncResult { + + /** + * Create a future that hasn't completed yet and that is passed to the {@code handler} before it is returned. + * + * @param handler the handler + * @param the result type + * @return the future. + */ + static Future future(Handler> handler) { + Promise promise = Promise.promise(); + try { + handler.handle(promise); + } catch (Throwable e){ + promise.tryFail(e); + } + return promise.future(); + } + + /** + * Create a succeeded future with a null result + * + * @param the result type + * @return the future + */ + static Future succeededFuture() { + return (Future) SucceededFuture.EMPTY; + } + + /** + * Created a succeeded future with the specified result. + * + * @param result the result + * @param the result type + * @return the future + */ + static Future succeededFuture(T result) { + if (result == null) { + return succeededFuture(); + } else { + return new SucceededFuture<>(result); + } + } + + /** + * Create a failed future with the specified failure cause. + * + * @param t the failure cause as a Throwable + * @param the result type + * @return the future + */ + static Future failedFuture(Throwable t) { + return new FailedFuture<>(t); + } + + /** + * Create a failed future with the specified failure message. + * + * @param failureMessage the failure message + * @param the result type + * @return the future + */ + static Future failedFuture(String failureMessage) { + return new FailedFuture<>(failureMessage); + } + + /** + * Has the future completed? + *

+ * It's completed if it's either succeeded or failed. + * + * @return true if completed, false if not + */ + boolean isComplete(); + + /** + * Add a handler to be notified of the result. + *

+ * WARNING: this is a terminal operation. + * If several {@code handler}s are registered, there is no guarantee that they will be invoked in order of registration. + * + * @param handler the handler that will be called with the result + * @return a reference to this, so it can be used fluently + */ + Future onComplete(Handler> handler); + + /** + * Add a handler to be notified of the succeeded result. + *

+ * WARNING: this is a terminal operation. + * If several {@code handler}s are registered, there is no guarantee that they will be invoked in order of registration. + * + * @param handler the handler that will be called with the succeeded result + * @return a reference to this, so it can be used fluently + */ + default Future onSuccess(Handler handler) { + return onComplete(ar -> { + if (ar.succeeded()) { + handler.handle(ar.result()); + } + }); + } + + /** + * Add a handler to be notified of the failed result. + *

+ * WARNING: this is a terminal operation. + * If several {@code handler}s are registered, there is no guarantee that they will be invoked in order of registration. + * + * @param handler the handler that will be called with the failed result + * @return a reference to this, so it can be used fluently + */ + default Future onFailure(Handler handler) { + return onComplete(ar -> { + if (ar.failed()) { + handler.handle(ar.cause()); + } + }); + } + + /** + * The result of the operation. This will be null if the operation failed. + * + * @return the result or null if the operation failed. + */ + @Override + T result(); + + /** + * A Throwable describing failure. This will be null if the operation succeeded. + * + * @return the cause or null if the operation succeeded. + */ + @Override + Throwable cause(); + + /** + * Did it succeed? + * + * @return true if it succeded or false otherwise + */ + @Override + boolean succeeded(); + + /** + * Did it fail? + * + * @return true if it failed or false otherwise + */ + @Override + boolean failed(); + + /** + * Alias for {@link #compose(Function)}. + */ + default Future flatMap(Function> mapper) { + return compose(mapper); + } + + /** + * Compose this future with a {@code mapper} function.

+ * + * When this future (the one on which {@code compose} is called) succeeds, the {@code mapper} will be called with + * the completed value and this mapper returns another future object. This returned future completion will complete + * the future returned by this method call.

+ * + * If the {@code mapper} throws an exception, the returned future will be failed with this exception.

+ * + * When this future fails, the failure will be propagated to the returned future and the {@code mapper} + * will not be called. + * + * @param mapper the mapper function + * @return the composed future + */ + default Future compose(Function> mapper) { + return compose(mapper, Future::failedFuture); + } + + /** + * Handles a failure of this Future by returning the result of another Future. + * If the mapper fails, then the returned future will be failed with this failure. + * + * @param mapper A function which takes the exception of a failure and returns a new future. + * @return A recovered future + */ + default Future recover(Function> mapper) { + return compose(Future::succeededFuture, mapper); + } + + /** + * Compose this future with a {@code successMapper} and {@code failureMapper} functions.

+ * + * When this future (the one on which {@code compose} is called) succeeds, the {@code successMapper} will be called with + * the completed value and this mapper returns another future object. This returned future completion will complete + * the future returned by this method call.

+ * + * When this future (the one on which {@code compose} is called) fails, the {@code failureMapper} will be called with + * the failure and this mapper returns another future object. This returned future completion will complete + * the future returned by this method call.

+ * + * If any mapper function throws an exception, the returned future will be failed with this exception.

+ * + * @param successMapper the function mapping the success + * @param failureMapper the function mapping the failure + * @return the composed future + */ + Future compose(Function> successMapper, Function> failureMapper); + + /** + * Transform this future with a {@code mapper} functions.

+ * + * When this future (the one on which {@code transform} is called) completes, the {@code mapper} will be called with + * the async result and this mapper returns another future object. This returned future completion will complete + * the future returned by this method call.

+ * + * If any mapper function throws an exception, the returned future will be failed with this exception.

+ * + * @param mapper the function mapping the future + * @return the transformed future + */ + Future transform(Function, Future> mapper); + + /** + * Compose this future with a {@code mapper} that will be always be called. + * + *

When this future (the one on which {@code eventually} is called) completes, the {@code mapper} will be called + * and this mapper returns another future object. This returned future completion will complete the future returned + * by this method call with the original result of the future. + * + *

The outcome of the future returned by the {@code mapper} will not influence the nature + * of the returned future. + * + * @param mapper the function returning the future. + * @return the composed future + */ + Future eventually(Function> mapper); + + /** + * Apply a {@code mapper} function on this future.

+ * + * When this future succeeds, the {@code mapper} will be called with the completed value and this mapper + * returns a value. This value will complete the future returned by this method call.

+ * + * If the {@code mapper} throws an exception, the returned future will be failed with this exception.

+ * + * When this future fails, the failure will be propagated to the returned future and the {@code mapper} + * will not be called. + * + * @param mapper the mapper function + * @return the mapped future + */ + Future map(Function mapper); + + /** + * Map the result of a future to a specific {@code value}.

+ * + * When this future succeeds, this {@code value} will complete the future returned by this method call.

+ * + * When this future fails, the failure will be propagated to the returned future. + * + * @param value the value that eventually completes the mapped future + * @return the mapped future + */ + Future map(V value); + + /** + * Map the result of a future to {@code null}.

+ * + * This is a conveniency for {@code future.map((T) null)} or {@code future.map((Void) null)}.

+ * + * When this future succeeds, {@code null} will complete the future returned by this method call.

+ * + * When this future fails, the failure will be propagated to the returned future. + * + * @return the mapped future + */ + @Override + default Future mapEmpty() { + return (Future) AsyncResult.super.mapEmpty(); + } + + /** + * Apply a {@code mapper} function on this future.

+ * + * When this future fails, the {@code mapper} will be called with the completed value and this mapper + * returns a value. This value will complete the future returned by this method call.

+ * + * If the {@code mapper} throws an exception, the returned future will be failed with this exception.

+ * + * When this future succeeds, the result will be propagated to the returned future and the {@code mapper} + * will not be called. + * + * @param mapper the mapper function + * @return the mapped future + */ + Future otherwise(Function mapper); + + /** + * Map the failure of a future to a specific {@code value}.

+ * + * When this future fails, this {@code value} will complete the future returned by this method call.

+ * + * When this future succeeds, the result will be propagated to the returned future. + * + * @param value the value that eventually completes the mapped future + * @return the mapped future + */ + Future otherwise(T value); + + /** + * Map the failure of a future to {@code null}.

+ * + * This is a convenience for {@code future.otherwise((T) null)}.

+ * + * When this future fails, the {@code null} value will complete the future returned by this method call.

+ * + * When this future succeeds, the result will be propagated to the returned future. + * + * @return the mapped future + */ + default Future otherwiseEmpty() { + return (Future) AsyncResult.super.otherwiseEmpty(); + } + + /** + * Invokes the given {@code handler} upon completion. + *

+ * If the {@code handler} throws an exception, the returned future will be failed with this exception. + * + * @param handler invoked upon completion of this future + * @return a future completed after the {@code handler} has been invoked + */ + default Future andThen(Handler> handler) { + return transform(ar -> { + handler.handle(ar); + return (Future) ar; + }); + } + + /** + * Bridges this Vert.x future to a {@link CompletionStage} instance. + *

+ * The {@link CompletionStage} handling methods will be called from the thread that resolves this future. + * + * @return a {@link CompletionStage} that completes when this future resolves + */ + default CompletionStage toCompletionStage() { + CompletableFuture completableFuture = new CompletableFuture<>(); + onComplete(ar -> { + if (ar.succeeded()) { + completableFuture.complete(ar.result()); + } else { + completableFuture.completeExceptionally(ar.cause()); + } + }); + return completableFuture; + } + + /** + * Bridges a {@link CompletionStage} object to a Vert.x future instance. + *

+ * The Vert.x future handling methods will be called from the thread that completes {@code completionStage}. + * + * @param completionStage a completion stage + * @param the result type + * @return a Vert.x future that resolves when {@code completionStage} resolves + */ + static Future fromCompletionStage(CompletionStage completionStage) { + Promise promise = Promise.promise(); + completionStage.whenComplete((value, err) -> { + if (err != null) { + promise.fail(err); + } else { + promise.complete(value); + } + }); + return promise.future(); + } + + /** + * Bridges a {@link CompletionStage} object to a Vert.x future instance. + *

+ * The Vert.x future handling methods will be called on the provided {@code context}. + * + * @param completionStage a completion stage + * @param context a Vert.x context to dispatch to + * @param the result type + * @return a Vert.x future that resolves when {@code completionStage} resolves + */ + static Future fromCompletionStage(CompletionStage completionStage, Context context) { + Promise promise = ((ContextInternal) context).promise(); + completionStage.whenComplete((value, err) -> { + if (err != null) { + promise.fail(err); + } else { + promise.complete(value); + } + }); + return promise.future(); + } +} diff --git a/src/main/java/org/xbib/event/async/Handler.java b/src/main/java/org/xbib/event/async/Handler.java new file mode 100644 index 0000000..90aee93 --- /dev/null +++ b/src/main/java/org/xbib/event/async/Handler.java @@ -0,0 +1,18 @@ +package org.xbib.event.async; + +/** + * A generic event handler. + *

+ * This interface is used heavily throughout Vert.x as a handler for all types of asynchronous occurrences. + *

+ */ +@FunctionalInterface +public interface Handler { + + /** + * Something has happened, so handle it. + * + * @param event the event to handle + */ + void handle(E event); +} diff --git a/src/main/java/org/xbib/event/async/NoStackTraceThrowable.java b/src/main/java/org/xbib/event/async/NoStackTraceThrowable.java new file mode 100644 index 0000000..210d43f --- /dev/null +++ b/src/main/java/org/xbib/event/async/NoStackTraceThrowable.java @@ -0,0 +1,8 @@ +package org.xbib.event.async; + +public class NoStackTraceThrowable extends Throwable { + + public NoStackTraceThrowable(String message) { + super(message, null, false, false); + } +} diff --git a/src/main/java/org/xbib/event/async/Promise.java b/src/main/java/org/xbib/event/async/Promise.java new file mode 100644 index 0000000..1bc5184 --- /dev/null +++ b/src/main/java/org/xbib/event/async/Promise.java @@ -0,0 +1,130 @@ +package org.xbib.event.async; + +import org.xbib.event.async.impl.future.PromiseImpl; + +/** + * Represents the writable side of an action that may, or may not, have occurred yet. + *

+ * The {@link #future()} method returns the {@link Future} associated with a promise, the future + * can be used for getting notified of the promise completion and retrieve its value. + *

+ * A promise extends {@code Handler>} so it can be used as a callback. + */ +public interface Promise extends Handler> { + + /** + * Create a promise that hasn't completed yet + * + * @param the result type + * @return the promise + */ + static Promise promise() { + return new PromiseImpl<>(); + } + + /** + * Succeed or fail this promise with the {@link AsyncResult} event. + * + * @param asyncResult the async result to handle + */ + @Override + default void handle(AsyncResult asyncResult) { + if (asyncResult.succeeded()) { + complete(asyncResult.result()); + } else { + fail(asyncResult.cause()); + } + } + + /** + * Set the result. Any handler will be called, if there is one, and the promise will be marked as completed. + *

+ * Any handler set on the associated promise will be called. + * + * @param result the result + * @throws IllegalStateException when the promise is already completed + */ + default void complete(T result) { + if (!tryComplete(result)) { + throw new IllegalStateException("Result is already complete"); + } + } + + /** + * Calls {@code complete(null)} + * + * @throws IllegalStateException when the promise is already completed + */ + default void complete() { + if (!tryComplete()) { + throw new IllegalStateException("Result is already complete"); + } + } + + /** + * Set the failure. Any handler will be called, if there is one, and the future will be marked as completed. + * + * @param cause the failure cause + * @throws IllegalStateException when the promise is already completed + */ + default void fail(Throwable cause) { + if (!tryFail(cause)) { + throw new IllegalStateException("Result is already complete"); + } + } + + /** + * Calls {@link #fail(Throwable)} with the {@code message}. + * + * @param message the failure message + * @throws IllegalStateException when the promise is already completed + */ + default void fail(String message) { + if (!tryFail(message)) { + throw new IllegalStateException("Result is already complete"); + } + } + + /** + * Like {@link #complete(Object)} but returns {@code false} when the promise is already completed instead of throwing + * an {@link IllegalStateException}, it returns {@code true} otherwise. + * + * @param result the result + * @return {@code false} when the future is already completed + */ + boolean tryComplete(T result); + + /** + * Calls {@code tryComplete(null)}. + * + * @return {@code false} when the future is already completed + */ + default boolean tryComplete() { + return tryComplete(null); + } + + /** + * Like {@link #fail(Throwable)} but returns {@code false} when the promise is already completed instead of throwing + * an {@link IllegalStateException}, it returns {@code true} otherwise. + * + * @param cause the failure cause + * @return {@code false} when the future is already completed + */ + boolean tryFail(Throwable cause); + + /** + * Calls {@link #fail(Throwable)} with the {@code message}. + * + * @param message the failure message + * @return false when the future is already completed + */ + default boolean tryFail(String message) { + return tryFail(new NoStackTraceThrowable(message)); + } + + /** + * @return the {@link Future} associated with this promise, it can be used to be aware of the promise completion + */ + Future future(); + +} diff --git a/src/main/java/org/xbib/event/async/TimeoutStream.java b/src/main/java/org/xbib/event/async/TimeoutStream.java new file mode 100644 index 0000000..38d6f00 --- /dev/null +++ b/src/main/java/org/xbib/event/async/TimeoutStream.java @@ -0,0 +1,40 @@ +package org.xbib.event.async; + +import org.xbib.event.async.streams.ReadStream; + +/** + * A timeout stream is triggered by a timer, the {@link Handler} will be call when the timer is fired, + * it can be once or several times depending on the nature of the timer related to this stream. The + * {@link ReadStream#endHandler(Handler)} will be called after the timer handler has been called. + *

+ * Pausing the timer inhibits the timer shots until the stream is resumed. Setting a null handler callback cancels + * the timer. + * + */ +public interface TimeoutStream extends ReadStream { + + @Override + TimeoutStream exceptionHandler(Handler handler); + + @Override + TimeoutStream handler(Handler handler); + + @Override + TimeoutStream pause(); + + @Override + TimeoutStream resume(); + + @Override + TimeoutStream fetch(long amount); + + @Override + TimeoutStream endHandler(Handler endHandler); + + /** + * Cancels the timeout. Note this has the same effect as calling {@link #handler(Handler)} with a null + * argument. + */ + void cancel(); + +} diff --git a/src/main/java/org/xbib/event/async/WorkerExecutor.java b/src/main/java/org/xbib/event/async/WorkerExecutor.java new file mode 100644 index 0000000..b04a45a --- /dev/null +++ b/src/main/java/org/xbib/event/async/WorkerExecutor.java @@ -0,0 +1,47 @@ +package org.xbib.event.async; + +/** + * An executor for executing blocking code.

+ * + * It provides the same executeBlocking operation than {@link Context} and + * {@link Async} but on a separate worker pool.

+ */ +public interface WorkerExecutor { + + /** + * Safely execute some blocking code. + *

+ * Executes the blocking code in the handler {@code blockingCodeHandler} using a thread from the worker pool. + *

+ * When the code is complete the handler {@code resultHandler} will be called with the result on the original context + * (i.e. on the original event loop of the caller). + *

+ * A {@code Future} instance is passed into {@code blockingCodeHandler}. When the blocking code successfully completes, + * the handler should call the {@link Promise#complete} or {@link Promise#complete(Object)} method, or the {@link Promise#fail} + * method if it failed. + *

+ * In the {@code blockingCodeHandler} the current context remains the original context and therefore any task + * scheduled in the {@code blockingCodeHandler} will be executed on the this context and not on the worker thread. + * + * @param blockingCodeHandler handler representing the blocking code to run + * @param ordered if true then if executeBlocking is called several times on the same context, the executions + * for that context will be executed serially, not in parallel. if false then they will be no ordering + * guarantees + * @param the type of the result + * @return a future notified with the result + */ + Future executeBlocking(Handler> blockingCodeHandler, boolean ordered); + + /** + * Like {@link #executeBlocking(Handler, boolean)} called with ordered = true. + */ + default Future executeBlocking(Handler> blockingCodeHandler) { + return executeBlocking(blockingCodeHandler, true); + } + + /** + * Close the executor. + */ + Future close(); + +} diff --git a/src/main/java/org/xbib/event/async/impl/AsyncBuilder.java b/src/main/java/org/xbib/event/async/impl/AsyncBuilder.java new file mode 100644 index 0000000..ef81ec5 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/AsyncBuilder.java @@ -0,0 +1,110 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Async; +import org.xbib.event.async.AsyncOptions; +import org.xbib.event.async.spi.AsyncThreadFactory; +import org.xbib.event.async.spi.ExecutorServiceFactory; +import org.xbib.event.loop.EventLoopGroup; + +/** + * Async builder for creating instances with SPI overrides. + */ +public class AsyncBuilder { + + private final AsyncOptions options; + private AsyncThreadFactory threadFactory; + private ExecutorServiceFactory executorServiceFactory; + + private EventLoopGroup eventLoopGroup; + + public AsyncBuilder() { + this(new AsyncOptions()); + } + + public AsyncBuilder(AsyncOptions options) { + this.options = options; + } + + /** + * @return the options + */ + public AsyncOptions options() { + return options; + } + + /** + * @return the {@code AsyncThreadFactory} to use + */ + public AsyncThreadFactory threadFactory() { + return threadFactory; + } + + /** + * Set the {@code AsyncThreadFactory} instance to use. + * @param factory the metrics + * @return this builder instance + */ + public AsyncBuilder threadFactory(AsyncThreadFactory factory) { + this.threadFactory = factory; + return this; + } + + /** + * @return the {@code ExecutorServiceFactory} to use + */ + public ExecutorServiceFactory executorServiceFactory() { + return executorServiceFactory; + } + + /** + * Set the {@code ExecutorServiceFactory} instance to use. + * @param factory the factory + * @return this builder instance + */ + public AsyncBuilder executorServiceFactory(ExecutorServiceFactory factory) { + this.executorServiceFactory = factory; + return this; + } + + public EventLoopGroup eventLoopGroup() { + return eventLoopGroup; + } + + public AsyncBuilder eventLoopGroup(EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + return this; + } + + /** + * Build and return the instance + */ + public Async newInstance() { + AsyncImpl async = new AsyncImpl(options, threadFactory, executorServiceFactory, eventLoopGroup); + async.init(); + return async; + } + + /** + * Initialize the service providers. + * @return this builder instance + */ + public AsyncBuilder init() { + initThreadFactory(); + initExecutorServiceFactory(); + return this; + } + + private void initThreadFactory() { + if (threadFactory != null) { + return; + } + threadFactory = AsyncThreadFactory.INSTANCE; + } + + private void initExecutorServiceFactory() { + if (executorServiceFactory != null) { + return; + } + executorServiceFactory = ExecutorServiceFactory.INSTANCE; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/AsyncImpl.java b/src/main/java/org/xbib/event/async/impl/AsyncImpl.java new file mode 100644 index 0000000..2eefc54 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/AsyncImpl.java @@ -0,0 +1,558 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Async; +import org.xbib.event.async.AsyncOptions; +import org.xbib.event.async.Closeable; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.Promise; +import org.xbib.event.async.TimeoutStream; +import org.xbib.event.async.impl.future.PromiseInternal; +import org.xbib.event.async.spi.AsyncThreadFactory; +import org.xbib.event.async.spi.ExecutorServiceFactory; +import org.xbib.event.loop.EventLoop; +import org.xbib.event.loop.EventLoopGroup; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AsyncImpl implements AsyncInternal { + + /** + * Context dispatch info for context running with external threads. + */ + static final ThreadLocal nonContextDispatch = new ThreadLocal<>(); + + private static final Logger log = Logger.getLogger(AsyncImpl.class.getName()); + + private final ConcurrentMap timeouts = new ConcurrentHashMap<>(); + private final AtomicLong timeoutCounter = new AtomicLong(0); + final WorkerPool workerPool; + final WorkerPool internalWorkerPool; + + private final EventLoopGroup eventLoopGroup; + private boolean closed; + private volatile Handler exceptionHandler; + private final CloseFuture closeFuture; + private final ThreadLocal> stickyContext = new ThreadLocal<>(); + private final boolean disableTCCL; + + AsyncImpl(AsyncOptions options, + AsyncThreadFactory threadFactory, + ExecutorServiceFactory executorServiceFactory, + EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + if (Async.currentContext() != null) { + log.log(Level.WARNING, "You're already on a context, are you sure you want to create a new instance?"); + } + + Boolean useDaemonThread = options.getUseDaemonThread(); + int workerPoolSize = options.getWorkerPoolSize(); + int internalBlockingPoolSize = options.getInternalBlockingPoolSize(); + long maxEventLoopExecuteTime = options.getMaxEventLoopExecuteTime(); + TimeUnit maxEventLoopExecuteTimeUnit = options.getMaxEventLoopExecuteTimeUnit(); + TimeUnit maxWorkerExecuteTimeUnit = options.getMaxWorkerExecuteTimeUnit(); + long maxWorkerExecuteTime = options.getMaxWorkerExecuteTime(); + + ThreadFactory workerThreadFactory = createThreadFactory(threadFactory, useDaemonThread, maxWorkerExecuteTime, maxWorkerExecuteTimeUnit, "async-worker-thread-", true); + ExecutorService workerExec = executorServiceFactory.createExecutor(workerThreadFactory, workerPoolSize, workerPoolSize); + ThreadFactory internalWorkerThreadFactory = createThreadFactory(threadFactory, useDaemonThread, maxWorkerExecuteTime, maxWorkerExecuteTimeUnit, "async-internal-blocking-", true); + ExecutorService internalWorkerExec = executorServiceFactory.createExecutor(internalWorkerThreadFactory, internalBlockingPoolSize, internalBlockingPoolSize); + + closeFuture = new CloseFuture(log); + + internalWorkerPool = new WorkerPool(internalWorkerExec); + workerPool = new WorkerPool(workerExec); + int defaultWorkerPoolSize = options.getWorkerPoolSize(); + disableTCCL = options.getDisableTCCL(); + } + + void init() { + } + + @Override + public TimeoutStream periodicStream(long initialDelay, long delay) { + return new TimeoutStreamImpl(initialDelay, delay, true); + } + + @Override + public long setTimer(long delay, Handler handler) { + ContextInternal ctx = getOrCreateContext(); + return scheduleTimeout(ctx, false, delay, TimeUnit.MILLISECONDS, false, handler); + } + + @Override + public TimeoutStream timerStream(long delay) { + return new TimeoutStreamImpl(delay, false); + } + + @Override + public long setPeriodic(long initialDelay, long delay, Handler handler) { + ContextInternal ctx = getOrCreateContext(); + return scheduleTimeout(ctx, true, initialDelay, delay, TimeUnit.MILLISECONDS, false, handler); + } + + @Override + public PromiseInternal promise() { + ContextInternal context = getOrCreateContext(); + return context.promise(); + } + + public PromiseInternal promise(Promise p) { + if (p instanceof PromiseInternal) { + PromiseInternal promise = (PromiseInternal) p; + if (promise.context() != null) { + return promise; + } + } + PromiseInternal promise = promise(); + promise.future().onComplete(p); + return promise; + } + + public void runOnContext(Handler task) { + ContextInternal context = getOrCreateContext(); + context.runOnContext(task); + } + + // The background pool is used for making blocking calls to legacy synchronous APIs + public WorkerPool getWorkerPool() { + return workerPool; + } + + @Override + public WorkerPool getInternalWorkerPool() { + return internalWorkerPool; + } + + + @Override + public ContextInternal getOrCreateContext() { + ContextInternal ctx = getContext(); + if (ctx == null) { + ctx = createEventLoopContext(); + stickyContext.set(new WeakReference<>(ctx)); + } + return ctx; + } + + @Override + public boolean cancelTimer(long id) { + InternalTimerHandler handler = timeouts.get(id); + if (handler != null) { + return handler.cancel(); + } else { + return false; + } + } + + private EventLoopContext createEventLoopContext() { + return createEventLoopContext(closeFuture, null, Thread.currentThread().getContextClassLoader()); + } + + private EventLoopContext createEventLoopContext(CloseFuture closeFuture, WorkerPool workerPool, ClassLoader tccl) { + return new EventLoopContext(this, eventLoopGroup.next(), internalWorkerPool, workerPool != null ? workerPool : this.workerPool, closeFuture, disableTCCL ? null : tccl); + } + + private long scheduleTimeout(ContextInternal context, + boolean periodic, + long initialDelay, + long delay, + TimeUnit timeUnit, + boolean addCloseHook, + Handler handler) { + if (delay < 1) { + throw new IllegalArgumentException("Cannot schedule a timer with delay < 1 ms"); + } + if (initialDelay < 0) { + throw new IllegalArgumentException("Cannot schedule a timer with initialDelay < 0"); + } + long timerId = timeoutCounter.getAndIncrement(); + InternalTimerHandler task = new InternalTimerHandler(timerId, handler, periodic, context); + timeouts.put(timerId, task); + if (addCloseHook) { + context.addCloseHook(task); + } + EventLoop el = context.eventLoop(); + if (periodic) { + task.future = el.scheduleAtFixedRate(task, initialDelay, delay, timeUnit); + } else { + task.future = el.schedule(task, delay, timeUnit); + } + return task.id; + } + + public long scheduleTimeout(ContextInternal context, + boolean periodic, + long delay, + TimeUnit timeUnit, + boolean addCloseHook, + Handler handler) { + return scheduleTimeout(context, periodic, delay, delay, timeUnit, addCloseHook, handler); + } + + public ContextInternal getContext() { + ContextInternal context = ContextInternal.current(); + if (context != null && context.owner() == this) { + return context; + } else { + WeakReference ref = stickyContext.get(); + return ref != null ? ref.get() : null; + } + } + + @Override + public synchronized Future close() { + // Create this promise purposely without a context because the close operation will close thread pools + if (closed) { + // Just call the handler directly since pools shutdown + return Future.succeededFuture(); + } + closed = true; + Future fut = closeFuture + .close(); + Future val = fut; + Promise p = Promise.promise(); + val.onComplete(ar -> { + if (ar.succeeded()) { + p.complete(); + } else { + p.fail(ar.cause()); + } + }); + return p.future(); + } + + /** + * Timers are stored in the {@link #timeouts} map at creation time. + *

+ * Timers are removed from the {@link #timeouts} map when they are cancelled or are fired. The thread + * removing the timer successfully owns the timer termination (i.e cancel or timer) to avoid race conditions + * between timeout and cancellation. + *

+ * This class does not rely on the internal {@link #future} for the termination to handle the worker case + * since the actual timer {@link #handler} execution is scheduled when the {@link #future} executes. + */ + class InternalTimerHandler implements Handler, Closeable, Runnable { + + private final Handler handler; + private final boolean periodic; + private final long id; + private final ContextInternal context; + private final AtomicBoolean disposed = new AtomicBoolean(); + private volatile java.util.concurrent.Future future; + + InternalTimerHandler(long id, Handler runnable, boolean periodic, ContextInternal context) { + this.context = context; + this.id = id; + this.handler = runnable; + this.periodic = periodic; + } + + @Override + public void run() { + context.emit(this); + } + + public void handle(Void v) { + if (periodic) { + if (!disposed.get()) { + handler.handle(id); + } + } else if (disposed.compareAndSet(false, true)) { + timeouts.remove(id); + try { + handler.handle(id); + } finally { + // Clean up after it's fired + context.removeCloseHook(this); + } + } + } + + private boolean cancel() { + boolean cancelled = tryCancel(); + if (cancelled) { + context.removeCloseHook(this); + } + return cancelled; + } + + private boolean tryCancel() { + if (disposed.compareAndSet(false, true)) { + timeouts.remove(id); + future.cancel(false); + return true; + } else { + return false; + } + } + + // Called via Context close hook when Verticle is undeployed + public void close(Promise completion) { + tryCancel(); + completion.complete(); + } + } + + private static ThreadFactory createThreadFactory(AsyncThreadFactory threadFactory, Boolean useDaemonThread, long maxExecuteTime, TimeUnit maxExecuteTimeUnit, String prefix, boolean worker) { + AtomicInteger threadCount = new AtomicInteger(0); + return runnable -> { + AsyncThread thread = threadFactory.newVertxThread(runnable, prefix + threadCount.getAndIncrement(), worker, maxExecuteTime, maxExecuteTimeUnit); + if (useDaemonThread != null && thread.isDaemon() != useDaemonThread) { + thread.setDaemon(useDaemonThread); + } + return thread; + }; + } + + @Override + public Async exceptionHandler(Handler handler) { + exceptionHandler = handler; + return this; + } + + @Override + public Handler exceptionHandler() { + return exceptionHandler; + } + + @Override + public CloseFuture closeFuture() { + return closeFuture; + } + + private CloseFuture resolveCloseFuture() { + ContextInternal context = getContext(); + return context != null ? context.closeFuture() : closeFuture; + } + + /** + * Execute the {@code task} disabling the thread-local association for the duration + * of the execution. {@link Async#currentContext()} will return {@code null}, + * @param task the task to execute + * @throws IllegalStateException if the current thread is not a Async thread + */ + void executeIsolated(Handler task) { + if (Thread.currentThread() instanceof AsyncThread) { + ContextInternal prev = beginDispatch(null); + try { + task.handle(null); + } finally { + endDispatch(prev); + } + } else { + task.handle(null); + } + } + + static class ContextDispatch { + ContextInternal context; + ClassLoader topLevelTCCL; + } + + /** + * Begin the emission of a context event. + *

+ * This is a low level interface that should not be used, instead {@link ContextInternal#dispatch(Object, Handler)} + * shall be used. + * + * @param context the context on which the event is emitted on + * @return the current context that shall be restored + */ + ContextInternal beginDispatch(ContextInternal context) { + Thread thread = Thread.currentThread(); + ContextInternal prev; + if (thread instanceof AsyncThread) { + AsyncThread asyncThread = (AsyncThread) thread; + prev = asyncThread.context; + asyncThread.executeStart(); + asyncThread.context = context; + if (!disableTCCL) { + if (prev == null) { + asyncThread.topLevelTCCL = Thread.currentThread().getContextClassLoader(); + } + if (context != null) { + thread.setContextClassLoader(context.classLoader()); + } + } + } else { + prev = beginDispatch2(thread, context); + } + return prev; + } + + private ContextInternal beginDispatch2(Thread thread, ContextInternal context) { + ContextDispatch current = nonContextDispatch.get(); + ContextInternal prev; + if (current != null) { + prev = current.context; + } else { + current = new ContextDispatch(); + nonContextDispatch.set(current); + prev = null; + } + current.context = context; + if (!disableTCCL) { + if (prev == null) { + current.topLevelTCCL = Thread.currentThread().getContextClassLoader(); + } + thread.setContextClassLoader(context.classLoader()); + } + return prev; + } + + /** + * End the emission of a context task. + *

+ * This is a low level interface that should not be used, instead {@link ContextInternal#dispatch(Object, Handler)} + * shall be used. + * + * @param prev the previous context thread to restore, might be {@code null} + */ + void endDispatch(ContextInternal prev) { + Thread thread = Thread.currentThread(); + if (thread instanceof AsyncThread) { + AsyncThread asyncThread = (AsyncThread) thread; + asyncThread.context = prev; + if (!disableTCCL) { + ClassLoader tccl; + if (prev == null) { + tccl = asyncThread.topLevelTCCL; + asyncThread.topLevelTCCL = null; + } else { + tccl = prev.classLoader(); + } + Thread.currentThread().setContextClassLoader(tccl); + } + asyncThread.executeEnd(); + } else { + endDispatch2(prev); + } + } + + private void endDispatch2(ContextInternal prev) { + ClassLoader tccl; + ContextDispatch current = nonContextDispatch.get(); + if (prev != null) { + current.context = prev; + tccl = prev.classLoader(); + } else { + nonContextDispatch.remove(); + tccl = current.topLevelTCCL; + } + if (!disableTCCL) { + Thread.currentThread().setContextClassLoader(tccl); + } + } + + /* + * + * This class is optimised for performance when used on the same event loop that is was passed to the handler with. + * However it can be used safely from other threads. + * + * The internal state is protected using the synchronized keyword. If always used on the same event loop, then + * we benefit from biased locking which makes the overhead of synchronized near zero. + * + */ + private class TimeoutStreamImpl implements TimeoutStream, Handler { + + private final long initialDelay; + private final long delay; + private final boolean periodic; + + private Long id; + private Handler handler; + private Handler endHandler; + private long demand; + + public TimeoutStreamImpl(long delay, boolean periodic) { + this(delay, delay, periodic); + } + + public TimeoutStreamImpl(long initialDelay, long delay, boolean periodic) { + this.initialDelay = initialDelay; + this.delay = delay; + this.periodic = periodic; + this.demand = Long.MAX_VALUE; + } + + @Override + public synchronized void handle(Long event) { + try { + if (demand > 0) { + demand--; + handler.handle(event); + } + } finally { + if (!periodic && endHandler != null) { + endHandler.handle(null); + } + } + } + + @Override + public synchronized TimeoutStream fetch(long amount) { + demand += amount; + if (demand < 0) { + demand = Long.MAX_VALUE; + } + return this; + } + + @Override + public TimeoutStream exceptionHandler(Handler handler) { + return this; + } + + @Override + public void cancel() { + if (id != null) { + AsyncImpl.this.cancelTimer(id); + } + } + + @Override + public synchronized TimeoutStream handler(Handler handler) { + if (handler != null) { + if (id != null) { + throw new IllegalStateException(); + } + ContextInternal ctx = getOrCreateContext(); + this.handler = handler; + this.id = scheduleTimeout(ctx, periodic, initialDelay, delay, TimeUnit.MILLISECONDS, false, this); + } else { + cancel(); + } + return this; + } + + @Override + public synchronized TimeoutStream pause() { + demand = 0; + return this; + } + + @Override + public synchronized TimeoutStream resume() { + demand = Long.MAX_VALUE; + return this; + } + + @Override + public synchronized TimeoutStream endHandler(Handler endHandler) { + this.endHandler = endHandler; + return this; + } + } + +} diff --git a/src/main/java/org/xbib/event/async/impl/AsyncInternal.java b/src/main/java/org/xbib/event/async/impl/AsyncInternal.java new file mode 100644 index 0000000..0a153b6 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/AsyncInternal.java @@ -0,0 +1,52 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Async; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.Promise; +import org.xbib.event.async.impl.future.PromiseInternal; + +/** + * This interface provides services for internal use only. + * It is not part of the public API and should not be used by + * developers. + */ +public interface AsyncInternal extends Async { + + /** + * @return a promise associated with the context returned by {@link #getOrCreateContext()}. + */ + PromiseInternal promise(); + + /** + * @return a promise associated with the context returned by {@link #getOrCreateContext()} or the {@code handler} + * if that handler is already an instance of {@code PromiseInternal} + */ + PromiseInternal promise(Promise promise); + + @Override + ContextInternal getOrCreateContext(); + + WorkerPool getWorkerPool(); + + WorkerPool getInternalWorkerPool(); + + /** + * Get the current context + * @return the context + */ + ContextInternal getContext(); + + default Future executeBlockingInternal(Handler> blockingCodeHandler) { + ContextInternal context = getOrCreateContext(); + return context.executeBlockingInternal(blockingCodeHandler); + } + + default Future executeBlockingInternal(Handler> blockingCodeHandler, boolean ordered) { + ContextInternal context = getOrCreateContext(); + return context.executeBlockingInternal(blockingCodeHandler, ordered); + } + + CloseFuture closeFuture(); + +} diff --git a/src/main/java/org/xbib/event/async/impl/AsyncThread.java b/src/main/java/org/xbib/event/async/impl/AsyncThread.java new file mode 100644 index 0000000..56bb4e0 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/AsyncThread.java @@ -0,0 +1,56 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.thread.FastThreadLocalThread; +import org.xbib.event.thread.ThreadInfo; + +import java.util.concurrent.TimeUnit; + +public class AsyncThread extends FastThreadLocalThread { + + private final boolean worker; + final ThreadInfo info; + ContextInternal context; + ClassLoader topLevelTCCL; + + public AsyncThread(Runnable target, String name, boolean worker, long maxExecTime, TimeUnit maxExecTimeUnit) { + super(target, name); + this.worker = worker; + this.info = new ThreadInfo(maxExecTimeUnit, maxExecTime); + } + + /** + * @return the current context of this thread, this method must be called from the current thread + */ + ContextInternal context() { + return context; + } + + void executeStart() { + if (context == null) { + info.startTime = System.nanoTime(); + } + } + + void executeEnd() { + if (context == null) { + info.startTime = 0; + } + } + + public long startTime() { + return info.startTime; + } + + public boolean isWorker() { + return worker; + } + + public long maxExecTime() { + return info.maxExecTime; + } + + public TimeUnit maxExecTimeUnit() { + return info.maxExecTimeUnit; + } + +} diff --git a/src/main/java/org/xbib/event/async/impl/CloseFuture.java b/src/main/java/org/xbib/event/async/impl/CloseFuture.java new file mode 100644 index 0000000..e403e5c --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/CloseFuture.java @@ -0,0 +1,176 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Closeable; +import org.xbib.event.async.Future; +import org.xbib.event.async.Promise; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A close future object is a state machine managing the closing sequence of a resource. A close future can be closed + * explicitly with the {@link #close()} method or when the future is unreachable in order to release the resource. + * + *

A closed future holds a set of nested {@link Closeable} that are processed when the future is closed. When a close + * future is closed, nested {@link Closeable} will be closed and the close future will notify the completion of the close + * sequence after the nested closeables are closed. + */ +public class CloseFuture extends NestedCloseable implements Closeable { + + private final Logger log; + private final Promise promise = Promise.promise(); + private boolean closed; + private Map children; + + public CloseFuture() { + this(null); + } + + public CloseFuture(Logger log) { + this.log = log; + } + + /** + * Add a {@code child} closeable, notified when this instance is closed. + * + * @param child the child closeable to add + */ + public synchronized void add(Closeable child) { + if (closed) { + throw new IllegalStateException(); + } + if (child instanceof NestedCloseable) { + NestedCloseable base = (NestedCloseable) child; + synchronized (base) { + if (base.owner != null) { + throw new IllegalStateException(); + } + base.owner = this; + } + } + if (children == null) { + children = new HashMap<>(); + } + children.put(child, this); + } + + /** + * Remove an existing {@code nested} closeable. + * + * @param nested the closeable to remove + */ + public boolean remove(Closeable nested) { + if (nested instanceof NestedCloseable) { + NestedCloseable base = (NestedCloseable) nested; + synchronized (base) { + if (base.owner == this) { + base.owner = null; + } + } + } + synchronized (this) { + if (children != null) { + return children.remove(nested) != null; + } + } + return false; + } + + /** + * @return whether the future is closed. + */ + public synchronized boolean isClosed() { + return closed; + } + + /** + * @return the future completed after completion of all close hooks. + */ + public Future future() { + return promise.future(); + } + + /** + * Run all close hooks, after completion of all hooks, the future is closed. + * + * @return the future completed after completion of all close hooks + */ + public Future close() { + synchronized (this) { + if (closed) { + return promise.future(); + } + closed = true; + } + cascadeClose(); + return promise.future(); + } + + private void cascadeClose() { + List toClose = Collections.emptyList(); + synchronized (this) { + if (children != null) { + toClose = new ArrayList<>(children.keySet()); + } + children = null; + } + // We want an immutable version of the list holding strong references to avoid racing against finalization + int num = toClose.size(); + if (num > 0) { + AtomicInteger count = new AtomicInteger(); + for (Closeable hook : toClose) { + // Clear the reference before notifying to avoid a callback to this + if (hook instanceof NestedCloseable) { + NestedCloseable base = (NestedCloseable) hook; + synchronized (base) { + base.owner = null; + } + } + Promise p = Promise.promise(); + p.future().onComplete(ar -> { + if (count.incrementAndGet() == num) { + unregisterFromOwner(); + promise.complete(); + } + }); + try { + hook.close(p); + } catch (Throwable t) { + if (log != null) { + log.log(Level.WARNING, "Failed to run close hook", t); + } + p.tryFail(t); + } + } + } else { + unregisterFromOwner(); + promise.complete(); + } + } + + private void unregisterFromOwner() { + CloseFuture owner; + synchronized (this) { + owner = super.owner; + } + if (owner != null) { + owner.remove(this); + } + } + + /** + * Run the close hooks, this method should not be called directly instead it should be called when + * this close future is added to another close future. + * + * @param promise called when all hooks have been executed + */ + public void close(Promise promise) { + close().onComplete(promise); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/ContextBase.java b/src/main/java/org/xbib/event/async/impl/ContextBase.java new file mode 100644 index 0000000..3e22bd5 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/ContextBase.java @@ -0,0 +1,189 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Context; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.Promise; +import org.xbib.event.loop.EventLoop; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +/** + * A base class for {@link Context} implementations. + * + */ +public abstract class ContextBase implements ContextInternal { + + private final AsyncInternal owner; + private final CloseFuture closeFuture; + private final ClassLoader tccl; + private final EventLoop eventLoop; + private ConcurrentMap data; + private ConcurrentMap localData; + private volatile Handler exceptionHandler; + final TaskQueue internalOrderedTasks; + final WorkerPool internalWorkerPool; + final WorkerPool workerPool; + final TaskQueue orderedTasks; + + protected ContextBase(AsyncInternal asyncInternal, + EventLoop eventLoop, + WorkerPool internalWorkerPool, + WorkerPool workerPool, + CloseFuture closeFuture, + ClassLoader tccl) { + this.eventLoop = eventLoop; + this.tccl = tccl; + this.owner = asyncInternal; + this.workerPool = workerPool; + this.closeFuture = closeFuture; + this.internalWorkerPool = internalWorkerPool; + this.orderedTasks = new TaskQueue(); + this.internalOrderedTasks = new TaskQueue(); + } + + @Override + public CloseFuture closeFuture() { + return closeFuture; + } + + public EventLoop eventLoop() { + return eventLoop; + } + + public AsyncInternal owner() { + return owner; + } + + @Override + public Future executeBlockingInternal(Handler> action) { + return executeBlocking(this, action, internalWorkerPool, internalOrderedTasks); + } + + @Override + public Future executeBlockingInternal(Handler> action, boolean ordered) { + return executeBlocking(this, action, internalWorkerPool, ordered ? internalOrderedTasks : null); + } + + @Override + public Future executeBlocking(Handler> blockingCodeHandler, boolean ordered) { + return executeBlocking(this, blockingCodeHandler, workerPool, ordered ? orderedTasks : null); + } + + @Override + public Future executeBlocking(Handler> blockingCodeHandler, TaskQueue queue) { + return executeBlocking(this, blockingCodeHandler, workerPool, queue); + } + + static Future executeBlocking(ContextInternal context, Handler> blockingCodeHandler, + WorkerPool workerPool, TaskQueue queue) { + Promise promise = context.promise(); + Future fut = promise.future(); + try { + Runnable command = () -> { + Object execMetric = null; + context.dispatch(promise, f -> { + try { + blockingCodeHandler.handle(promise); + } catch (Throwable e) { + promise.tryFail(e); + } + }); + }; + Executor exec = workerPool.executor(); + if (queue != null) { + queue.execute(command, exec); + } else { + exec.execute(command); + } + } catch (RejectedExecutionException e) { + throw e; + } + return fut; + } + + @Override + public ClassLoader classLoader() { + return tccl; + } + + @Override + public WorkerPool workerPool() { + return workerPool; + } + + @Override + public synchronized ConcurrentMap contextData() { + if (data == null) { + data = new ConcurrentHashMap<>(); + } + return data; + } + + @Override + public synchronized ConcurrentMap localContextData() { + if (localData == null) { + localData = new ConcurrentHashMap<>(); + } + return localData; + } + + public void reportException(Throwable t) { + Handler handler = exceptionHandler; + if (handler == null) { + handler = owner.exceptionHandler(); + } + if (handler != null) { + handler.handle(t); + } else { + throw new RuntimeException("Unhandled exception", t); + } + } + + @Override + public Context exceptionHandler(Handler handler) { + exceptionHandler = handler; + return this; + } + + @Override + public Handler exceptionHandler() { + return exceptionHandler; + } + + @Override + public final void runOnContext(Handler action) { + runOnContext(this, action); + } + + protected abstract void runOnContext(ContextInternal ctx, Handler action); + + @Override + public void execute(Runnable task) { + execute(this, task); + } + + protected abstract void execute(ContextInternal ctx, Runnable task); + + @Override + public final void execute(T argument, Handler task) { + execute(this, argument, task); + } + + protected abstract void execute(ContextInternal ctx, T argument, Handler task); + + @Override + public void emit(T argument, Handler task) { + emit(this, argument, task); + } + + protected abstract void emit(ContextInternal ctx, T argument, Handler task); + + @Override + public ContextInternal duplicate() { + return new DuplicatedContext(this); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/ContextInternal.java b/src/main/java/org/xbib/event/async/impl/ContextInternal.java new file mode 100644 index 0000000..93a9fb6 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/ContextInternal.java @@ -0,0 +1,392 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.*; +import org.xbib.event.async.Context; +import org.xbib.event.async.impl.future.FailedFuture; +import org.xbib.event.async.impl.future.PromiseImpl; +import org.xbib.event.async.impl.future.PromiseInternal; +import org.xbib.event.async.impl.future.SucceededFuture; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +/** + * This interface provides an api for internal use only + * It is not part of the public API and should not be used by + * developers. + */ +public interface ContextInternal extends Context { + + /** + * @return the current context + */ + static ContextInternal current() { + Thread thread = Thread.currentThread(); + if (thread instanceof AsyncThread) { + return ((AsyncThread) thread).context(); + } else { + AsyncImpl.ContextDispatch current = AsyncImpl.nonContextDispatch.get(); + if (current != null) { + return current.context; + } + } + return null; + } + + @Override + default void runOnContext(Handler action) { + executor().execute(() -> dispatch(action)); + } + + /** + * @return an executor that schedule a task on this context, the thread executing the task will not be associated with this context + */ + Executor executor(); + + /** + * @return a {@link Promise} associated with this context + */ + default PromiseInternal promise() { + return new PromiseImpl<>(this); + } + + /** + * @return a {@link Promise} associated with this context or the {@code handler} + * if that handler is already an instance of {@code PromiseInternal} + */ + default PromiseInternal promise(Promise p) { + if (p instanceof PromiseInternal) { + PromiseInternal promise = (PromiseInternal) p; + if (promise.context() != null) { + return promise; + } + } + PromiseInternal promise = promise(); + promise.future().onComplete(p); + return promise; + } + + /** + * @return an empty succeeded {@link Future} associated with this context + */ + default Future succeededFuture() { + return new SucceededFuture<>(this, null); + } + + /** + * @return a succeeded {@link Future} of the {@code result} associated with this context + */ + default Future succeededFuture(T result) { + return new SucceededFuture<>(this, result); + } + + /** + * @return a {@link Future} failed with the {@code failure} associated with this context + */ + default Future failedFuture(Throwable failure) { + return new FailedFuture<>(this, failure); + } + + /** + * @return a {@link Future} failed with the {@code message} associated with this context + */ + default Future failedFuture(String message) { + return new FailedFuture<>(this, message); + } + + /** + * Like {@link #executeBlocking(Handler, boolean)} but uses the {@code queue} to order the tasks instead + * of the internal queue of this context. + */ + Future executeBlocking(Handler> blockingCodeHandler, TaskQueue queue); + + /** + * Execute an internal task on the internal blocking ordered executor. + */ + Future executeBlockingInternal(Handler> action); + + /** + * Execute an internal task on the internal blocking ordered executor. + */ + Future executeBlockingInternal(Handler> action, boolean ordered); + + /** + * @return the deployment associated with this context or {@code null} + */ + + @Override + AsyncInternal owner(); + + boolean inThread(); + + /** + * Emit the given {@code argument} event to the {@code task} and switch on this context if necessary, this also associates the + * current thread with the current context so {@link Async#currentContext()} returns this context. + *
+ * Any exception thrown from the {@literal task} will be reported on this context. + *
+ * Calling this method is equivalent to {@code execute(v -> dispatch(argument, task))} + * + * @param argument the {@code task} argument + * @param task the handler to execute with the {@code event} argument + */ + void emit(T argument, Handler task); + + /** + * @see #emit(Object, Handler) + */ + default void emit(Handler task) { + emit(null, task); + } + + /** + * @see #execute(Object, Handler) + */ + default void execute(Handler task) { + execute(null, task); + } + + /** + * Execute the {@code task} on this context, it will be executed according to the + * context concurrency model. + * + * @param task the task to execute + */ + void execute(Runnable task); + + /** + * Execute a {@code task} on this context, the task will be executed according to the + * context concurrency model. + * + * @param argument the {@code task} argument + * @param task the task to execute + */ + void execute(T argument, Handler task); + + /** + * @return whether the current thread is running on this context + */ + default boolean isRunningOnContext() { + return current() == this && inThread(); + } + + /** + * @see #dispatch(Handler) + */ + default void dispatch(Runnable handler) { + ContextInternal prev = beginDispatch(); + try { + handler.run(); + } catch (Throwable t) { + reportException(t); + } finally { + endDispatch(prev); + } + } + + /** + * @see #dispatch(Object, Handler) + */ + default void dispatch(Handler handler) { + dispatch(null, handler); + } + + /** + * Dispatch an {@code event} to the {@code handler} on this context. + *

+ * The handler is executed directly by the caller thread which must be a context thread. + *

+ * The handler execution is monitored by the blocked thread checker. + *

+ * This context is thread-local associated during the task execution. + * + * @param event the event for the {@code handler} + * @param handler the handler to execute with the {@code event} + */ + default void dispatch(E event, Handler handler) { + ContextInternal prev = beginDispatch(); + try { + handler.handle(event); + } catch (Throwable t) { + reportException(t); + } finally { + endDispatch(prev); + } + } + + /** + * Begin the execution of a task on this context. + *

+ * The task execution is monitored by the blocked thread checker. + *

+ * This context is thread-local associated during the task execution. + *

+ * You should not use this API directly, instead you should use {@link #dispatch(Object, Handler)} + * + * @return the previous context that shall be restored after or {@code null} if there is none + * @throws IllegalStateException when the current thread of execution cannot execute this task + */ + default ContextInternal beginDispatch() { + AsyncImpl async = (AsyncImpl) owner(); + return async.beginDispatch(this); + } + + /** + * End the execution of a task on this context, see {@link #beginDispatch()} + *

+ * You should not use this API directly, instead you should use {@link #dispatch(Object, Handler)} + * + * @param previous the previous context to restore or {@code null} if there is none + * @throws IllegalStateException when the current thread of execution cannot execute this task + */ + default void endDispatch(ContextInternal previous) { + AsyncImpl async = (AsyncImpl) owner(); + async.endDispatch(previous); + } + + /** + * Report an exception to this context synchronously. + *

+ * The exception handler will be called when there is one, otherwise the exception will be logged. + * + * @param t the exception to report + */ + void reportException(Throwable t); + + /** + * @return the {@link ConcurrentMap} used to store context data + * @see Context#get(Object) + * @see Context#put(Object, Object) + */ + ConcurrentMap contextData(); + + @SuppressWarnings("unchecked") + @Override + default T get(Object key) { + return (T) contextData().get(key); + } + + @Override + default void put(Object key, Object value) { + contextData().put(key, value); + } + + @Override + default boolean remove(Object key) { + return contextData().remove(key) != null; + } + + /** + * @return the {@link ConcurrentMap} used to store local context data + */ + ConcurrentMap localContextData(); + + @SuppressWarnings("unchecked") + @Override + default T getLocal(Object key) { + return (T) localContextData().get(key); + } + + @Override + default void putLocal(Object key, Object value) { + localContextData().put(key, value); + } + + @Override + default boolean removeLocal(Object key) { + return localContextData().remove(key) != null; + } + + /** + * @return the classloader associated with this context + */ + ClassLoader classLoader(); + + /** + * @return the context worker pool + */ + WorkerPool workerPool(); + + + /** + * Returns a context sharing with this context + *

    + *
  • the same concurrency
  • + *
  • the same exception handler
  • + *
  • the same context data
  • + *
  • the same deployment
  • + *
  • the same config
  • + *
  • the same classloader
  • + *
+ *

+ * The duplicate context has its own + *

    + *
  • local context data
  • + *
  • worker task queue
  • + *
+ * + * @return a duplicate of this context + */ + ContextInternal duplicate(); + + /** + * Like {@link Async#setPeriodic(long, Handler)} except the periodic timer will fire on this context and the + * timer will not be associated with the context close hook. + */ + default long setPeriodic(long delay, Handler handler) { + AsyncImpl owner = (AsyncImpl) owner(); + return owner.scheduleTimeout(this, true, delay, TimeUnit.MILLISECONDS, false, handler); + } + + /** + * Like {@link Async#setTimer(long, Handler)} except the timer will fire on this context and the timer + * will not be associated with the context close hook. + */ + default long setTimer(long delay, Handler handler) { + AsyncImpl owner = (AsyncImpl) owner(); + return owner.scheduleTimeout(this, false, delay, TimeUnit.MILLISECONDS, false, handler); + } + + CloseFuture closeFuture(); + + /** + * Add a close hook. + * + *

The {@code hook} will be called when the associated resource needs to be released. + * + * @param hook the close hook + */ + default void addCloseHook(Closeable hook) { + closeFuture().add(hook); + } + + /** + * Remove a close hook. + * + *

This is called when the resource is released explicitly and does not need anymore a managed close. + * + * @param hook the close hook + */ + default void removeCloseHook(Closeable hook) { + closeFuture().remove(hook); + } + + /** + * Returns the original context, a duplicate context returns the wrapped context otherwise this instance is returned. + * + * @return the wrapped context + */ + default ContextInternal unwrap() { + return this; + } + + /** + * Returns {@code true} if this context is a duplicated context. + * + * @return {@code true} if this context is a duplicated context, {@code false} otherwise. + */ + default boolean isDuplicate() { + return false; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/DuplicatedContext.java b/src/main/java/org/xbib/event/async/impl/DuplicatedContext.java new file mode 100644 index 0000000..7a3102d --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/DuplicatedContext.java @@ -0,0 +1,161 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Context; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.Promise; +import org.xbib.event.loop.EventLoop; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; + +/** + * A context that forwards most operations to a delegate. This context + * + *

    + *
  • maintains its own ordered task queue, ordered execute blocking are ordered on this + * context instead of the delegate.
  • + *
  • maintains its own local data instead of the delegate.
  • + *
+ */ +class DuplicatedContext implements ContextInternal { + + protected final ContextBase delegate; + private ConcurrentMap localData; + + DuplicatedContext(ContextBase delegate) { + this.delegate = delegate; + } + + @Override + public boolean inThread() { + return delegate.inThread(); + } + + @Override + public final CloseFuture closeFuture() { + return delegate.closeFuture(); + } + + + @Override + public final Context exceptionHandler(Handler handler) { + delegate.exceptionHandler(handler); + return this; + } + + @Override + public Executor executor() { + return delegate.executor(); + } + + @Override + public final Handler exceptionHandler() { + return delegate.exceptionHandler(); + } + + public final EventLoop eventLoop() { + return delegate.eventLoop(); + } + + @Override + public final AsyncInternal owner() { + return delegate.owner(); + } + + @Override + public final ClassLoader classLoader() { + return delegate.classLoader(); + } + + @Override + public WorkerPool workerPool() { + return delegate.workerPool(); + } + + @Override + public final void reportException(Throwable t) { + delegate.reportException(t); + } + + @Override + public final ConcurrentMap contextData() { + return delegate.contextData(); + } + + @Override + public final ConcurrentMap localContextData() { + synchronized (this) { + if (localData == null) { + localData = new ConcurrentHashMap<>(); + } + return localData; + } + } + + @Override + public final Future executeBlockingInternal(Handler> action) { + return ContextBase.executeBlocking(this, action, delegate.internalWorkerPool, delegate.internalOrderedTasks); + } + + @Override + public final Future executeBlockingInternal(Handler> action, boolean ordered) { + return ContextBase.executeBlocking(this, action, delegate.internalWorkerPool, ordered ? delegate.internalOrderedTasks : null); + } + + @Override + public final Future executeBlocking(Handler> action, boolean ordered) { + return ContextBase.executeBlocking(this, action, delegate.workerPool, ordered ? delegate.orderedTasks : null); + } + + @Override + public final Future executeBlocking(Handler> blockingCodeHandler, TaskQueue queue) { + return ContextBase.executeBlocking(this, blockingCodeHandler, delegate.workerPool, queue); + } + + @Override + public final void runOnContext(Handler action) { + delegate.runOnContext(this, action); + } + + @Override + public final void execute(T argument, Handler task) { + delegate.execute(this, argument, task); + } + + @Override + public void emit(T argument, Handler task) { + delegate.emit(this, argument, task); + } + + @Override + public void execute(Runnable task) { + delegate.execute(this, task); + } + + @Override + public boolean isEventLoopContext() { + return delegate.isEventLoopContext(); + } + + @Override + public boolean isWorkerContext() { + return delegate.isWorkerContext(); + } + + @Override + public ContextInternal duplicate() { + return new DuplicatedContext(delegate); + } + + @Override + public ContextInternal unwrap() { + return delegate; + } + + @Override + public boolean isDuplicate() { + return true; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/EventLoopContext.java b/src/main/java/org/xbib/event/async/impl/EventLoopContext.java new file mode 100644 index 0000000..7f955fe --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/EventLoopContext.java @@ -0,0 +1,92 @@ +package org.xbib.event.async.impl; + +import org.xbib.event.async.Handler; +import org.xbib.event.loop.EventLoop; + +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; + +public class EventLoopContext extends ContextBase { + + EventLoopContext(AsyncInternal asyncInternal, + EventLoop eventLoop, + WorkerPool internalBlockingPool, + WorkerPool workerPool, + CloseFuture closeFuture, + ClassLoader tccl) { + super(asyncInternal, eventLoop, internalBlockingPool, workerPool, closeFuture, tccl); + } + + @Override + public Executor executor() { + return eventLoop(); + } + + @Override + protected void runOnContext(ContextInternal ctx, Handler action) { + try { + eventLoop().execute(() -> ctx.dispatch(action)); + } catch (RejectedExecutionException ignore) { + // Pool is already shut down + } + } + + @Override + protected void emit(ContextInternal ctx, T argument, Handler task) { + EventLoop eventLoop = eventLoop(); + if (eventLoop.inEventLoop()) { + ContextInternal prev = ctx.beginDispatch(); + try { + task.handle(argument); + } catch (Throwable t) { + reportException(t); + } finally { + ctx.endDispatch(prev); + } + } else { + eventLoop.execute(() -> emit(ctx, argument, task)); + } + } + + /** + *
    + *
  • When the current thread is event-loop thread of this context the implementation will execute the {@code task} directly
  • + *
  • Otherwise the task will be scheduled on the event-loop thread for execution
  • + *
+ */ + @Override + protected void execute(ContextInternal ctx, T argument, Handler task) { + EventLoop eventLoop = eventLoop(); + if (eventLoop.inEventLoop()) { + task.handle(argument); + } else { + eventLoop.execute(() -> task.handle(argument)); + } + } + + @Override + protected void execute(ContextInternal ctx, Runnable task) { + EventLoop eventLoop = eventLoop(); + if (eventLoop.inEventLoop()) { + task.run(); + } else { + eventLoop.execute(task); + } + } + + @Override + public boolean isEventLoopContext() { + return true; + } + + @Override + public boolean isWorkerContext() { + return false; + } + + @Override + public boolean inThread() { + return eventLoop().inEventLoop(); + } + +} diff --git a/src/main/java/org/xbib/event/async/impl/NestedCloseable.java b/src/main/java/org/xbib/event/async/impl/NestedCloseable.java new file mode 100644 index 0000000..050497d --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/NestedCloseable.java @@ -0,0 +1,10 @@ +package org.xbib.event.async.impl; + +/** + * Keeps a reference to an owner close future so we can unregister from it when the closeable is closed. + */ +abstract class NestedCloseable { + + CloseFuture owner; + +} diff --git a/src/main/java/org/xbib/event/async/impl/TaskQueue.java b/src/main/java/org/xbib/event/async/impl/TaskQueue.java new file mode 100644 index 0000000..d5d7f54 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/TaskQueue.java @@ -0,0 +1,88 @@ +package org.xbib.event.async.impl; + +import java.util.LinkedList; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A task queue that always run all tasks in order. The executor to run the tasks is passed + * when the tasks are executed, this executor is not guaranteed to be used, as if several + * tasks are queued, the original thread will be used. + * + * More specifically, any call B to the {@link #execute(Runnable, Executor)} method that happens-after another call A to the + * same method, will result in B's task running after A's. + * + */ +public class TaskQueue { + + static final Logger log = Logger.getLogger(TaskQueue.class.getName()); + + private static class Task { + + private final Runnable runnable; + private final Executor exec; + + public Task(Runnable runnable, Executor exec) { + this.runnable = runnable; + this.exec = exec; + } + } + + // @protectedby tasks + private final LinkedList tasks = new LinkedList<>(); + + // @protectedby tasks + private Executor current; + + private final Runnable runner; + + public TaskQueue() { + runner = this::run; + } + + private void run() { + for (; ; ) { + final Task task; + synchronized (tasks) { + task = tasks.poll(); + if (task == null) { + current = null; + return; + } + if (task.exec != current) { + tasks.addFirst(task); + task.exec.execute(runner); + current = task.exec; + return; + } + } + try { + task.runnable.run(); + } catch (Throwable t) { + log.log(Level.SEVERE, "Caught unexpected Throwable", t); + } + } + }; + + /** + * Run a task. + * + * @param task the task to run. + */ + public void execute(Runnable task, Executor executor) { + synchronized (tasks) { + tasks.add(new Task(task, executor)); + if (current == null) { + current = executor; + try { + executor.execute(runner); + } catch (RejectedExecutionException e) { + current = null; + throw e; + } + } + } + } +} diff --git a/src/main/java/org/xbib/event/async/impl/WorkerPool.java b/src/main/java/org/xbib/event/async/impl/WorkerPool.java new file mode 100644 index 0000000..c8b6058 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/WorkerPool.java @@ -0,0 +1,20 @@ +package org.xbib.event.async.impl; + +import java.util.concurrent.ExecutorService; + +public class WorkerPool { + + private final ExecutorService pool; + + public WorkerPool(ExecutorService pool) { + this.pool = pool; + } + + public ExecutorService executor() { + return pool; + } + + void close() { + pool.shutdownNow(); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/CompositeFutureImpl.java b/src/main/java/org/xbib/event/async/impl/future/CompositeFutureImpl.java new file mode 100644 index 0000000..3deafbc --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/CompositeFutureImpl.java @@ -0,0 +1,189 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.CompositeFuture; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; + +import java.util.function.Function; + +public class CompositeFutureImpl extends FutureImpl implements CompositeFuture { + + public static CompositeFuture all(Future... results) { + CompositeFutureImpl composite = new CompositeFutureImpl(results); + int len = results.length; + for (Future result : results) { + result.onComplete(ar -> { + if (ar.succeeded()) { + synchronized (composite) { + if (composite.count == len || ++composite.count != len) { + return; + } + } + composite.trySucceed(); + } else { + synchronized (composite) { + if (composite.count == len) { + return; + } + composite.count = len; + } + composite.tryFail(ar.cause()); + } + }); + } + if (len == 0) { + composite.trySucceed(); + } + return composite; + } + + public static CompositeFuture any(Future... results) { + CompositeFutureImpl composite = new CompositeFutureImpl(results); + int len = results.length; + for (Future result : results) { + result.onComplete(ar -> { + if (ar.succeeded()) { + synchronized (composite) { + if (composite.count == len) { + return; + } + composite.count = len; + } + composite.trySucceed(); + } else { + synchronized (composite) { + if (composite.count == len || ++composite.count != len) { + return; + } + } + composite.tryFail(ar.cause()); + } + }); + } + if (results.length == 0) { + composite.trySucceed(); + } + return composite; + } + + private static final Function ALL = cf -> { + int size = cf.size(); + for (int i = 0;i < size;i++) { + if (!cf.succeeded(i)) { + return cf.cause(i); + } + } + return cf; + }; + + public static CompositeFuture join(Future... results) { + return join(ALL, results); + } + + private static CompositeFuture join(Function pred, Future... results) { + CompositeFutureImpl composite = new CompositeFutureImpl(results); + int len = results.length; + for (Future result : results) { + result.onComplete(ar -> { + synchronized (composite) { + if (++composite.count < len) { + return; + } + } + composite.complete(pred.apply(composite)); + }); + } + if (len == 0) { + composite.trySucceed(); + } + return composite; + } + + private final Future[] results; + private int count; + + private CompositeFutureImpl(Future... results) { + this.results = results; + } + + @Override + public Throwable cause(int index) { + return future(index).cause(); + } + + @Override + public boolean succeeded(int index) { + return future(index).succeeded(); + } + + @Override + public boolean failed(int index) { + return future(index).failed(); + } + + @Override + public boolean isComplete(int index) { + return future(index).isComplete(); + } + + @Override + public T resultAt(int index) { + return this.future(index).result(); + } + + private Future future(int index) { + if (index < 0 || index >= results.length) { + throw new IndexOutOfBoundsException(); + } + return (Future) results[index]; + } + + @Override + public int size() { + return results.length; + } + + private void trySucceed() { + tryComplete(this); + } + + private void fail(Throwable t) { + complete(t); + } + + private void complete(Object result) { + if (result == this) { + tryComplete(this); + } else if (result instanceof Throwable) { + tryFail((Throwable) result); + } + } + + @Override + public CompositeFuture onComplete(Handler> handler) { + return (CompositeFuture)super.onComplete(handler); + } + + @Override + public CompositeFuture onSuccess(Handler handler) { + return (CompositeFuture)super.onSuccess(handler); + } + + @Override + public CompositeFuture onFailure(Handler handler) { + return (CompositeFuture)super.onFailure(handler); + } + + @Override + protected void formatValue(Object value, StringBuilder sb) { + sb.append('('); + for (int i = 0;i < results.length;i++) { + if (i > 0) { + sb.append(','); + } + sb.append(results[i]); + } + sb.append(')'); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Composition.java b/src/main/java/org/xbib/event/async/impl/future/Composition.java new file mode 100644 index 0000000..d83b6be --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Composition.java @@ -0,0 +1,58 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.Future; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +/** + * Function compose transformation. + */ +class Composition extends Operation implements Listener { + + private final Function> successMapper; + private final Function> failureMapper; + + Composition(ContextInternal context, Function> successMapper, Function> failureMapper) { + super(context); + this.successMapper = successMapper; + this.failureMapper = failureMapper; + } + + @Override + public void onSuccess(T value) { + FutureInternal future; + try { + future = (FutureInternal) successMapper.apply(value); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(newListener()); + } + + @Override + public void onFailure(Throwable failure) { + FutureInternal future; + try { + future = (FutureInternal) failureMapper.apply(failure); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(newListener()); + } + + private Listener newListener() { + return new Listener() { + @Override + public void onSuccess(U value) { + tryComplete(value); + } + @Override + public void onFailure(Throwable failure) { + tryFail(failure); + } + }; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Eventually.java b/src/main/java/org/xbib/event/async/impl/future/Eventually.java new file mode 100644 index 0000000..f56bb42 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Eventually.java @@ -0,0 +1,62 @@ +package org.xbib.event.async.impl.future; + + +import org.xbib.event.async.Future; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +/** + * Eventually operation. + */ +class Eventually extends Operation implements Listener { + + private final Function> mapper; + + Eventually(ContextInternal context, Function> mapper) { + super(context); + this.mapper = mapper; + } + + @Override + public void onSuccess(T value) { + FutureInternal future; + try { + future = (FutureInternal) mapper.apply(null); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(new Listener() { + @Override + public void onSuccess(U ignore) { + tryComplete(value); + } + @Override + public void onFailure(Throwable ignore) { + tryComplete(value); + } + }); + } + + @Override + public void onFailure(Throwable failure) { + FutureInternal future; + try { + future = (FutureInternal) mapper.apply(null); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(new Listener<>() { + @Override + public void onSuccess(U ignore) { + tryFail(failure); + } + @Override + public void onFailure(Throwable ignore) { + tryFail(failure); + } + }); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FailedFuture.java b/src/main/java/org/xbib/event/async/impl/future/FailedFuture.java new file mode 100644 index 0000000..9bc800d --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FailedFuture.java @@ -0,0 +1,127 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.NoStackTraceThrowable; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +/** + * Failed future implementation. + */ +public final class FailedFuture extends FutureBase { + + private final Throwable cause; + + /** + * Create a future that has already failed + * @param t the throwable + */ + public FailedFuture(Throwable t) { + this(null, t); + } + + /** + * Create a future that has already failed + * @param t the throwable + */ + public FailedFuture(ContextInternal context, Throwable t) { + super(context); + this.cause = t != null ? t : new NoStackTraceThrowable(null); + } + + /** + * Create a future that has already failed + * @param failureMessage the failure message + */ + public FailedFuture(String failureMessage) { + this(null, failureMessage); + } + + /** + * Create a future that has already failed + * @param failureMessage the failure message + */ + public FailedFuture(ContextInternal context, String failureMessage) { + this(context, new NoStackTraceThrowable(failureMessage)); + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public Future onComplete(Handler> handler) { + if (handler instanceof Listener) { + emitFailure(cause, (Listener) handler); + } else if (context != null) { + context.emit(this, handler); + } else { + handler.handle(this); + } + return this; + } + + @Override + public Future onSuccess(Handler handler) { + return this; + } + + @Override + public Future onFailure(Handler handler) { + if (context != null) { + context.emit(cause, handler); + } else { + handler.handle(cause); + } + return this; + } + + @Override + public void addListener(Listener listener) { + emitFailure(cause, listener); + } + + @Override + public T result() { + return null; + } + + @Override + public Throwable cause() { + return cause; + } + + @Override + public boolean succeeded() { + return false; + } + + @Override + public boolean failed() { + return true; + } + + @Override + public Future map(Function mapper) { + return (Future) this; + } + + @Override + public Future map(V value) { + return (Future) this; + } + + @Override + public Future otherwise(T value) { + return new SucceededFuture<>(context, value); + } + + @Override + public String toString() { + return "Future{cause=" + cause.getMessage() + "}"; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FixedMapping.java b/src/main/java/org/xbib/event/async/impl/future/FixedMapping.java new file mode 100644 index 0000000..8a602ec --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FixedMapping.java @@ -0,0 +1,26 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.impl.ContextInternal; + +/** + * Map value transformation. + */ +class FixedMapping extends Operation implements Listener { + + private final U value; + + FixedMapping(ContextInternal context, U value) { + super(context); + this.value = value; + } + + @Override + public void onSuccess(T value) { + tryComplete(this.value); + } + + @Override + public void onFailure(Throwable failure) { + tryFail(failure); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FixedOtherwise.java b/src/main/java/org/xbib/event/async/impl/future/FixedOtherwise.java new file mode 100644 index 0000000..6728f40 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FixedOtherwise.java @@ -0,0 +1,26 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.impl.ContextInternal; + +/** + * Otherwise value transformation. + */ +class FixedOtherwise extends Operation implements Listener { + + private final T value; + + FixedOtherwise(ContextInternal context, T value) { + super(context); + this.value = value; + } + + @Override + public void onSuccess(T value) { + tryComplete(value); + } + + @Override + public void onFailure(Throwable failure) { + tryComplete(value); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FutureBase.java b/src/main/java/org/xbib/event/async/impl/future/FutureBase.java new file mode 100644 index 0000000..ad9d151 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FutureBase.java @@ -0,0 +1,119 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Future base implementation. + */ +public abstract class FutureBase implements FutureInternal { + + protected final ContextInternal context; + + /** + * Create a future that hasn't completed yet + */ + protected FutureBase() { + this(null); + } + + /** + * Create a future that hasn't completed yet + */ + FutureBase(ContextInternal context) { + this.context = context; + } + + public final ContextInternal context() { + return context; + } + + protected final void emitSuccess(T value, Listener listener) { + if (context != null && !context.isRunningOnContext()) { + context.execute(() -> { + ContextInternal prev = context.beginDispatch(); + try { + listener.onSuccess(value); + } finally { + context.endDispatch(prev); + } + }); + } else { + listener.onSuccess(value); + } + } + + protected final void emitFailure(Throwable cause, Listener listener) { + if (context != null && !context.isRunningOnContext()) { + context.execute(() -> { + ContextInternal prev = context.beginDispatch(); + try { + listener.onFailure(cause); + } finally { + context.endDispatch(prev); + } + }); + } else { + listener.onFailure(cause); + } + } + + @Override + public Future compose(Function> successMapper, Function> failureMapper) { + Objects.requireNonNull(successMapper, "No null success mapper accepted"); + Objects.requireNonNull(failureMapper, "No null failure mapper accepted"); + Composition operation = new Composition<>(context, successMapper, failureMapper); + addListener(operation); + return operation; + } + + @Override + public Future transform(Function, Future> mapper) { + Objects.requireNonNull(mapper, "No null mapper accepted"); + Transformation operation = new Transformation<>(context, this, mapper); + addListener(operation); + return operation; + } + + @Override + public Future eventually(Function> mapper) { + Objects.requireNonNull(mapper, "No null mapper accepted"); + Eventually operation = new Eventually<>(context, mapper); + addListener(operation); + return operation; + } + + @Override + public Future map(Function mapper) { + Objects.requireNonNull(mapper, "No null mapper accepted"); + Mapping operation = new Mapping<>(context, mapper); + addListener(operation); + return operation; + } + + @Override + public Future map(V value) { + FixedMapping transformation = new FixedMapping<>(context, value); + addListener(transformation); + return transformation; + } + + @Override + public Future otherwise(Function mapper) { + Objects.requireNonNull(mapper, "No null mapper accepted"); + Otherwise transformation = new Otherwise<>(context, mapper); + addListener(transformation); + return transformation; + } + + @Override + public Future otherwise(T value) { + FixedOtherwise operation = new FixedOtherwise<>(context, value); + addListener(operation); + return operation; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FutureImpl.java b/src/main/java/org/xbib/event/async/impl/future/FutureImpl.java new file mode 100644 index 0000000..2626efc --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FutureImpl.java @@ -0,0 +1,268 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.NoStackTraceThrowable; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * Future implementation. + */ +class FutureImpl extends FutureBase { + + private static final Object NULL_VALUE = new Object(); + + private Object value; + private Listener listener; + + /** + * Create a future that hasn't completed yet + */ + FutureImpl() { + super(); + } + + /** + * Create a future that hasn't completed yet + */ + FutureImpl(ContextInternal context) { + super(context); + } + + /** + * The result of the operation. This will be null if the operation failed. + */ + public synchronized T result() { + return value instanceof CauseHolder ? null : value == NULL_VALUE ? null : (T) value; + } + + /** + * An exception describing failure. This will be null if the operation succeeded. + */ + public synchronized Throwable cause() { + return value instanceof CauseHolder ? ((CauseHolder)value).cause : null; + } + + /** + * Did it succeed? + */ + public synchronized boolean succeeded() { + return value != null && !(value instanceof CauseHolder); + } + + /** + * Did it fail? + */ + public synchronized boolean failed() { + return value instanceof CauseHolder; + } + + /** + * Has it completed? + */ + public synchronized boolean isComplete() { + return value != null; + } + + @Override + public Future onSuccess(Handler handler) { + Objects.requireNonNull(handler, "No null handler accepted"); + addListener(new Listener() { + @Override + public void onSuccess(T value) { + try { + handler.handle(value); + } catch (Throwable t) { + if (context != null) { + context.reportException(t); + } else { + throw t; + } + } + } + @Override + public void onFailure(Throwable failure) { + } + }); + return this; + } + + @Override + public Future onFailure(Handler handler) { + Objects.requireNonNull(handler, "No null handler accepted"); + addListener(new Listener() { + @Override + public void onSuccess(T value) { + } + @Override + public void onFailure(Throwable failure) { + try { + handler.handle(failure); + } catch (Throwable t) { + if (context != null) { + context.reportException(t); + } else { + throw t; + } + } + } + }); + return this; + } + + @Override + public Future onComplete(Handler> handler) { + Objects.requireNonNull(handler, "No null handler accepted"); + Listener listener; + if (handler instanceof Listener) { + listener = (Listener) handler; + } else { + listener = new Listener() { + @Override + public void onSuccess(T value) { + try { + handler.handle(FutureImpl.this); + } catch (Throwable t) { + if (context != null) { + context.reportException(t); + } else { + throw t; + } + } + } + @Override + public void onFailure(Throwable failure) { + try { + handler.handle(FutureImpl.this); + } catch (Throwable t) { + if (context != null) { + context.reportException(t); + } else { + throw t; + } + } + } + }; + } + addListener(listener); + return this; + } + + @Override + public void addListener(Listener listener) { + Object v; + synchronized (this) { + v = value; + if (v == null) { + if (this.listener == null) { + this.listener = listener; + } else { + ListenerArray listeners; + if (this.listener instanceof FutureImpl.ListenerArray) { + listeners = (ListenerArray) this.listener; + } else { + listeners = new ListenerArray<>(); + listeners.add(this.listener); + this.listener = listeners; + } + listeners.add(listener); + } + return; + } + } + if (v instanceof CauseHolder) { + emitFailure(((CauseHolder)v).cause, listener); + } else { + if (v == NULL_VALUE) { + v = null; + } + emitSuccess((T) v, listener); + } + } + + public boolean tryComplete(T result) { + Listener l; + synchronized (this) { + if (value != null) { + return false; + } + value = result == null ? NULL_VALUE : result; + l = listener; + listener = null; + } + if (l != null) { + emitSuccess(result, l); + } + return true; + } + + public boolean tryFail(Throwable cause) { + if (cause == null) { + cause = new NoStackTraceThrowable(null); + } + Listener l; + synchronized (this) { + if (value != null) { + return false; + } + value = new CauseHolder(cause); + l = listener; + listener = null; + } + if (l != null) { + emitFailure(cause, l); + } + return true; + } + + @Override + public String toString() { + synchronized (this) { + if (value instanceof CauseHolder) { + return "Future{cause=" + ((CauseHolder)value).cause.getMessage() + "}"; + } + if (value != null) { + if (value == NULL_VALUE) { + return "Future{result=null}"; + } + StringBuilder sb = new StringBuilder("Future{result="); + formatValue(value, sb); + sb.append("}"); + return sb.toString(); + } + return "Future{unresolved}"; + } + } + + protected void formatValue(Object value, StringBuilder sb) { + sb.append(value); + } + + private static class ListenerArray extends ArrayList> implements Listener { + @Override + public void onSuccess(T value) { + for (Listener handler : this) { + handler.onSuccess(value); + } + } + @Override + public void onFailure(Throwable failure) { + for (Listener handler : this) { + handler.onFailure(failure); + } + } + } + + private static class CauseHolder { + + private final Throwable cause; + + CauseHolder(Throwable cause) { + this.cause = cause; + } + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/FutureInternal.java b/src/main/java/org/xbib/event/async/impl/future/FutureInternal.java new file mode 100644 index 0000000..c49ea59 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/FutureInternal.java @@ -0,0 +1,23 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.Future; +import org.xbib.event.async.impl.ContextInternal; + +/** + * Expose some of the future internal stuff. + */ +public interface FutureInternal extends Future { + + /** + * @return the context associated with this promise or {@code null} when there is none + */ + ContextInternal context(); + + /** + * Add a listener to the future result. + * + * @param listener the listener + */ + void addListener(Listener listener); + +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Listener.java b/src/main/java/org/xbib/event/async/impl/future/Listener.java new file mode 100644 index 0000000..8667aee --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Listener.java @@ -0,0 +1,21 @@ +package org.xbib.event.async.impl.future; + +/** + * Internal listener that signals success or failure when a future completes. + */ +public interface Listener { + + /** + * Signal the success. + * + * @param value the value + */ + void onSuccess(T value); + + /** + * Signal the failure + * + * @param failure the failure + */ + void onFailure(Throwable failure); +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Mapping.java b/src/main/java/org/xbib/event/async/impl/future/Mapping.java new file mode 100644 index 0000000..d8a177a --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Mapping.java @@ -0,0 +1,35 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +/** + * Function map transformation. + */ +class Mapping extends Operation implements Listener { + + private final Function successMapper; + + Mapping(ContextInternal context, Function successMapper) { + super(context); + this.successMapper = successMapper; + } + + @Override + public void onSuccess(T value) { + U result; + try { + result = successMapper.apply(value); + } catch (Throwable e) { + tryFail(e); + return; + } + tryComplete(result); + } + + @Override + public void onFailure(Throwable failure) { + tryFail(failure); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Operation.java b/src/main/java/org/xbib/event/async/impl/future/Operation.java new file mode 100644 index 0000000..87dc739 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Operation.java @@ -0,0 +1,13 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.impl.ContextInternal; + +/** + * Base class for transforming the completion of a future. + */ +abstract class Operation extends FutureImpl { + + protected Operation(ContextInternal context) { + super(context); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Otherwise.java b/src/main/java/org/xbib/event/async/impl/future/Otherwise.java new file mode 100644 index 0000000..c966472 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Otherwise.java @@ -0,0 +1,32 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +class Otherwise extends Operation implements Listener { + + private final Function mapper; + + Otherwise(ContextInternal context, Function mapper) { + super(context); + this.mapper = mapper; + } + + @Override + public void onSuccess(T value) { + tryComplete(value); + } + + @Override + public void onFailure(Throwable failure) { + T result; + try { + result = mapper.apply(failure); + } catch (Throwable e) { + tryFail(e); + return; + } + tryComplete(result); + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/PromiseImpl.java b/src/main/java/org/xbib/event/async/impl/future/PromiseImpl.java new file mode 100644 index 0000000..d2f5f29 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/PromiseImpl.java @@ -0,0 +1,57 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.Future; +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.impl.ContextInternal; + +/** + * Promise implementation. + */ +public final class PromiseImpl extends FutureImpl implements PromiseInternal, Listener { + + /** + * Create a promise that hasn't completed yet + */ + public PromiseImpl() { + super(); + } + + /** + * Create a promise that hasn't completed yet + */ + public PromiseImpl(ContextInternal context) { + super(context); + } + + public void handle(AsyncResult ar) { + if (ar.succeeded()) { + onSuccess(ar.result()); + } else { + onFailure(ar.cause()); + } + } + + @Override + public void onSuccess(T value) { + tryComplete(value); + } + + @Override + public void onFailure(Throwable failure) { + tryFail(failure); + } + + @Override + public org.xbib.event.async.Future future() { + return this; + } + + @Override + public void operationComplete(Future future) { + if (future.isSuccess()) { + complete(future.getNow()); + } else { + fail(future.cause()); + } + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/PromiseInternal.java b/src/main/java/org/xbib/event/async/impl/future/PromiseInternal.java new file mode 100644 index 0000000..dde8ebe --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/PromiseInternal.java @@ -0,0 +1,14 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.FutureListener; +import org.xbib.event.async.Promise; +import org.xbib.event.async.impl.ContextInternal; + +public interface PromiseInternal extends Promise, FutureListener, FutureInternal { + + /** + * @return the context associated with this promise or {@code null} when there is none + */ + ContextInternal context(); + +} diff --git a/src/main/java/org/xbib/event/async/impl/future/SucceededFuture.java b/src/main/java/org/xbib/event/async/impl/future/SucceededFuture.java new file mode 100644 index 0000000..1987192 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/SucceededFuture.java @@ -0,0 +1,118 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.Objects; +import java.util.function.Function; + +/** + * Succeeded future implementation. + */ +public final class SucceededFuture extends FutureBase { + + /** + * Stateless instance of empty results that can be shared safely. + */ + public static final SucceededFuture EMPTY = new SucceededFuture(null, null); + + private final T result; + + /** + * Create a future that has already succeeded + * @param result the result + */ + public SucceededFuture(T result) { + this(null, result); + } + + /** + * Create a future that has already succeeded + * @param context the context + * @param result the result + */ + public SucceededFuture(ContextInternal context, T result) { + super(context); + this.result = result; + } + + @Override + public boolean isComplete() { + return true; + } + + @Override + public Future onSuccess(Handler handler) { + if (context != null) { + context.emit(result, handler); + } else { + handler.handle(result); + } + return this; + } + + @Override + public Future onFailure(Handler handler) { + return this; + } + + @Override + public Future onComplete(Handler> handler) { + if (handler instanceof Listener) { + emitSuccess(result ,(Listener) handler); + } else if (context != null) { + context.emit(this, handler); + } else { + handler.handle(this); + } + return this; + } + + @Override + public void addListener(Listener listener) { + emitSuccess(result ,listener); + } + + @Override + public T result() { + return result; + } + + @Override + public Throwable cause() { + return null; + } + + @Override + public boolean succeeded() { + return true; + } + + @Override + public boolean failed() { + return false; + } + + @Override + public Future map(V value) { + return new SucceededFuture<>(context, value); + } + + @Override + public Future otherwise(Function mapper) { + Objects.requireNonNull(mapper, "No null mapper accepted"); + return this; + } + + @Override + public Future otherwise(T value) { + return this; + } + + @Override + public String toString() { + return "Future{result=" + result + "}"; + } +} diff --git a/src/main/java/org/xbib/event/async/impl/future/Transformation.java b/src/main/java/org/xbib/event/async/impl/future/Transformation.java new file mode 100644 index 0000000..0a09463 --- /dev/null +++ b/src/main/java/org/xbib/event/async/impl/future/Transformation.java @@ -0,0 +1,59 @@ +package org.xbib.event.async.impl.future; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.impl.ContextInternal; + +import java.util.function.Function; + +/** + * Function compose transformation. + */ +class Transformation extends Operation implements Listener { + + private final Future future; + private final Function, Future> mapper; + + Transformation(ContextInternal context, Future future, Function, Future> mapper) { + super(context); + this.future = future; + this.mapper = mapper; + } + + @Override + public void onSuccess(T value) { + FutureInternal future; + try { + future = (FutureInternal) mapper.apply(this.future); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(newListener()); + } + + @Override + public void onFailure(Throwable failure) { + FutureInternal future; + try { + future = (FutureInternal) mapper.apply(this.future); + } catch (Throwable e) { + tryFail(e); + return; + } + future.addListener(newListener()); + } + + private Listener newListener() { + return new Listener() { + @Override + public void onSuccess(U value) { + tryComplete(value); + } + @Override + public void onFailure(Throwable failure) { + tryFail(failure); + } + }; + } +} diff --git a/src/main/java/org/xbib/event/async/spi/AsyncServiceProvider.java b/src/main/java/org/xbib/event/async/spi/AsyncServiceProvider.java new file mode 100644 index 0000000..54adeff --- /dev/null +++ b/src/main/java/org/xbib/event/async/spi/AsyncServiceProvider.java @@ -0,0 +1,17 @@ +package org.xbib.event.async.spi; + +import org.xbib.event.async.impl.AsyncBuilder; + +/** + * Entry point for loading Vert.x SPI implementations. + */ +public interface AsyncServiceProvider { + + /** + * Let the provider initialize the Vert.x builder. + * + * @param builder the builder + */ + void init(AsyncBuilder builder); + +} diff --git a/src/main/java/org/xbib/event/async/spi/AsyncThreadFactory.java b/src/main/java/org/xbib/event/async/spi/AsyncThreadFactory.java new file mode 100644 index 0000000..b122105 --- /dev/null +++ b/src/main/java/org/xbib/event/async/spi/AsyncThreadFactory.java @@ -0,0 +1,23 @@ +package org.xbib.event.async.spi; + +import org.xbib.event.async.impl.AsyncBuilder; +import org.xbib.event.async.impl.AsyncThread; + +import java.util.concurrent.TimeUnit; + +public interface AsyncThreadFactory extends AsyncServiceProvider { + + AsyncThreadFactory INSTANCE = new AsyncThreadFactory() { + }; + + @Override + default void init(AsyncBuilder builder) { + if (builder.threadFactory() == null) { + builder.threadFactory(this); + } + } + + default AsyncThread newVertxThread(Runnable target, String name, boolean worker, long maxExecTime, TimeUnit maxExecTimeUnit) { + return new AsyncThread(target, name, worker, maxExecTime, maxExecTimeUnit); + } +} diff --git a/src/main/java/org/xbib/event/async/spi/ExecutorServiceFactory.java b/src/main/java/org/xbib/event/async/spi/ExecutorServiceFactory.java new file mode 100644 index 0000000..a222970 --- /dev/null +++ b/src/main/java/org/xbib/event/async/spi/ExecutorServiceFactory.java @@ -0,0 +1,45 @@ +package org.xbib.event.async.spi; + +import org.xbib.event.async.impl.AsyncBuilder; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * The interface for a factory used to obtain an external + * {@code ExecutorService}. + */ +public interface ExecutorServiceFactory extends AsyncServiceProvider { + + /** + * Default instance that delegates to {@link Executors#newFixedThreadPool(int, ThreadFactory)} + */ + ExecutorServiceFactory INSTANCE = (threadFactory, concurrency, maxConcurrency) -> + Executors.newFixedThreadPool(maxConcurrency, threadFactory); + + @Override + default void init(AsyncBuilder builder) { + if (builder.executorServiceFactory() == null) { + builder.executorServiceFactory(this); + } + } + + /** + * Create an ExecutorService + * + * @param threadFactory A {@link ThreadFactory} which must be used by the + * created {@link ExecutorService} to create threads. Null + * indicates there is no requirement to use a specific + * factory. + * @param concurrency The target level of concurrency or 0 which indicates + * unspecified + * @param maxConcurrency A hard limit to the level of concurrency required, + * should be greater than {@code concurrency} or 0 which + * indicates unspecified. + * + * @return an {@link ExecutorService} that can be used to run tasks + */ + ExecutorService createExecutor(ThreadFactory threadFactory, Integer concurrency, Integer maxConcurrency); + +} diff --git a/src/main/java/org/xbib/event/async/streams/Pipe.java b/src/main/java/org/xbib/event/async/streams/Pipe.java new file mode 100644 index 0000000..5b4496d --- /dev/null +++ b/src/main/java/org/xbib/event/async/streams/Pipe.java @@ -0,0 +1,68 @@ +package org.xbib.event.async.streams; + +import org.xbib.event.async.Future; + +/** + * Pipe data from a {@link ReadStream} to a {@link WriteStream} and performs flow control where necessary to + * prevent the write stream buffer from getting overfull. + *

+ * Instances of this class read items from a {@link ReadStream} and write them to a {@link WriteStream}. If data + * can be read faster than it can be written this could result in the write queue of the {@link WriteStream} growing + * without bound, eventually causing it to exhaust all available RAM. + *

+ * To prevent this, after each write, instances of this class check whether the write queue of the {@link + * WriteStream} is full, and if so, the {@link ReadStream} is paused, and a {@code drainHandler} is set on the + * {@link WriteStream}. + *

+ * When the {@link WriteStream} has processed half of its backlog, the {@code drainHandler} will be + * called, which results in the pump resuming the {@link ReadStream}. + *

+ * This class can be used to pipe from any {@link ReadStream} to any {@link WriteStream}. + *

+ */ +public interface Pipe { + + /** + * Set to {@code true} to call {@link WriteStream#end()} when the source {@code ReadStream} fails, {@code false} otherwise. + * + * @param end {@code true} to end the stream on a source {@code ReadStream} failure + * @return a reference to this, so the API can be used fluently + */ + Pipe endOnFailure(boolean end); + + /** + * Set to {@code true} to call {@link WriteStream#end()} when the source {@code ReadStream} succeeds, {@code false} otherwise. + * + * @param end {@code true} to end the stream on a source {@code ReadStream} success + * @return a reference to this, so the API can be used fluently + */ + Pipe endOnSuccess(boolean end); + + /** + * Set to {@code true} to call {@link WriteStream#end()} when the source {@code ReadStream} completes, {@code false} otherwise. + *

+ * Calling this overwrites {@link #endOnFailure} and {@link #endOnSuccess}. + * + * @param end {@code true} to end the stream on a source {@code ReadStream} completion + * @return a reference to this, so the API can be used fluently + */ + Pipe endOnComplete(boolean end); + + /** + * Start to pipe the elements to the destination {@code WriteStream}. + *

+ * When the operation fails with a write error, the source stream is resumed. + * + * @param dst the destination write stream + * @return a future notified when the pipe operation completes + */ + Future to(WriteStream dst); + + /** + * Close the pipe. + *

+ * The streams handlers will be unset and the read stream resumed unless it is already ended. + */ + void close(); + +} diff --git a/src/main/java/org/xbib/event/async/streams/ReadStream.java b/src/main/java/org/xbib/event/async/streams/ReadStream.java new file mode 100644 index 0000000..5ddf5db --- /dev/null +++ b/src/main/java/org/xbib/event/async/streams/ReadStream.java @@ -0,0 +1,102 @@ +package org.xbib.event.async.streams; + +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.streams.impl.PipeImpl; + +/** + * Represents a stream of items that can be read from. + *

+ * Any class that implements this interface can be used by a {@link Pipe} to pipe data from it + * to a {@link WriteStream}. + *

+ *

Streaming mode

+ * The stream is either in flowing or fetch mode. + *
    + * Initially the stream is in flowing mode. + *
  • When the stream is in flowing mode, elements are delivered to the {@code handler}.
  • + *
  • When the stream is in fetch mode, only the number of requested elements will be delivered to the {@code handler}.
  • + *
+ * The mode can be changed with the {@link #pause()}, {@link #resume()} and {@link #fetch} methods: + *
    + *
  • Calling {@link #resume()} sets the flowing mode
  • + *
  • Calling {@link #pause()} sets the fetch mode and resets the demand to {@code 0}
  • + *
  • Calling {@link #fetch(long)} requests a specific amount of elements and adds it to the actual demand
  • + *
+ * + */ +public interface ReadStream extends StreamBase { + + /** + * Set an exception handler on the read stream. + * + * @param handler the exception handler + * @return a reference to this, so the API can be used fluently + */ + ReadStream exceptionHandler(Handler handler); + + /** + * Set a data handler. As data is read, the handler will be called with the data. + * + * @return a reference to this, so the API can be used fluently + */ + ReadStream handler(Handler handler); + + /** + * Pause the {@code ReadStream}, it sets the buffer in {@code fetch} mode and clears the actual demand. + *

+ * While it's paused, no data will be sent to the data {@code handler}. + * + * @return a reference to this, so the API can be used fluently + */ + ReadStream pause(); + + /** + * Resume reading, and sets the buffer in {@code flowing} mode. + *

+ * If the {@code ReadStream} has been paused, reading will recommence on it. + * + * @return a reference to this, so the API can be used fluently + */ + ReadStream resume(); + + /** + * Fetch the specified {@code amount} of elements. If the {@code ReadStream} has been paused, reading will + * recommence with the specified {@code amount} of items, otherwise the specified {@code amount} will + * be added to the current stream demand. + * + * @return a reference to this, so the API can be used fluently + */ + ReadStream fetch(long amount); + + /** + * Set an end handler. Once the stream has ended, and there is no more data to be read, this handler will be called. + * + * @return a reference to this, so the API can be used fluently + */ + ReadStream endHandler(Handler endHandler); + + /** + * Pause this stream and return a {@link Pipe} to transfer the elements of this stream to a destination {@link WriteStream}. + *

+ * The stream will be resumed when the pipe will be wired to a {@code WriteStream}. + * + * @return a pipe + */ + default Pipe pipe() { + pause(); + return new PipeImpl<>(this); + } + + /** + * Pipe this {@code ReadStream} to the {@code WriteStream}. + *

+ * Elements emitted by this stream will be written to the write stream until this stream ends or fails. + * + * @param dst the destination write stream + * @return a future notified when the write stream will be ended with the outcome + */ + default Future pipeTo(WriteStream dst) { + return new PipeImpl<>(this).to(dst); + } +} diff --git a/src/main/java/org/xbib/event/async/streams/StreamBase.java b/src/main/java/org/xbib/event/async/streams/StreamBase.java new file mode 100644 index 0000000..7609939 --- /dev/null +++ b/src/main/java/org/xbib/event/async/streams/StreamBase.java @@ -0,0 +1,17 @@ +package org.xbib.event.async.streams; + +import org.xbib.event.async.Handler; + +/** + * Base interface for a stream. + */ +public interface StreamBase { + + /** + * Set an exception handler. + * + * @param handler the handler + * @return a reference to this, so the API can be used fluently + */ + StreamBase exceptionHandler(Handler handler); +} diff --git a/src/main/java/org/xbib/event/async/streams/WriteStream.java b/src/main/java/org/xbib/event/async/streams/WriteStream.java new file mode 100644 index 0000000..a288784 --- /dev/null +++ b/src/main/java/org/xbib/event/async/streams/WriteStream.java @@ -0,0 +1,95 @@ +package org.xbib.event.async.streams; + +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; + +/** + * + * Represents a stream of data that can be written to. + *

+ * Any class that implements this interface can be used by a {@link Pipe} to pipe data from a {@code ReadStream} + * to it. + */ +public interface WriteStream extends StreamBase { + + /** + * Set an exception handler on the write stream. + * + * @param handler the exception handler + * @return a reference to this, so the API can be used fluently + */ + @Override + WriteStream exceptionHandler(Handler handler); + + /** + * Write some data to the stream. + * + *

The data is usually put on an internal write queue, and the write actually happens + * asynchronously. To avoid running out of memory by putting too much on the write queue, + * check the {@link #writeQueueFull} method before writing. This is done automatically if + * using a {@link Pipe}. + * + *

When the {@code data} is moved from the queue to the actual medium, the returned + * {@link Future} will be completed with the write result, e.g the future is succeeded + * when a server HTTP response buffer is written to the socket and failed if the remote + * client has closed the socket while the data was still pending for write. + * + * @param data the data to write + * @return a future completed with the write result + */ + Future write(T data); + + /** + * Ends the stream. + *

+ * Once the stream has ended, it cannot be used any more. + * + * @return a future completed with the result + */ + Future end(); + + /** + * Same as {@link #end()} but writes some data to the stream before ending. + * + * @implSpec The default default implementation calls sequentially {@link #write(Object)} then {@link #end()} + * @apiNote Implementations might want to perform a single operation + * @param data the data to write + * @return a future completed with the result + */ + default Future end(T data) { + return write(data).compose(v -> end()); + } + + /** + * Set the maximum size of the write queue to {@code maxSize}. You will still be able to write to the stream even + * if there is more than {@code maxSize} items in the write queue. This is used as an indicator by classes such as + * {@link Pipe} to provide flow control. + *

+ * The value is defined by the implementation of the stream. + * + * @param maxSize the max size of the write stream + * @return a reference to this, so the API can be used fluently + */ + WriteStream setWriteQueueMaxSize(int maxSize); + + /** + * This will return {@code true} if there are more bytes in the write queue than the value set using {@link + * #setWriteQueueMaxSize} + * + * @return {@code true} if write queue is full + */ + boolean writeQueueFull(); + + /** + * Set a drain handler on the stream. If the write queue is full, then the handler will be called when the write + * queue is ready to accept buffers again. See {@link Pipe} for an example of this being used. + * + *

The stream implementation defines when the drain handler, for example it could be when the queue size has been + * reduced to {@code maxSize / 2}. + * + * @param handler the handler + * @return a reference to this, so the API can be used fluently + */ + WriteStream drainHandler(Handler handler); + +} diff --git a/src/main/java/org/xbib/event/async/streams/impl/PipeImpl.java b/src/main/java/org/xbib/event/async/streams/impl/PipeImpl.java new file mode 100644 index 0000000..0d4ee6e --- /dev/null +++ b/src/main/java/org/xbib/event/async/streams/impl/PipeImpl.java @@ -0,0 +1,141 @@ +package org.xbib.event.async.streams.impl; + +import org.xbib.event.async.AsyncResult; +import org.xbib.event.async.Future; +import org.xbib.event.async.Handler; +import org.xbib.event.async.Promise; +import org.xbib.event.async.EventException; +import org.xbib.event.async.streams.Pipe; +import org.xbib.event.async.streams.ReadStream; +import org.xbib.event.async.streams.WriteStream; + +public class PipeImpl implements Pipe { + + private final Promise result; + private final ReadStream src; + private boolean endOnSuccess = true; + private boolean endOnFailure = true; + private WriteStream dst; + + public PipeImpl(ReadStream src) { + this.src = src; + this.result = Promise.promise(); + + // Set handlers now + src.endHandler(result::tryComplete); + src.exceptionHandler(result::tryFail); + } + + @Override + public synchronized Pipe endOnFailure(boolean end) { + endOnFailure = end; + return this; + } + + @Override + public synchronized Pipe endOnSuccess(boolean end) { + endOnSuccess = end; + return this; + } + + @Override + public synchronized Pipe endOnComplete(boolean end) { + endOnSuccess = end; + endOnFailure = end; + return this; + } + + private void handleWriteResult(AsyncResult ack) { + if (ack.failed()) { + result.tryFail(new WriteException(ack.cause())); + } + } + + @Override + public Future to(WriteStream ws) { + Promise promise = Promise.promise(); + if (ws == null) { + throw new NullPointerException(); + } + synchronized (PipeImpl.this) { + if (dst != null) { + throw new IllegalStateException(); + } + dst = ws; + } + Handler drainHandler = v -> src.resume(); + src.handler(item -> { + ws.write(item).onComplete(this::handleWriteResult); + if (ws.writeQueueFull()) { + src.pause(); + ws.drainHandler(drainHandler); + } + }); + src.resume(); + result.future().onComplete(ar -> { + try { + src.handler(null); + } catch (Exception ignore) { + } + try { + src.exceptionHandler(null); + } catch (Exception ignore) { + } + try { + src.endHandler(null); + } catch (Exception ignore) { + } + if (ar.succeeded()) { + handleSuccess(promise); + } else { + Throwable err = ar.cause(); + if (err instanceof WriteException) { + src.resume(); + err = err.getCause(); + } + handleFailure(err, promise); + } + }); + return promise.future(); + } + + private void handleSuccess(Promise promise) { + if (endOnSuccess) { + dst.end().onComplete(promise); + } else { + promise.complete(); + } + } + + private void handleFailure(Throwable cause, Promise completionHandler) { + if (endOnFailure){ + dst + .end() + .transform(ar -> Future.failedFuture(cause)) + .onComplete(completionHandler); + } else { + completionHandler.fail(cause); + } + } + + public void close() { + synchronized (this) { + src.exceptionHandler(null); + src.handler(null); + if (dst != null) { + dst.drainHandler(null); + dst.exceptionHandler(null); + } + } + EventException err = new EventException("Pipe closed", true); + if (result.tryFail(err)) { + src.resume(); + } + } + + private static class WriteException extends EventException { + private WriteException(Throwable cause) { + super(cause, true); + } + } +} diff --git a/src/main/java/org/xbib/event/async/AbstractAsyncFileReaderLines.java b/src/main/java/org/xbib/event/io/AbstractAsyncFileReaderLines.java similarity index 99% rename from src/main/java/org/xbib/event/async/AbstractAsyncFileReaderLines.java rename to src/main/java/org/xbib/event/io/AbstractAsyncFileReaderLines.java index 188b559..4ef887d 100644 --- a/src/main/java/org/xbib/event/async/AbstractAsyncFileReaderLines.java +++ b/src/main/java/org/xbib/event/io/AbstractAsyncFileReaderLines.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/src/main/java/org/xbib/event/async/AddOnComplete.java b/src/main/java/org/xbib/event/io/AddOnComplete.java similarity index 92% rename from src/main/java/org/xbib/event/async/AddOnComplete.java rename to src/main/java/org/xbib/event/io/AddOnComplete.java index ecf9151..601f7a6 100644 --- a/src/main/java/org/xbib/event/async/AddOnComplete.java +++ b/src/main/java/org/xbib/event/io/AddOnComplete.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; diff --git a/src/main/java/org/xbib/event/async/AddOnError.java b/src/main/java/org/xbib/event/io/AddOnError.java similarity index 95% rename from src/main/java/org/xbib/event/async/AddOnError.java rename to src/main/java/org/xbib/event/io/AddOnError.java index 7c40c29..ce13ac8 100644 --- a/src/main/java/org/xbib/event/async/AddOnError.java +++ b/src/main/java/org/xbib/event/io/AddOnError.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; diff --git a/src/main/java/org/xbib/event/async/AddOnNext.java b/src/main/java/org/xbib/event/io/AddOnNext.java similarity index 92% rename from src/main/java/org/xbib/event/async/AddOnNext.java rename to src/main/java/org/xbib/event/io/AddOnNext.java index 2e27ac3..c8b6638 100644 --- a/src/main/java/org/xbib/event/async/AddOnNext.java +++ b/src/main/java/org/xbib/event/io/AddOnNext.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; diff --git a/src/main/java/org/xbib/event/async/AddOnSubscribe.java b/src/main/java/org/xbib/event/io/AddOnSubscribe.java similarity index 94% rename from src/main/java/org/xbib/event/async/AddOnSubscribe.java rename to src/main/java/org/xbib/event/io/AddOnSubscribe.java index ee1a509..af6967b 100644 --- a/src/main/java/org/xbib/event/async/AddOnSubscribe.java +++ b/src/main/java/org/xbib/event/io/AddOnSubscribe.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; diff --git a/src/main/java/org/xbib/event/async/AsyncFileQuery.java b/src/main/java/org/xbib/event/io/AsyncFileQuery.java similarity index 98% rename from src/main/java/org/xbib/event/async/AsyncFileQuery.java rename to src/main/java/org/xbib/event/io/AsyncFileQuery.java index 31a1bfa..d7dd29b 100644 --- a/src/main/java/org/xbib/event/async/AsyncFileQuery.java +++ b/src/main/java/org/xbib/event/io/AsyncFileQuery.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.xbib.event.yield.AsyncQuery; diff --git a/src/main/java/org/xbib/event/async/AsyncFileReaderBytes.java b/src/main/java/org/xbib/event/io/AsyncFileReaderBytes.java similarity index 98% rename from src/main/java/org/xbib/event/async/AsyncFileReaderBytes.java rename to src/main/java/org/xbib/event/io/AsyncFileReaderBytes.java index 7f6b429..ae621b6 100644 --- a/src/main/java/org/xbib/event/async/AsyncFileReaderBytes.java +++ b/src/main/java/org/xbib/event/io/AsyncFileReaderBytes.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/src/main/java/org/xbib/event/async/AsyncFileReaderLines.java b/src/main/java/org/xbib/event/io/AsyncFileReaderLines.java similarity index 99% rename from src/main/java/org/xbib/event/async/AsyncFileReaderLines.java rename to src/main/java/org/xbib/event/io/AsyncFileReaderLines.java index 0b9b1ca..8b9e3a2 100644 --- a/src/main/java/org/xbib/event/async/AsyncFileReaderLines.java +++ b/src/main/java/org/xbib/event/io/AsyncFileReaderLines.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; diff --git a/src/main/java/org/xbib/event/async/AsyncFileWriter.java b/src/main/java/org/xbib/event/io/AsyncFileWriter.java similarity index 99% rename from src/main/java/org/xbib/event/async/AsyncFileWriter.java rename to src/main/java/org/xbib/event/io/AsyncFileWriter.java index 9b79e15..145658e 100644 --- a/src/main/java/org/xbib/event/async/AsyncFileWriter.java +++ b/src/main/java/org/xbib/event/io/AsyncFileWriter.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import java.io.IOException; import java.nio.ByteBuffer; diff --git a/src/main/java/org/xbib/event/async/AsyncFiles.java b/src/main/java/org/xbib/event/io/AsyncFiles.java similarity index 99% rename from src/main/java/org/xbib/event/async/AsyncFiles.java rename to src/main/java/org/xbib/event/io/AsyncFiles.java index 61b6bfe..229283c 100644 --- a/src/main/java/org/xbib/event/async/AsyncFiles.java +++ b/src/main/java/org/xbib/event/io/AsyncFiles.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Publisher; import org.xbib.event.yield.AsyncQuery; diff --git a/src/main/java/org/xbib/event/async/EmptySubscriber.java b/src/main/java/org/xbib/event/io/EmptySubscriber.java similarity index 94% rename from src/main/java/org/xbib/event/async/EmptySubscriber.java rename to src/main/java/org/xbib/event/io/EmptySubscriber.java index 0d7c882..cf99de3 100644 --- a/src/main/java/org/xbib/event/async/EmptySubscriber.java +++ b/src/main/java/org/xbib/event/io/EmptySubscriber.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscription; diff --git a/src/main/java/org/xbib/event/async/SubscriberBuilder.java b/src/main/java/org/xbib/event/io/SubscriberBuilder.java similarity index 97% rename from src/main/java/org/xbib/event/async/SubscriberBuilder.java rename to src/main/java/org/xbib/event/io/SubscriberBuilder.java index a3753fe..dadb044 100644 --- a/src/main/java/org/xbib/event/async/SubscriberBuilder.java +++ b/src/main/java/org/xbib/event/io/SubscriberBuilder.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; diff --git a/src/main/java/org/xbib/event/async/Subscribers.java b/src/main/java/org/xbib/event/io/Subscribers.java similarity index 87% rename from src/main/java/org/xbib/event/async/Subscribers.java rename to src/main/java/org/xbib/event/io/Subscribers.java index c31bb11..fbd8614 100644 --- a/src/main/java/org/xbib/event/async/Subscribers.java +++ b/src/main/java/org/xbib/event/io/Subscribers.java @@ -1,4 +1,4 @@ -package org.xbib.event.async; +package org.xbib.event.io; import java.util.function.Consumer; diff --git a/src/main/java/org/xbib/event/loop/AbstractEventExecutor.java b/src/main/java/org/xbib/event/loop/AbstractEventExecutor.java new file mode 100644 index 0000000..5dd7817 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/AbstractEventExecutor.java @@ -0,0 +1,181 @@ +package org.xbib.event.loop; + +import org.xbib.event.DefaultProgressivePromise; +import org.xbib.event.DefaultPromise; +import org.xbib.event.FailedFuture; +import org.xbib.event.Future; +import org.xbib.event.ProgressivePromise; +import org.xbib.event.Promise; +import org.xbib.event.PromiseTask; +import org.xbib.event.ScheduledFuture; +import org.xbib.event.SucceededFuture; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.Callable; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Abstract base class for {@link EventExecutor} implementations. + */ +public abstract class AbstractEventExecutor extends AbstractExecutorService implements EventExecutor { + private static final Logger logger = Logger.getLogger(AbstractEventExecutor.class.getName()); + + static final long DEFAULT_SHUTDOWN_QUIET_PERIOD = 2; + static final long DEFAULT_SHUTDOWN_TIMEOUT = 15; + + private final EventExecutorGroup parent; + private final Collection selfCollection = Collections.singleton(this); + + protected AbstractEventExecutor() { + this(null); + } + + protected AbstractEventExecutor(EventExecutorGroup parent) { + this.parent = parent; + } + + @Override + public EventExecutorGroup parent() { + return parent; + } + + @Override + public EventExecutor next() { + return this; + } + + @Override + public boolean inEventLoop() { + return inEventLoop(Thread.currentThread()); + } + + @Override + public Iterator iterator() { + return selfCollection.iterator(); + } + + @Override + public Future shutdownGracefully() { + return shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS); + } + + /** + * @deprecated {@link #shutdownGracefully(long, long, TimeUnit)} or {@link #shutdownGracefully()} instead. + */ + @Override + @Deprecated + public abstract void shutdown(); + + /** + * @deprecated {@link #shutdownGracefully(long, long, TimeUnit)} or {@link #shutdownGracefully()} instead. + */ + @Override + @Deprecated + public List shutdownNow() { + shutdown(); + return Collections.emptyList(); + } + + @Override + public Promise newPromise() { + return new DefaultPromise(this); + } + + @Override + public ProgressivePromise newProgressivePromise() { + return new DefaultProgressivePromise(this); + } + + @Override + public Future newSucceededFuture(V result) { + return new SucceededFuture(this, result); + } + + @Override + public Future newFailedFuture(Throwable cause) { + return new FailedFuture(this, cause); + } + + @Override + public Future submit(Runnable task) { + return (Future) super.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return (Future) super.submit(task, result); + } + + @Override + public Future submit(Callable task) { + return (Future) super.submit(task); + } + + @Override + protected final RunnableFuture newTaskFor(Runnable runnable, T value) { + return new PromiseTask(this, runnable, value); + } + + @Override + protected final RunnableFuture newTaskFor(Callable callable) { + return new PromiseTask(this, callable); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, + TimeUnit unit) { + throw new UnsupportedOperationException(); + } + + @Override + public ScheduledFuture schedule(Callable 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(); + } + + /** + * Try to execute the given {@link Runnable} and just log if it throws a {@link Throwable}. + */ + protected static void safeExecute(Runnable task) { + try { + task.run(); + } catch (Throwable t) { + logger.log(Level.WARNING, "A task raised an exception. Task: " + task, t); + } + } + + /** + * Like {@link #execute(Runnable)} but does not guarantee the task will be run until either + * a non-lazy task is executed or the executor is shut down. + * + * This is equivalent to submitting a {@link AbstractEventExecutor.LazyRunnable} to + * {@link #execute(Runnable)} but for an arbitrary {@link Runnable}. + * + * The default implementation just delegates to {@link #execute(Runnable)}. + */ + public void lazyExecute(Runnable task) { + execute(task); + } + + /** + * Marker interface for {@link Runnable} to indicate that it should be queued for execution + * but does not need to run immediately. + */ + public interface LazyRunnable extends Runnable { } +} diff --git a/src/main/java/org/xbib/event/loop/AbstractEventExecutorGroup.java b/src/main/java/org/xbib/event/loop/AbstractEventExecutorGroup.java new file mode 100644 index 0000000..bc24b69 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/AbstractEventExecutorGroup.java @@ -0,0 +1,85 @@ +package org.xbib.event.loop; + +import org.xbib.event.Future; +import org.xbib.event.ScheduledFuture; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + + +/** + * Abstract base class for {@link EventExecutorGroup} implementations. + */ +public abstract class AbstractEventExecutorGroup implements EventExecutorGroup { + @Override + public Future submit(Runnable task) { + return next().submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + return next().submit(task, result); + } + + @Override + public Future submit(Callable task) { + return next().submit(task); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return next().schedule(command, delay, unit); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + return next().schedule(callable, delay, unit); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + return next().scheduleAtFixedRate(command, initialDelay, period, unit); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + return next().scheduleWithFixedDelay(command, initialDelay, delay, unit); + } + + @Override + public Future shutdownGracefully() { + return shutdownGracefully(2, 15, TimeUnit.SECONDS); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return next().invokeAll(tasks); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + return next().invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + return next().invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return next().invokeAny(tasks, timeout, unit); + } + + @Override + public void execute(Runnable command) { + next().execute(command); + } +} diff --git a/src/main/java/org/xbib/event/loop/AbstractScheduledEventExecutor.java b/src/main/java/org/xbib/event/loop/AbstractScheduledEventExecutor.java new file mode 100644 index 0000000..058dead --- /dev/null +++ b/src/main/java/org/xbib/event/loop/AbstractScheduledEventExecutor.java @@ -0,0 +1,271 @@ +package org.xbib.event.loop; + +import org.xbib.event.ScheduledFuture; +import org.xbib.event.ScheduledFutureTask; +import org.xbib.event.util.DefaultPriorityQueue; +import org.xbib.event.util.PriorityQueue; + +import java.util.Comparator; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.xbib.event.ScheduledFutureTask.deadlineNanos; + +/** + * 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 = + new Comparator>() { + @Override + public int compare(ScheduledFutureTask o1, ScheduledFutureTask o2) { + return o1.compareTo(o2); + } + }; + + static final Runnable WAKEUP_TASK = new Runnable() { + @Override + public void run() { } // Do nothing + }; + + PriorityQueue> scheduledTaskQueue; + + long nextTaskId; + + protected AbstractScheduledEventExecutor() { + } + + protected AbstractScheduledEventExecutor(EventExecutorGroup parent) { + super(parent); + } + + protected static long nanoTime() { + return ScheduledFutureTask.nanoTime(); + } + + /** + * Given an arbitrary deadline {@code deadlineNanos}, calculate the number of nano seconds from now + * {@code deadlineNanos} would expire. + * @param deadlineNanos An arbitrary deadline in nano seconds. + * @return the number of nano seconds from now {@code deadlineNanos} would expire. + */ + protected static long deadlineToDelayNanos(long deadlineNanos) { + return ScheduledFutureTask.deadlineToDelayNanos(deadlineNanos); + } + + /** + * The initial value used for delay and computations based upon a monatomic time source. + * @return initial value used for delay and computations based upon a monatomic time source. + */ + protected static long initialNanoTime() { + return ScheduledFutureTask.initialNanoTime(); + } + + public PriorityQueue> scheduledTaskQueue() { + if (scheduledTaskQueue == null) { + scheduledTaskQueue = new DefaultPriorityQueue>( + SCHEDULED_FUTURE_TASK_COMPARATOR, + // Use same initial capacity as java.util.PriorityQueue + 11); + } + return scheduledTaskQueue; + } + + private static boolean isNullOrEmpty(Queue> queue) { + return queue == null || queue.isEmpty(); + } + + /** + * Cancel all scheduled tasks. + * + * This method MUST be called only when {@link #inEventLoop()} is {@code true}. + */ + protected void cancelScheduledTasks() { + assert inEventLoop(); + PriorityQueue> scheduledTaskQueue = this.scheduledTaskQueue; + if (isNullOrEmpty(scheduledTaskQueue)) { + return; + } + + final ScheduledFutureTask[] scheduledTasks = + scheduledTaskQueue.toArray(new ScheduledFutureTask[0]); + + for (ScheduledFutureTask task: scheduledTasks) { + task.cancelWithoutRemove(false); + } + + scheduledTaskQueue.clearIgnoringIndexes(); + } + + /** + * @see #pollScheduledTask(long) + */ + protected final Runnable pollScheduledTask() { + return pollScheduledTask(nanoTime()); + } + + /** + * Return the {@link Runnable} which is ready to be executed with the given {@code nanoTime}. + * You should use {@link #nanoTime()} to retrieve the correct {@code nanoTime}. + */ + protected final Runnable pollScheduledTask(long nanoTime) { + assert inEventLoop(); + + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) { + return null; + } + scheduledTaskQueue.remove(); + scheduledTask.setConsumed(); + return scheduledTask; + } + + /** + * Return the nanoseconds until the next scheduled task is ready to be run or {@code -1} if no task is scheduled. + */ + protected final long nextScheduledTaskNano() { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + return scheduledTask != null ? scheduledTask.delayNanos() : -1; + } + + /** + * Return the deadline (in nanoseconds) when the next scheduled task is ready to be run or {@code -1} + * if no task is scheduled. + */ + protected final long nextScheduledTaskDeadlineNanos() { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + return scheduledTask != null ? scheduledTask.deadlineNanos() : -1; + } + + final ScheduledFutureTask peekScheduledTask() { + Queue> scheduledTaskQueue = this.scheduledTaskQueue; + return scheduledTaskQueue != null ? scheduledTaskQueue.peek() : null; + } + + /** + * Returns {@code true} if a scheduled task is ready for processing. + */ + protected final boolean hasScheduledTasks() { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + return scheduledTask != null && scheduledTask.deadlineNanos() <= nanoTime(); + } + + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + Objects.requireNonNull(command, "command"); + Objects.requireNonNull(unit, "unit"); + if (delay < 0) { + delay = 0; + } + return schedule(new ScheduledFutureTask( + this, + command, + deadlineNanos(unit.toNanos(delay)))); + } + + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + Objects.requireNonNull(callable, "callable"); + Objects.requireNonNull(unit, "unit"); + if (delay < 0) { + delay = 0; + } + return schedule(new ScheduledFutureTask(this, callable, deadlineNanos(unit.toNanos(delay)))); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { + Objects.requireNonNull(command, "command"); + Objects.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)); + } + return schedule(new ScheduledFutureTask( + this, command, deadlineNanos(unit.toNanos(initialDelay)), unit.toNanos(period))); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { + Objects.requireNonNull(command, "command"); + Objects.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)); + } + return schedule(new ScheduledFutureTask( + this, command, deadlineNanos(unit.toNanos(initialDelay)), -unit.toNanos(delay))); + } + + public final void scheduleFromEventLoop(final ScheduledFutureTask task) { + // nextTaskId a long and so there is no chance it will overflow back to 0 + scheduledTaskQueue().add(task.setId(++nextTaskId)); + } + + private ScheduledFuture schedule(final ScheduledFutureTask task) { + if (inEventLoop()) { + scheduleFromEventLoop(task); + } else { + final long deadlineNanos = task.deadlineNanos(); + // task will add itself to scheduled task queue when run if not expired + if (beforeScheduledTaskSubmitted(deadlineNanos)) { + execute(task); + } else { + lazyExecute(task); + // Second hook after scheduling to facilitate race-avoidance + if (afterScheduledTaskSubmitted(deadlineNanos)) { + execute(WAKEUP_TASK); + } + } + } + + return task; + } + + public final void removeScheduled(final ScheduledFutureTask task) { + assert task.isCancelled(); + if (inEventLoop()) { + scheduledTaskQueue().removeTyped(task); + } else { + // task will remove itself from scheduled task queue when it runs + lazyExecute(task); + } + } + + /** + * Called from arbitrary non-{@link EventExecutor} threads prior to scheduled task submission. + * Returns {@code true} if the {@link EventExecutor} thread should be woken immediately to + * process the scheduled task (if not already awake). + *

+ * If {@code false} is returned, {@link #afterScheduledTaskSubmitted(long)} will be called with + * the same value after the scheduled task is enqueued, providing another opportunity + * to wake the {@link EventExecutor} thread if required. + * + * @param deadlineNanos deadline of the to-be-scheduled task + * relative to {@link AbstractScheduledEventExecutor#nanoTime()} + * @return {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise + */ + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + return true; + } + + /** + * See {@link #beforeScheduledTaskSubmitted(long)}. Called only after that method returns false. + * + * @param deadlineNanos relative to {@link AbstractScheduledEventExecutor#nanoTime()} + * @return {@code true} if the {@link EventExecutor} thread should be woken, {@code false} otherwise + */ + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + return true; + } +} diff --git a/src/main/java/org/xbib/event/loop/BlockingOperationException.java b/src/main/java/org/xbib/event/loop/BlockingOperationException.java new file mode 100644 index 0000000..b1e3e99 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/BlockingOperationException.java @@ -0,0 +1,24 @@ +package org.xbib.event.loop; + +/** + * An {@link IllegalStateException} which is raised when a user performed a blocking operation + * when the user is in an event loop thread. If a blocking operation is performed in an event loop + * thread, the blocking operation will most likely enter a dead lock state, hence throwing this + * exception. + */ +public class BlockingOperationException extends IllegalStateException { + + public BlockingOperationException() { } + + public BlockingOperationException(String s) { + super(s); + } + + public BlockingOperationException(Throwable cause) { + super(cause); + } + + public BlockingOperationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/xbib/event/loop/DefaultEventExecutorChooserFactory.java b/src/main/java/org/xbib/event/loop/DefaultEventExecutorChooserFactory.java new file mode 100644 index 0000000..497c92a --- /dev/null +++ b/src/main/java/org/xbib/event/loop/DefaultEventExecutorChooserFactory.java @@ -0,0 +1,58 @@ +package org.xbib.event.loop; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Default implementation which uses simple round-robin to choose next {@link EventExecutor}. + */ +public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory { + + public static final DefaultEventExecutorChooserFactory INSTANCE = new DefaultEventExecutorChooserFactory(); + + private DefaultEventExecutorChooserFactory() { } + + @Override + public EventExecutorChooser newChooser(EventExecutor[] executors) { + if (isPowerOfTwo(executors.length)) { + return new PowerOfTwoEventExecutorChooser(executors); + } else { + return new GenericEventExecutorChooser(executors); + } + } + + private static boolean isPowerOfTwo(int val) { + return (val & -val) == val; + } + + private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser { + private final AtomicInteger idx = new AtomicInteger(); + private final EventExecutor[] executors; + + PowerOfTwoEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[idx.getAndIncrement() & executors.length - 1]; + } + } + + private static final class GenericEventExecutorChooser implements EventExecutorChooser { + // Use a 'long' counter to avoid non-round-robin behaviour at the 32-bit overflow boundary. + // The 64-bit long solves this by placing the overflow so far into the future that no system + // will encounter this in practice. + private final AtomicLong idx = new AtomicLong(); + private final EventExecutor[] executors; + + GenericEventExecutorChooser(EventExecutor[] executors) { + this.executors = executors; + } + + @Override + public EventExecutor next() { + return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)]; + } + } +} diff --git a/src/main/java/org/xbib/event/loop/EventExecutor.java b/src/main/java/org/xbib/event/loop/EventExecutor.java new file mode 100644 index 0000000..ec41e5a --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventExecutor.java @@ -0,0 +1,63 @@ + +package org.xbib.event.loop; + +import org.xbib.event.Future; +import org.xbib.event.FutureListener; +import org.xbib.event.ProgressivePromise; +import org.xbib.event.Promise; + +/** + * The {@link EventExecutor} is a special {@link EventExecutorGroup} which comes + * with some handy methods to see if a {@link Thread} is executed in a event loop. + * Besides this, it also extends the {@link EventExecutorGroup} to allow for a generic + * way to access methods. + * + */ +public interface EventExecutor extends EventExecutorGroup { + + /** + * Returns a reference to itself. + */ + @Override + EventExecutor next(); + + /** + * Return the {@link EventExecutorGroup} which is the parent of this {@link EventExecutor}, + */ + EventExecutorGroup parent(); + + /** + * Calls {@link #inEventLoop(Thread)} with {@link Thread#currentThread()} as argument + */ + boolean inEventLoop(); + + /** + * Return {@code true} if the given {@link Thread} is executed in the event loop, + * {@code false} otherwise. + */ + boolean inEventLoop(Thread thread); + + /** + * Return a new {@link Promise}. + */ + Promise newPromise(); + + /** + * Create a new {@link ProgressivePromise}. + */ + ProgressivePromise newProgressivePromise(); + + /** + * Create a new {@link Future} which is marked as succeeded already. So {@link Future#isSuccess()} + * will return {@code true}. All {@link FutureListener} added to it will be notified directly. Also + * every call of blocking methods will just return without blocking. + */ + Future newSucceededFuture(V result); + + /** + * Create a new {@link Future} which is marked as failed already. So {@link Future#isSuccess()} + * will return {@code false}. All {@link FutureListener} added to it will be notified directly. Also + * every call of blocking methods will just return without blocking. + */ + Future newFailedFuture(Throwable cause); +} diff --git a/src/main/java/org/xbib/event/loop/EventExecutorChooserFactory.java b/src/main/java/org/xbib/event/loop/EventExecutorChooserFactory.java new file mode 100644 index 0000000..cc849fa --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventExecutorChooserFactory.java @@ -0,0 +1,23 @@ +package org.xbib.event.loop; + +/** + * Factory that creates new {@link EventExecutorChooser}s. + */ +public interface EventExecutorChooserFactory { + + /** + * Returns a new {@link EventExecutorChooser}. + */ + EventExecutorChooser newChooser(EventExecutor[] executors); + + /** + * Chooses the next {@link EventExecutor} to use. + */ + interface EventExecutorChooser { + + /** + * Returns the new {@link EventExecutor} to use. + */ + EventExecutor next(); + } +} diff --git a/src/main/java/org/xbib/event/loop/EventExecutorGroup.java b/src/main/java/org/xbib/event/loop/EventExecutorGroup.java new file mode 100644 index 0000000..b2f6589 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventExecutorGroup.java @@ -0,0 +1,82 @@ +package org.xbib.event.loop; + +import org.xbib.event.Future; +import org.xbib.event.ScheduledFuture; + +import java.util.Iterator; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * The {@link EventExecutorGroup} is responsible for providing the {@link EventExecutor}'s to use + * via its {@link #next()} method. Besides this, it is also responsible for handling their + * life-cycle and allows shutting them down in a global fashion. + * + */ +public interface EventExecutorGroup extends ScheduledExecutorService, Iterable { + + /** + * Returns {@code true} if and only if all {@link EventExecutor}s managed by this {@link EventExecutorGroup} + * are being {@linkplain #shutdownGracefully() shut down gracefully} or was {@linkplain #isShutdown() shut down}. + */ + boolean isShuttingDown(); + + /** + * Shortcut method for {@link #shutdownGracefully(long, long, TimeUnit)} with sensible default values. + * + * @return the {@link #terminationFuture()} + */ + Future shutdownGracefully(); + + /** + * Signals this executor that the caller wants the executor to be shut down. Once this method is called, + * {@link #isShuttingDown()} starts to return {@code true}, and the executor prepares to shut itself down. + * Unlike {@link #shutdown()}, graceful shutdown ensures that no tasks are submitted for 'the quiet period' + * (usually a couple seconds) before it shuts itself down. If a task is submitted during the quiet period, + * it is guaranteed to be accepted and the quiet period will start over. + * + * @param quietPeriod the quiet period as described in the documentation + * @param timeout the maximum amount of time to wait until the executor is {@linkplain #shutdown()} + * regardless if a task was submitted during the quiet period + * @param unit the unit of {@code quietPeriod} and {@code timeout} + * + * @return the {@link #terminationFuture()} + */ + Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit); + + /** + * Returns the {@link Future} which is notified when all {@link EventExecutor}s managed by this + * {@link EventExecutorGroup} have been terminated. + */ + Future terminationFuture(); + + /** + * Returns one of the {@link EventExecutor}s managed by this {@link EventExecutorGroup}. + */ + EventExecutor next(); + + @Override + Iterator iterator(); + + @Override + Future submit(Runnable task); + + @Override + Future submit(Runnable task, T result); + + @Override + Future submit(Callable task); + + @Override + ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit); + + @Override + ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit); + + @Override + ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit); + + @Override + ScheduledFuture scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit); +} diff --git a/src/main/java/org/xbib/event/loop/EventLoop.java b/src/main/java/org/xbib/event/loop/EventLoop.java new file mode 100644 index 0000000..0392809 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventLoop.java @@ -0,0 +1,6 @@ + +package org.xbib.event.loop; + +public interface EventLoop extends OrderedEventExecutor, EventLoopGroup { + EventLoopGroup parent(); +} diff --git a/src/main/java/org/xbib/event/loop/EventLoopException.java b/src/main/java/org/xbib/event/loop/EventLoopException.java new file mode 100644 index 0000000..f3df186 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventLoopException.java @@ -0,0 +1,24 @@ +package org.xbib.event.loop; + +/** + * Special {@link RuntimeException} which will be thrown by {@link EventLoop} and {@link EventLoopGroup} + * implementations when an error occurs. + */ +public class EventLoopException extends RuntimeException { + + public EventLoopException() { + } + + public EventLoopException(String message, Throwable cause) { + super(message, cause); + } + + public EventLoopException(String message) { + super(message); + } + + public EventLoopException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/org/xbib/event/loop/EventLoopGroup.java b/src/main/java/org/xbib/event/loop/EventLoopGroup.java new file mode 100644 index 0000000..d075802 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventLoopGroup.java @@ -0,0 +1,7 @@ +package org.xbib.event.loop; + +public interface EventLoopGroup extends EventExecutorGroup { + + EventLoop next(); + +} diff --git a/src/main/java/org/xbib/event/loop/EventLoopTaskQueueFactory.java b/src/main/java/org/xbib/event/loop/EventLoopTaskQueueFactory.java new file mode 100644 index 0000000..83b58c3 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/EventLoopTaskQueueFactory.java @@ -0,0 +1,20 @@ +package org.xbib.event.loop; + +import java.util.Queue; + +/** + * Factory used to create {@link Queue} instances that will be used to store tasks for an {@link EventLoop}. + * + * Generally speaking the returned {@link Queue} MUST be thread-safe and depending on the {@link EventLoop} + * implementation must be of type {@link java.util.concurrent.BlockingQueue}. + */ +public interface EventLoopTaskQueueFactory { + + /** + * Returns a new {@link Queue} to use. + * @param maxCapacity the maximum amount of elements that can be stored in the {@link Queue} at a given point + * in time. + * @return the new queue. + */ + Queue newTaskQueue(int maxCapacity); +} diff --git a/src/main/java/org/xbib/event/loop/GlobalEventExecutor.java b/src/main/java/org/xbib/event/loop/GlobalEventExecutor.java new file mode 100644 index 0000000..c41782e --- /dev/null +++ b/src/main/java/org/xbib/event/loop/GlobalEventExecutor.java @@ -0,0 +1,265 @@ +package org.xbib.event.loop; + +import org.xbib.event.FailedFuture; +import org.xbib.event.Future; +import org.xbib.event.ScheduledFutureTask; +import org.xbib.event.thread.DefaultThreadFactory; +import org.xbib.event.thread.ThreadExecutorMap; + +import java.util.Objects; +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; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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 Logger logger = Logger.getLogger(GlobalEventExecutor.class.getName()); + + private static final long SCHEDULE_QUIET_PERIOD_INTERVAL = TimeUnit.SECONDS.toNanos(1); + + public static final GlobalEventExecutor INSTANCE = new GlobalEventExecutor(); + + final BlockingQueue taskQueue = new LinkedBlockingQueue(); + final ScheduledFutureTask quietPeriodTask = new ScheduledFutureTask( + this, Executors.callable(new Runnable() { + @Override + public void run() { + // NOOP + } + }, null), ScheduledFutureTask.deadlineNanos(SCHEDULE_QUIET_PERIOD_INTERVAL), -SCHEDULE_QUIET_PERIOD_INTERVAL); + + // 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 terminationFuture = new FailedFuture(this, new UnsupportedOperationException()); + + private GlobalEventExecutor() { + scheduledTaskQueue().add(quietPeriodTask); + 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. + */ + Runnable takeTask() { + BlockingQueue taskQueue = this.taskQueue; + for (;;) { + ScheduledFutureTask 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) { + taskQueue.add(Objects.requireNonNull(task, "task")); + } + + @Override + public boolean inEventLoop(Thread thread) { + return thread == this.thread; + } + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { + return terminationFuture(); + } + + @Override + public Future terminationFuture() { + return terminationFuture; + } + + @Override + @Deprecated + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @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 after 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 { + Objects.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) { + addTask(Objects.requireNonNull(task, "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 + t.setContextClassLoader(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.log(Level.WARNING, "Unexpected exception from the global event executor: ", t); + } + + if (task != quietPeriodTask) { + continue; + } + } + + Queue> scheduledTaskQueue = GlobalEventExecutor.this.scheduledTaskQueue; + // Terminate if there is no task in the queue (except the noop task). + if (taskQueue.isEmpty() && (scheduledTaskQueue == null || 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; + + // Check if there are pending entries added by execute() or schedule*() while we do CAS above. + // 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. + } + } + } + } +} diff --git a/src/main/java/org/xbib/event/loop/MultithreadEventExecutorGroup.java b/src/main/java/org/xbib/event/loop/MultithreadEventExecutorGroup.java new file mode 100644 index 0000000..f0c4172 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/MultithreadEventExecutorGroup.java @@ -0,0 +1,220 @@ +package org.xbib.event.loop; + +import org.xbib.event.DefaultPromise; +import org.xbib.event.Future; +import org.xbib.event.FutureListener; +import org.xbib.event.Promise; +import org.xbib.event.thread.DefaultThreadFactory; +import org.xbib.event.thread.ThreadPerTaskExecutor; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Abstract base class for {@link EventExecutorGroup} implementations that handles their tasks with multiple threads at + * the same time. + */ +public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup { + + private final EventExecutor[] children; + private final Set readonlyChildren; + private final AtomicInteger terminatedChildren = new AtomicInteger(); + private final Promise terminationFuture = new DefaultPromise<>(GlobalEventExecutor.INSTANCE); + private final EventExecutorChooserFactory.EventExecutorChooser chooser; + + /** + * Create a new instance. + * + * @param nThreads the number of threads that will be used by this instance. + * @param threadFactory the ThreadFactory to use, or {@code null} if the default should be used. + * @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call + */ + protected MultithreadEventExecutorGroup(int nThreads, ThreadFactory threadFactory, Object... args) { + this(nThreads, threadFactory == null ? null : new ThreadPerTaskExecutor(threadFactory), args); + } + + /** + * Create a new instance. + * + * @param nThreads the number of threads that will be used by this instance. + * @param executor the Executor to use, or {@code null} if the default should be used. + * @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call + */ + protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) { + this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args); + } + + /** + * Create a new instance. + * + * @param nThreads the number of threads that will be used by this instance. + * @param executor the Executor to use, or {@code null} if the default should be used. + * @param chooserFactory the {@link EventExecutorChooserFactory} to use. + * @param args arguments which will passed to each {@link #newChild(Executor, Object...)} call + */ + protected MultithreadEventExecutorGroup(int nThreads, Executor executor, + EventExecutorChooserFactory chooserFactory, Object... args) { + if (nThreads <= 0) { + throw new IllegalArgumentException("nThreads must be positive " + nThreads); + } + + if (executor == null) { + executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); + } + + children = new EventExecutor[nThreads]; + + for (int i = 0; i < nThreads; i ++) { + boolean success = false; + try { + children[i] = newChild(executor, args); + success = true; + } catch (Exception e) { + // TODO: Think about if this is a good exception type + throw new IllegalStateException("failed to create a child event loop", e); + } finally { + if (!success) { + for (int j = 0; j < i; j ++) { + children[j].shutdownGracefully(); + } + + for (int j = 0; j < i; j ++) { + EventExecutor e = children[j]; + try { + while (!e.isTerminated()) { + e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS); + } + } catch (InterruptedException interrupted) { + // Let the caller handle the interruption. + Thread.currentThread().interrupt(); + break; + } + } + } + } + } + + chooser = chooserFactory.newChooser(children); + + final FutureListener terminationListener = new FutureListener() { + @Override + public void operationComplete(Future future) throws Exception { + if (terminatedChildren.incrementAndGet() == children.length) { + terminationFuture.setSuccess(null); + } + } + }; + + for (EventExecutor e: children) { + e.terminationFuture().addListener(terminationListener); + } + + Set childrenSet = new LinkedHashSet(children.length); + Collections.addAll(childrenSet, children); + readonlyChildren = Collections.unmodifiableSet(childrenSet); + } + + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory(getClass()); + } + + @Override + public EventExecutor next() { + return chooser.next(); + } + + @Override + public Iterator iterator() { + return readonlyChildren.iterator(); + } + + /** + * Return the number of {@link EventExecutor} this implementation uses. This number is the maps + * 1:1 to the threads it use. + */ + public final int executorCount() { + return children.length; + } + + /** + * Create a new EventExecutor which will later then accessible via the {@link #next()} method. This method will be + * called for each thread that will serve this {@link MultithreadEventExecutorGroup}. + * + */ + protected abstract EventExecutor newChild(Executor executor, Object... args) throws Exception; + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { + for (EventExecutor l: children) { + l.shutdownGracefully(quietPeriod, timeout, unit); + } + return terminationFuture(); + } + + @Override + public Future terminationFuture() { + return terminationFuture; + } + + @Override + @Deprecated + public void shutdown() { + for (EventExecutor l: children) { + l.shutdown(); + } + } + + @Override + public boolean isShuttingDown() { + for (EventExecutor l: children) { + if (!l.isShuttingDown()) { + return false; + } + } + return true; + } + + @Override + public boolean isShutdown() { + for (EventExecutor l: children) { + if (!l.isShutdown()) { + return false; + } + } + return true; + } + + @Override + public boolean isTerminated() { + for (EventExecutor l: children) { + if (!l.isTerminated()) { + return false; + } + } + return true; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException { + long deadline = System.nanoTime() + unit.toNanos(timeout); + loop: for (EventExecutor l: children) { + for (;;) { + long timeLeft = deadline - System.nanoTime(); + if (timeLeft <= 0) { + break loop; + } + if (l.awaitTermination(timeLeft, TimeUnit.NANOSECONDS)) { + break; + } + } + } + return isTerminated(); + } +} diff --git a/src/main/java/org/xbib/event/loop/MultithreadEventLoopGroup.java b/src/main/java/org/xbib/event/loop/MultithreadEventLoopGroup.java new file mode 100644 index 0000000..94fba29 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/MultithreadEventLoopGroup.java @@ -0,0 +1,66 @@ +package org.xbib.event.loop; + +import org.xbib.event.thread.DefaultThreadFactory; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Abstract base class for {@link EventLoopGroup} implementations that handles their tasks with multiple threads at + * the same time. + */ +public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup { + + private static final Logger logger = Logger.getLogger(MultithreadEventLoopGroup.class.getName()); + + private static final int DEFAULT_EVENT_LOOP_THREADS; + + static { + DEFAULT_EVENT_LOOP_THREADS = Math.max(1, + Integer.getInteger("org.xbib.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2)); + + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "-Dorg.xbib.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS); + } + } + + /** + * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...) + */ + protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { + super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); + } + + /** + * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, ThreadFactory, Object...) + */ + protected MultithreadEventLoopGroup(int nThreads, ThreadFactory threadFactory, Object... args) { + super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, threadFactory, args); + } + + /** + * @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, + * EventExecutorChooserFactory, Object...) + */ + protected MultithreadEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + Object... args) { + super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, chooserFactory, args); + } + + @Override + protected ThreadFactory newDefaultThreadFactory() { + return new DefaultThreadFactory(getClass(), Thread.MAX_PRIORITY); + } + + @Override + public EventLoop next() { + return (EventLoop) super.next(); + } + + @Override + protected abstract EventLoop newChild(Executor executor, Object... args) throws Exception; + + +} diff --git a/src/main/java/org/xbib/event/loop/OrderedEventExecutor.java b/src/main/java/org/xbib/event/loop/OrderedEventExecutor.java new file mode 100644 index 0000000..004bd25 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/OrderedEventExecutor.java @@ -0,0 +1,7 @@ +package org.xbib.event.loop; + +/** + * Marker interface for {@link EventExecutor}s that will process all submitted tasks in an ordered / serial fashion. + */ +public interface OrderedEventExecutor extends EventExecutor { +} diff --git a/src/main/java/org/xbib/event/loop/RejectedExecutionHandler.java b/src/main/java/org/xbib/event/loop/RejectedExecutionHandler.java new file mode 100644 index 0000000..df6ebd9 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/RejectedExecutionHandler.java @@ -0,0 +1,13 @@ +package org.xbib.event.loop; + +/** + * Similar to {@link java.util.concurrent.RejectedExecutionHandler} but specific to {@link SingleThreadEventExecutor}. + */ +public interface RejectedExecutionHandler { + + /** + * Called when someone tried to add a task to {@link SingleThreadEventExecutor} but this failed due capacity + * restrictions. + */ + void rejected(Runnable task, SingleThreadEventExecutor executor); +} diff --git a/src/main/java/org/xbib/event/loop/RejectedExecutionHandlers.java b/src/main/java/org/xbib/event/loop/RejectedExecutionHandlers.java new file mode 100644 index 0000000..985354e --- /dev/null +++ b/src/main/java/org/xbib/event/loop/RejectedExecutionHandlers.java @@ -0,0 +1,54 @@ +package org.xbib.event.loop; + +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +/** + * Expose helper methods which create different {@link RejectedExecutionHandler}s. + */ +public final class RejectedExecutionHandlers { + private static final RejectedExecutionHandler REJECT = (task, executor) -> { + throw new RejectedExecutionException(); + }; + + private RejectedExecutionHandlers() { } + + /** + * Returns a {@link RejectedExecutionHandler} that will always just throw a {@link RejectedExecutionException}. + */ + public static RejectedExecutionHandler reject() { + return REJECT; + } + + /** + * Tries to backoff when the task can not be added due restrictions for an configured amount of time. This + * is only done if the task was added from outside of the event loop which means + * {@link EventExecutor#inEventLoop()} returns {@code false}. + */ + public static RejectedExecutionHandler backoff(final int retries, long backoffAmount, TimeUnit unit) { + if (retries <= 0) { + throw new IllegalArgumentException("retries must be positive"); + } + final long backOffNanos = unit.toNanos(backoffAmount); + return new RejectedExecutionHandler() { + @Override + public void rejected(Runnable task, SingleThreadEventExecutor executor) { + if (!executor.inEventLoop()) { + for (int i = 0; i < retries; i++) { + // Try to wake up the executor so it will empty its task queue. + executor.wakeup(false); + + LockSupport.parkNanos(backOffNanos); + if (executor.offerTask(task)) { + return; + } + } + } + // Either we tried to add the task from within the EventLoop or we was not able to add it even with + // backoff. + throw new RejectedExecutionException(); + } + }; + } +} diff --git a/src/main/java/org/xbib/event/loop/SingleThreadEventExecutor.java b/src/main/java/org/xbib/event/loop/SingleThreadEventExecutor.java new file mode 100644 index 0000000..11fdc12 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/SingleThreadEventExecutor.java @@ -0,0 +1,1107 @@ +package org.xbib.event.loop; + +import org.xbib.event.DefaultPromise; +import org.xbib.event.Future; +import org.xbib.event.Promise; +import org.xbib.event.ScheduledFutureTask; +import org.xbib.event.thread.FastThreadLocal; +import org.xbib.event.thread.ThreadExecutorMap; +import org.xbib.event.thread.ThreadPerTaskExecutor; +import org.xbib.event.thread.ThreadProperties; + +import java.lang.Thread.State; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Abstract base class for {@link OrderedEventExecutor}'s that execute all its submitted tasks in a single thread. + * + */ +public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor { + + static final int DEFAULT_MAX_PENDING_EXECUTOR_TASKS = Math.max(16, + Integer.getInteger("org.xbib.eventexecutor.maxPendingTasks", Integer.MAX_VALUE)); + + private static final Logger logger = Logger.getLogger(SingleThreadEventExecutor.class.getName()); + + private static final int ST_NOT_STARTED = 1; + private static final int ST_STARTED = 2; + private static final int ST_SHUTTING_DOWN = 3; + private static final int ST_SHUTDOWN = 4; + private static final int ST_TERMINATED = 5; + + private static final Runnable NOOP_TASK = new Runnable() { + @Override + public void run() { + // Do nothing. + } + }; + + private static final AtomicIntegerFieldUpdater STATE_UPDATER = + AtomicIntegerFieldUpdater.newUpdater(SingleThreadEventExecutor.class, "state"); + private static final AtomicReferenceFieldUpdater PROPERTIES_UPDATER = + AtomicReferenceFieldUpdater.newUpdater( + SingleThreadEventExecutor.class, ThreadProperties.class, "threadProperties"); + + private final Queue taskQueue; + + private volatile Thread thread; + @SuppressWarnings("unused") + private volatile ThreadProperties threadProperties; + private final Executor executor; + private volatile boolean interrupted; + + private final CountDownLatch threadLock = new CountDownLatch(1); + private final Set shutdownHooks = new LinkedHashSet(); + private final boolean addTaskWakesUp; + private final int maxPendingTasks; + private final RejectedExecutionHandler rejectedExecutionHandler; + + private long lastExecutionTime; + + @SuppressWarnings({ "FieldMayBeFinal", "unused" }) + private volatile int state = ST_NOT_STARTED; + + private volatile long gracefulShutdownQuietPeriod; + private volatile long gracefulShutdownTimeout; + private long gracefulShutdownStartTime; + + private final Promise terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE); + + /** + * Create a new instance + * + * @param parent the {@link EventExecutorGroup} which is the parent of this instance and belongs to it + * @param threadFactory the {@link ThreadFactory} which will be used for the used {@link Thread} + * @param addTaskWakesUp {@code true} if and only if invocation of {@link #addTask(Runnable)} will wake up the + * executor thread + */ + protected SingleThreadEventExecutor( + EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) { + this(parent, new ThreadPerTaskExecutor(threadFactory), addTaskWakesUp); + } + + /** + * Create a new instance + * + * @param parent the {@link EventExecutorGroup} which is the parent of this instance and belongs to it + * @param threadFactory the {@link ThreadFactory} which will be used for the used {@link Thread} + * @param addTaskWakesUp {@code true} if and only if invocation of {@link #addTask(Runnable)} will wake up the + * executor thread + * @param maxPendingTasks the maximum number of pending tasks before new tasks will be rejected. + * @param rejectedHandler the {@link RejectedExecutionHandler} to use. + */ + protected SingleThreadEventExecutor( + EventExecutorGroup parent, ThreadFactory threadFactory, + boolean addTaskWakesUp, int maxPendingTasks, RejectedExecutionHandler rejectedHandler) { + this(parent, new ThreadPerTaskExecutor(threadFactory), addTaskWakesUp, maxPendingTasks, rejectedHandler); + } + + /** + * Create a new instance + * + * @param parent the {@link EventExecutorGroup} which is the parent of this instance and belongs to it + * @param executor the {@link Executor} which will be used for executing + * @param addTaskWakesUp {@code true} if and only if invocation of {@link #addTask(Runnable)} will wake up the + * executor thread + */ + protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, boolean addTaskWakesUp) { + this(parent, executor, addTaskWakesUp, DEFAULT_MAX_PENDING_EXECUTOR_TASKS, RejectedExecutionHandlers.reject()); + } + + /** + * Create a new instance + * + * @param parent the {@link EventExecutorGroup} which is the parent of this instance and belongs to it + * @param executor the {@link Executor} which will be used for executing + * @param addTaskWakesUp {@code true} if and only if invocation of {@link #addTask(Runnable)} will wake up the + * executor thread + * @param maxPendingTasks the maximum number of pending tasks before new tasks will be rejected. + * @param rejectedHandler the {@link RejectedExecutionHandler} to use. + */ + protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, + boolean addTaskWakesUp, int maxPendingTasks, + RejectedExecutionHandler rejectedHandler) { + super(parent); + this.addTaskWakesUp = addTaskWakesUp; + this.maxPendingTasks = Math.max(16, maxPendingTasks); + this.executor = ThreadExecutorMap.apply(executor, this); + taskQueue = newTaskQueue(this.maxPendingTasks); + rejectedExecutionHandler = Objects.requireNonNull(rejectedHandler, "rejectedHandler"); + } + + protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor, + boolean addTaskWakesUp, Queue taskQueue, + RejectedExecutionHandler rejectedHandler) { + super(parent); + this.addTaskWakesUp = addTaskWakesUp; + this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS; + this.executor = ThreadExecutorMap.apply(executor, this); + this.taskQueue = Objects.requireNonNull(taskQueue, "taskQueue"); + this.rejectedExecutionHandler = Objects.requireNonNull(rejectedHandler, "rejectedHandler"); + } + + /** + * @deprecated Please use and override {@link #newTaskQueue(int)}. + */ + @Deprecated + protected Queue newTaskQueue() { + return newTaskQueue(maxPendingTasks); + } + + /** + * Create a new {@link Queue} which will holds the tasks to execute. This default implementation will return a + * {@link LinkedBlockingQueue} but if your sub-class of {@link SingleThreadEventExecutor} will not do any blocking + * calls on the this {@link Queue} it may make sense to {@code @Override} this and return some more performant + * implementation that does not support blocking operations at all. + */ + protected Queue newTaskQueue(int maxPendingTasks) { + return new LinkedBlockingQueue(maxPendingTasks); + } + + /** + * Interrupt the current running {@link Thread}. + */ + protected void interruptThread() { + Thread currentThread = thread; + if (currentThread == null) { + interrupted = true; + } else { + currentThread.interrupt(); + } + } + + /** + * @see Queue#poll() + */ + protected Runnable pollTask() { + assert inEventLoop(); + return pollTaskFrom(taskQueue); + } + + protected static Runnable pollTaskFrom(Queue taskQueue) { + for (;;) { + Runnable task = taskQueue.poll(); + if (task != WAKEUP_TASK) { + return task; + } + } + } + + /** + * Take the next {@link Runnable} from the task queue and so will block if no task is currently present. + *

+ * Be aware that this method will throw an {@link UnsupportedOperationException} if the task queue, which was + * created via {@link #newTaskQueue()}, does not implement {@link BlockingQueue}. + *

+ * + * @return {@code null} if the executor thread has been interrupted or waken up. + */ + protected Runnable takeTask() { + assert inEventLoop(); + if (!(taskQueue instanceof BlockingQueue)) { + throw new UnsupportedOperationException(); + } + + BlockingQueue taskQueue = (BlockingQueue) this.taskQueue; + for (;;) { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { + Runnable task = null; + try { + task = taskQueue.take(); + if (task == WAKEUP_TASK) { + task = null; + } + } 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 boolean fetchFromScheduledTaskQueue() { + if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) { + return true; + } + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + for (;;) { + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + return true; + } + if (!taskQueue.offer(scheduledTask)) { + // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again. + scheduledTaskQueue.add((ScheduledFutureTask) scheduledTask); + return false; + } + } + } + + /** + * @return {@code true} if at least one scheduled task was executed. + */ + private boolean executeExpiredScheduledTasks() { + if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) { + return false; + } + long nanoTime = AbstractScheduledEventExecutor.nanoTime(); + Runnable scheduledTask = pollScheduledTask(nanoTime); + if (scheduledTask == null) { + return false; + } + do { + safeExecute(scheduledTask); + } while ((scheduledTask = pollScheduledTask(nanoTime)) != null); + return true; + } + + /** + * @see Queue#peek() + */ + protected Runnable peekTask() { + assert inEventLoop(); + return taskQueue.peek(); + } + + /** + * @see Queue#isEmpty() + */ + protected boolean hasTasks() { + assert inEventLoop(); + return !taskQueue.isEmpty(); + } + + /** + * 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. + */ + protected void addTask(Runnable task) { + Objects.requireNonNull(task, "task"); + if (!offerTask(task)) { + reject(task); + } + } + + final boolean offerTask(Runnable task) { + if (isShutdown()) { + reject(); + } + return taskQueue.offer(task); + } + + /** + * @see Queue#remove(Object) + */ + protected boolean removeTask(Runnable task) { + return taskQueue.remove(Objects.requireNonNull(task, "task")); + } + + /** + * Poll all tasks from the task queue and run them via {@link Runnable#run()} method. + * + * @return {@code true} if and only if at least one task was run + */ + protected boolean runAllTasks() { + assert inEventLoop(); + boolean fetchedAll; + boolean ranAtLeastOne = false; + + do { + fetchedAll = fetchFromScheduledTaskQueue(); + if (runAllTasksFrom(taskQueue)) { + ranAtLeastOne = true; + } + } while (!fetchedAll); // keep on processing until we fetched all scheduled tasks. + + if (ranAtLeastOne) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + } + afterRunningAllTasks(); + return ranAtLeastOne; + } + + /** + * Execute all expired scheduled tasks and all current tasks in the executor queue until both queues are empty, + * or {@code maxDrainAttempts} has been exceeded. + * @param maxDrainAttempts The maximum amount of times this method attempts to drain from queues. This is to prevent + * continuous task execution and scheduling from preventing the EventExecutor thread to + * make progress and return to the selector mechanism to process inbound I/O events. + * @return {@code true} if at least one task was run. + */ + protected final boolean runScheduledAndExecutorTasks(final int maxDrainAttempts) { + assert inEventLoop(); + boolean ranAtLeastOneTask; + int drainAttempt = 0; + do { + // We must run the taskQueue tasks first, because the scheduled tasks from outside the EventLoop are queued + // here because the taskQueue is thread safe and the scheduledTaskQueue is not thread safe. + ranAtLeastOneTask = runExistingTasksFrom(taskQueue) | executeExpiredScheduledTasks(); + } while (ranAtLeastOneTask && ++drainAttempt < maxDrainAttempts); + + if (drainAttempt > 0) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + } + afterRunningAllTasks(); + + return drainAttempt > 0; + } + + /** + * Runs all tasks from the passed {@code taskQueue}. + * + * @param taskQueue To poll and execute all tasks. + * + * @return {@code true} if at least one task was executed. + */ + protected final boolean runAllTasksFrom(Queue taskQueue) { + Runnable task = pollTaskFrom(taskQueue); + if (task == null) { + return false; + } + for (;;) { + safeExecute(task); + task = pollTaskFrom(taskQueue); + if (task == null) { + return true; + } + } + } + + /** + * What ever tasks are present in {@code taskQueue} when this method is invoked will be {@link Runnable#run()}. + * @param taskQueue the task queue to drain. + * @return {@code true} if at least {@link Runnable#run()} was called. + */ + private boolean runExistingTasksFrom(Queue taskQueue) { + Runnable task = pollTaskFrom(taskQueue); + if (task == null) { + return false; + } + int remaining = Math.min(maxPendingTasks, taskQueue.size()); + safeExecute(task); + // Use taskQueue.poll() directly rather than pollTaskFrom() since the latter may + // silently consume more than one item from the queue (skips over WAKEUP_TASK instances) + while (remaining-- > 0 && (task = taskQueue.poll()) != null) { + safeExecute(task); + } + return true; + } + + /** + * Poll all tasks from the task queue and run them via {@link Runnable#run()} method. This method stops running + * the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}. + */ + protected boolean runAllTasks(long timeoutNanos) { + fetchFromScheduledTaskQueue(); + Runnable task = pollTask(); + if (task == null) { + afterRunningAllTasks(); + return false; + } + + final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0; + long runTasks = 0; + long lastExecutionTime; + for (;;) { + safeExecute(task); + + runTasks ++; + + // Check timeout every 64 tasks because nanoTime() is relatively expensive. + // XXX: Hard-coded value - will make it configurable if it is really a problem. + if ((runTasks & 0x3F) == 0) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + if (lastExecutionTime >= deadline) { + break; + } + } + + task = pollTask(); + if (task == null) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + break; + } + } + + afterRunningAllTasks(); + this.lastExecutionTime = lastExecutionTime; + return true; + } + + /** + * Invoked before returning from {@link #runAllTasks()} and {@link #runAllTasks(long)}. + */ + protected void afterRunningAllTasks() { } + + /** + * Returns the amount of time left until the scheduled task with the closest dead line is executed. + */ + protected long delayNanos(long currentTimeNanos) { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { + return SCHEDULE_PURGE_INTERVAL; + } + + return scheduledTask.delayNanos(currentTimeNanos); + } + + /** + * Returns the absolute point in time (relative to {@link #nanoTime()}) at which the the next + * closest scheduled task should run. + */ + protected long deadlineNanos() { + ScheduledFutureTask scheduledTask = peekScheduledTask(); + if (scheduledTask == null) { + return nanoTime() + SCHEDULE_PURGE_INTERVAL; + } + return scheduledTask.deadlineNanos(); + } + + /** + * Updates the internal timestamp that tells when a submitted task was executed most recently. + * {@link #runAllTasks()} and {@link #runAllTasks(long)} updates this timestamp automatically, and thus there's + * usually no need to call this method. However, if you take the tasks manually using {@link #takeTask()} or + * {@link #pollTask()}, you have to call this method at the end of task execution loop for accurate quiet period + * checks. + */ + protected void updateLastExecutionTime() { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + } + + /** + * Run the tasks in the {@link #taskQueue} + */ + protected abstract void run(); + + /** + * Do nothing, sub-classes may override + */ + protected void cleanup() { + // NOOP + } + + protected void wakeup(boolean inEventLoop) { + if (!inEventLoop) { + // Use offer as we actually only need this to unblock the thread and if offer fails we do not care as there + // is already something in the queue. + taskQueue.offer(WAKEUP_TASK); + } + } + + @Override + public boolean inEventLoop(Thread thread) { + return thread == this.thread; + } + + /** + * Add a {@link Runnable} which will be executed on shutdown of this instance + */ + public void addShutdownHook(final Runnable task) { + if (inEventLoop()) { + shutdownHooks.add(task); + } else { + execute(new Runnable() { + @Override + public void run() { + shutdownHooks.add(task); + } + }); + } + } + + /** + * Remove a previous added {@link Runnable} as a shutdown hook + */ + public void removeShutdownHook(final Runnable task) { + if (inEventLoop()) { + shutdownHooks.remove(task); + } else { + execute(new Runnable() { + @Override + public void run() { + shutdownHooks.remove(task); + } + }); + } + } + + private boolean runShutdownHooks() { + boolean ran = false; + // Note shutdown hooks can add / remove shutdown hooks. + while (!shutdownHooks.isEmpty()) { + List copy = new ArrayList(shutdownHooks); + shutdownHooks.clear(); + for (Runnable task: copy) { + try { + task.run(); + } catch (Throwable t) { + logger.log(Level.WARNING, "Shutdown hook raised an exception.", t); + } finally { + ran = true; + } + } + } + + if (ran) { + lastExecutionTime = ScheduledFutureTask.nanoTime(); + } + + return ran; + } + + @Override + public Future shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) { + if (quietPeriod < 0) { + throw new IllegalArgumentException("quietPeriod must not be negative"); + } + if (timeout < quietPeriod) { + throw new IllegalArgumentException( + "timeout: " + timeout + " (expected >= quietPeriod (" + quietPeriod + "))"); + } + Objects.requireNonNull(unit, "unit"); + + if (isShuttingDown()) { + return terminationFuture(); + } + + boolean inEventLoop = inEventLoop(); + boolean wakeup; + int oldState; + for (;;) { + if (isShuttingDown()) { + return terminationFuture(); + } + int newState; + wakeup = true; + oldState = state; + if (inEventLoop) { + newState = ST_SHUTTING_DOWN; + } else { + switch (oldState) { + case ST_NOT_STARTED: + case ST_STARTED: + newState = ST_SHUTTING_DOWN; + break; + default: + newState = oldState; + wakeup = false; + } + } + if (STATE_UPDATER.compareAndSet(this, oldState, newState)) { + break; + } + } + gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod); + gracefulShutdownTimeout = unit.toNanos(timeout); + + if (ensureThreadStarted(oldState)) { + return terminationFuture; + } + + if (wakeup) { + taskQueue.offer(WAKEUP_TASK); + if (!addTaskWakesUp) { + wakeup(inEventLoop); + } + } + + return terminationFuture(); + } + + @Override + public Future terminationFuture() { + return terminationFuture; + } + + @Override + @Deprecated + public void shutdown() { + if (isShutdown()) { + return; + } + + boolean inEventLoop = inEventLoop(); + boolean wakeup; + int oldState; + for (;;) { + if (isShuttingDown()) { + return; + } + int newState; + wakeup = true; + oldState = state; + if (inEventLoop) { + newState = ST_SHUTDOWN; + } else { + switch (oldState) { + case ST_NOT_STARTED: + case ST_STARTED: + case ST_SHUTTING_DOWN: + newState = ST_SHUTDOWN; + break; + default: + newState = oldState; + wakeup = false; + } + } + if (STATE_UPDATER.compareAndSet(this, oldState, newState)) { + break; + } + } + + if (ensureThreadStarted(oldState)) { + return; + } + + if (wakeup) { + taskQueue.offer(WAKEUP_TASK); + if (!addTaskWakesUp) { + wakeup(inEventLoop); + } + } + } + + @Override + public boolean isShuttingDown() { + return state >= ST_SHUTTING_DOWN; + } + + @Override + public boolean isShutdown() { + return state >= ST_SHUTDOWN; + } + + @Override + public boolean isTerminated() { + return state == ST_TERMINATED; + } + + /** + * Confirm that the shutdown if the instance should be done now! + */ + protected boolean confirmShutdown() { + if (!isShuttingDown()) { + return false; + } + + if (!inEventLoop()) { + throw new IllegalStateException("must be invoked from an event loop"); + } + + cancelScheduledTasks(); + + if (gracefulShutdownStartTime == 0) { + gracefulShutdownStartTime = ScheduledFutureTask.nanoTime(); + } + + if (runAllTasks() || runShutdownHooks()) { + if (isShutdown()) { + // Executor shut down - no new tasks anymore. + return true; + } + + // There were tasks in the queue. Wait a little bit more until no tasks are queued for the quiet period or + // terminate if the quiet period is 0. + // See https://github.com/netty/netty/issues/4241 + if (gracefulShutdownQuietPeriod == 0) { + return true; + } + taskQueue.offer(WAKEUP_TASK); + return false; + } + + final long nanoTime = ScheduledFutureTask.nanoTime(); + + if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) { + return true; + } + + if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) { + // Check if any tasks were added to the queue every 100ms. + // TODO: Change the behavior of takeTask() so that it returns on timeout. + taskQueue.offer(WAKEUP_TASK); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // Ignore + } + + return false; + } + + // No tasks were added for last quiet period - hopefully safe to shut down. + // (Hopefully because we really cannot make a guarantee that there will be no execute() calls by a user.) + return true; + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + Objects.requireNonNull(unit, "unit"); + if (inEventLoop()) { + throw new IllegalStateException("cannot await termination of the current thread"); + } + + threadLock.await(timeout, unit); + + return isTerminated(); + } + + @Override + public void execute(Runnable task) { + Objects.requireNonNull(task, "task"); + execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task)); + } + + @Override + public void lazyExecute(Runnable task) { + execute(Objects.requireNonNull(task, "task"), false); + } + + private void execute(Runnable task, boolean immediate) { + boolean inEventLoop = inEventLoop(); + addTask(task); + if (!inEventLoop) { + startThread(); + if (isShutdown()) { + boolean reject = false; + try { + if (removeTask(task)) { + reject = true; + } + } catch (UnsupportedOperationException e) { + // The task queue does not support removal so the best thing we can do is to just move on and + // hope we will be able to pick-up the task before its completely terminated. + // In worst case we will log on termination. + } + if (reject) { + reject(); + } + } + } + + if (!addTaskWakesUp && immediate) { + wakeup(inEventLoop); + } + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + throwIfInEventLoop("invokeAny"); + return super.invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + throwIfInEventLoop("invokeAny"); + return super.invokeAny(tasks, timeout, unit); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + throwIfInEventLoop("invokeAll"); + return super.invokeAll(tasks); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + throwIfInEventLoop("invokeAll"); + return super.invokeAll(tasks, timeout, unit); + } + + private void throwIfInEventLoop(String method) { + if (inEventLoop()) { + throw new RejectedExecutionException("Calling " + method + " from within the EventLoop is not allowed"); + } + } + + /** + * Returns the {@link ThreadProperties} of the {@link Thread} that powers the {@link SingleThreadEventExecutor}. + * If the {@link SingleThreadEventExecutor} is not started yet, this operation will start it and block until + * it is fully started. + */ + public final ThreadProperties threadProperties() { + ThreadProperties threadProperties = this.threadProperties; + if (threadProperties == null) { + Thread thread = this.thread; + if (thread == null) { + assert !inEventLoop(); + submit(NOOP_TASK).syncUninterruptibly(); + thread = this.thread; + assert thread != null; + } + + threadProperties = new DefaultThreadProperties(thread); + if (!PROPERTIES_UPDATER.compareAndSet(this, null, threadProperties)) { + threadProperties = this.threadProperties; + } + } + + return threadProperties; + } + + /** + * @deprecated use {@link AbstractEventExecutor.LazyRunnable} + */ + @Deprecated + protected interface NonWakeupRunnable extends LazyRunnable { } + + /** + * Can be overridden to control which tasks require waking the {@link EventExecutor} thread + * if it is waiting so that they can be run immediately. + */ + protected boolean wakesUpForTask(Runnable task) { + return true; + } + + protected static void reject() { + throw new RejectedExecutionException("event executor terminated"); + } + + /** + * Offers the task to the associated {@link RejectedExecutionHandler}. + * + * @param task to reject. + */ + protected final void reject(Runnable task) { + rejectedExecutionHandler.rejected(task, this); + } + + // ScheduledExecutorService implementation + + private static final long SCHEDULE_PURGE_INTERVAL = TimeUnit.SECONDS.toNanos(1); + + private void startThread() { + if (state == ST_NOT_STARTED) { + if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { + boolean success = false; + try { + doStartThread(); + success = true; + } finally { + if (!success) { + STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED); + } + } + } + } + } + + private boolean ensureThreadStarted(int oldState) { + if (oldState == ST_NOT_STARTED) { + try { + doStartThread(); + } catch (Throwable cause) { + STATE_UPDATER.set(this, ST_TERMINATED); + terminationFuture.tryFailure(cause); + + if (!(cause instanceof Exception)) { + // Also rethrow as it may be an OOME for example + throw new RuntimeException(cause); + } + return true; + } + } + return false; + } + + private void doStartThread() { + assert thread == null; + executor.execute(new Runnable() { + @Override + public void run() { + thread = Thread.currentThread(); + if (interrupted) { + thread.interrupt(); + } + + boolean success = false; + updateLastExecutionTime(); + try { + SingleThreadEventExecutor.this.run(); + success = true; + } catch (Throwable t) { + logger.log(Level.WARNING, "Unexpected exception from an event executor: ", t); + } finally { + for (;;) { + int oldState = state; + if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet( + SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) { + break; + } + } + + // Check if confirmShutdown() was called at the end of the loop. + if (success && gracefulShutdownStartTime == 0) { + if (logger.isLoggable(Level.SEVERE)) { + logger.log(Level.SEVERE, "Buggy " + EventExecutor.class.getSimpleName() + " implementation; " + + SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must " + + "be called before run() implementation terminates."); + } + } + + try { + // Run all remaining tasks and shutdown hooks. At this point the event loop + // is in ST_SHUTTING_DOWN state still accepting tasks which is needed for + // graceful shutdown with quietPeriod. + for (;;) { + if (confirmShutdown()) { + break; + } + } + + // Now we want to make sure no more tasks can be added from this point. This is + // achieved by switching the state. Any new tasks beyond this point will be rejected. + for (;;) { + int oldState = state; + if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet( + SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) { + break; + } + } + + // We have the final set of tasks in the queue now, no more can be added, run all remaining. + // No need to loop here, this is the final pass. + confirmShutdown(); + } finally { + try { + cleanup(); + } finally { + // Lets remove all FastThreadLocals for the Thread as we are about to terminate and notify + // the future. The user may block on the future and once it unblocks the JVM may terminate + // and start unloading classes. + // See https://github.com/netty/netty/issues/6596. + FastThreadLocal.removeAll(); + + STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED); + threadLock.countDown(); + int numUserTasks = drainTasks(); + if (numUserTasks > 0 && logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "An event executor terminated with " + + "non-empty task queue (" + numUserTasks + ')'); + } + terminationFuture.setSuccess(null); + } + } + } + } + }); + } + + final int drainTasks() { + int numTasks = 0; + for (;;) { + Runnable runnable = taskQueue.poll(); + if (runnable == null) { + break; + } + // WAKEUP_TASK should be just discarded as these are added internally. + // The important bit is that we not have any user tasks left. + if (WAKEUP_TASK != runnable) { + numTasks++; + } + } + return numTasks; + } + + private static final class DefaultThreadProperties implements ThreadProperties { + private final Thread t; + + DefaultThreadProperties(Thread t) { + this.t = t; + } + + @Override + public State state() { + return t.getState(); + } + + @Override + public int priority() { + return t.getPriority(); + } + + @Override + public boolean isInterrupted() { + return t.isInterrupted(); + } + + @Override + public boolean isDaemon() { + return t.isDaemon(); + } + + @Override + public String name() { + return t.getName(); + } + + @Override + public long id() { + return t.getId(); + } + + @Override + public StackTraceElement[] stackTrace() { + return t.getStackTrace(); + } + + @Override + public boolean isAlive() { + return t.isAlive(); + } + } +} diff --git a/src/main/java/org/xbib/event/loop/SingleThreadEventLoop.java b/src/main/java/org/xbib/event/loop/SingleThreadEventLoop.java new file mode 100644 index 0000000..6a97374 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/SingleThreadEventLoop.java @@ -0,0 +1,116 @@ +package org.xbib.event.loop; + +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * Abstract base class for {@link EventLoop}s that execute all its submitted tasks in a single thread. + * + */ +public abstract class SingleThreadEventLoop extends SingleThreadEventExecutor implements EventLoop { + + protected static final int DEFAULT_MAX_PENDING_TASKS = Math.max(16, + Integer.getInteger("org.xbib.eventLoop.maxPendingTasks", Integer.MAX_VALUE)); + + private final Queue tailTasks; + + protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) { + this(parent, threadFactory, addTaskWakesUp, DEFAULT_MAX_PENDING_TASKS, RejectedExecutionHandlers.reject()); + } + + protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, boolean addTaskWakesUp) { + this(parent, executor, addTaskWakesUp, DEFAULT_MAX_PENDING_TASKS, RejectedExecutionHandlers.reject()); + } + + protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory, + boolean addTaskWakesUp, int maxPendingTasks, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, threadFactory, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); + tailTasks = newTaskQueue(maxPendingTasks); + } + + protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, + boolean addTaskWakesUp, int maxPendingTasks, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler); + tailTasks = newTaskQueue(maxPendingTasks); + } + + protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor, + boolean addTaskWakesUp, Queue taskQueue, Queue tailTaskQueue, + RejectedExecutionHandler rejectedExecutionHandler) { + super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler); + tailTasks = Objects.requireNonNull(tailTaskQueue, "tailTaskQueue"); + } + + @Override + public EventLoopGroup parent() { + return (EventLoopGroup) super.parent(); + } + + @Override + public EventLoop next() { + return (EventLoop) super.next(); + } + + /*@Override + public ChannelFuture register(Channel channel) { + return register(new DefaultChannelPromise(channel, this)); + }*/ + + /*@Override + public ChannelFuture register(final ChannelPromise promise) { + ObjectUtil.checkNotNull(promise, "promise"); + promise.channel().unsafe().register(this, promise); + return promise; + }*/ + + /** + * Adds a task to be run once at the end of next (or current) {@code eventloop} iteration. + * + * @param task to be added. + */ + public final void executeAfterEventLoopIteration(Runnable task) { + Objects.requireNonNull(task, "task"); + if (isShutdown()) { + reject(); + } + + if (!tailTasks.offer(task)) { + reject(task); + } + + if (!(task instanceof LazyRunnable) && wakesUpForTask(task)) { + wakeup(inEventLoop()); + } + } + + /** + * Removes a task that was added previously via {@link #executeAfterEventLoopIteration(Runnable)}. + * + * @param task to be removed. + * + * @return {@code true} if the task was removed as a result of this call. + */ + final boolean removeAfterEventLoopIterationTask(Runnable task) { + return tailTasks.remove(Objects.requireNonNull(task, "task")); + } + + @Override + protected void afterRunningAllTasks() { + runAllTasksFrom(tailTasks); + } + + @Override + protected boolean hasTasks() { + return super.hasTasks() || !tailTasks.isEmpty(); + } + + @Override + public int pendingTasks() { + return super.pendingTasks() + tailTasks.size(); + } + +} diff --git a/src/main/java/org/xbib/event/loop/nio/NioEventLoop.java b/src/main/java/org/xbib/event/loop/nio/NioEventLoop.java new file mode 100644 index 0000000..0e03c00 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/nio/NioEventLoop.java @@ -0,0 +1,780 @@ +package org.xbib.event.loop.nio; + +import org.xbib.event.loop.EventLoopException; +import org.xbib.event.loop.EventLoopTaskQueueFactory; +import org.xbib.event.loop.RejectedExecutionHandler; +import org.xbib.event.loop.selector.SelectStrategy; +import org.xbib.event.loop.SingleThreadEventLoop; +import org.xbib.event.util.IntSupplier; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.Selector; +import java.nio.channels.SelectionKey; + +import java.nio.channels.spi.SelectorProvider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * {@link SingleThreadEventLoop} implementation which register the channels to a + * {@link Selector} and so performs the multiplexing of these in the event loop. + * + */ +public final class NioEventLoop extends SingleThreadEventLoop { + + private static final Logger logger = Logger.getLogger(NioEventLoop.class.getName()); + + private static final int CLEANUP_INTERVAL = 256; // XXX Hard-coded value, but won't need customization. + + private static final boolean DISABLE_KEY_SET_OPTIMIZATION = + Boolean.getBoolean("org.xbib.noKeySetOptimization"); + + private static final int MIN_PREMATURE_SELECTOR_RETURNS = 3; + private static final int SELECTOR_AUTO_REBUILD_THRESHOLD; + + private final IntSupplier selectNowSupplier = this::selectNow; + + // Workaround for JDK NIO bug. + // + // See: + // - https://bugs.java.com/view_bug.do?bug_id=6427854 + // - https://github.com/netty/netty/issues/203 + static { + final String key = "sun.nio.ch.bugLevel"; + final String bugLevel = System.getProperty(key); + if (bugLevel == null) { + System.setProperty(key, ""); + } + + int selectorAutoRebuildThreshold = Integer.getInteger("org.xbib.selectorAutoRebuildThreshold", 512); + if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) { + selectorAutoRebuildThreshold = 0; + } + + SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold; + + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "-Dorg.xbib.noKeySetOptimization: {}", DISABLE_KEY_SET_OPTIMIZATION); + logger.log(Level.FINE, "-Dorg.xbib.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD); + } + } + + /** + * The NIO {@link Selector}. + */ + private Selector selector; + private Selector unwrappedSelector; + private SelectedSelectionKeySet selectedKeys; + + private final SelectorProvider provider; + + private static final long AWAKE = -1L; + private static final long NONE = Long.MAX_VALUE; + + // nextWakeupNanos is: + // AWAKE when EL is awake + // NONE when EL is waiting with no wakeup scheduled + // other value T when EL is waiting with wakeup scheduled at time T + private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE); + + private final SelectStrategy selectStrategy; + + private volatile int ioRatio = 50; + private int cancelledKeys; + private boolean needsToSelectAgain; + + NioEventLoop(NioEventLoopGroup parent, + Executor executor, + SelectorProvider selectorProvider, + SelectStrategy strategy, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory taskQueueFactory, + EventLoopTaskQueueFactory tailTaskQueueFactory) { + super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory), + rejectedExecutionHandler); + this.provider = Objects.requireNonNull(selectorProvider, "selectorProvider"); + this.selectStrategy = Objects.requireNonNull(strategy, "selectStrategy"); + final SelectorTuple selectorTuple = null; // openSelector(); + this.selector = selectorTuple.selector; + this.unwrappedSelector = selectorTuple.unwrappedSelector; + } + + private static Queue newTaskQueue(EventLoopTaskQueueFactory queueFactory) { + if (queueFactory == null) { + return newTaskQueue0(DEFAULT_MAX_PENDING_TASKS); + } + return queueFactory.newTaskQueue(DEFAULT_MAX_PENDING_TASKS); + } + + private static final class SelectorTuple { + final Selector unwrappedSelector; + final Selector selector; + + SelectorTuple(Selector unwrappedSelector) { + this.unwrappedSelector = unwrappedSelector; + this.selector = unwrappedSelector; + } + + SelectorTuple(Selector unwrappedSelector, Selector selector) { + this.unwrappedSelector = unwrappedSelector; + this.selector = selector; + } + } + + /*private SelectorTuple openSelector() { + final Selector unwrappedSelector; + try { + unwrappedSelector = provider.openSelector(); + } catch (IOException e) { + throw new ChannelException("failed to open a new selector", e); + } + + if (DISABLE_KEY_SET_OPTIMIZATION) { + return new SelectorTuple(unwrappedSelector); + } + + Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + try { + return Class.forName( + "sun.nio.ch.SelectorImpl", + false, + PlatformDependent.getSystemClassLoader()); + } catch (Throwable cause) { + return cause; + } + } + }); + + if (!(maybeSelectorImplClass instanceof Class) || + // ensure the current selector implementation is what we can instrument. + !((Class) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) { + if (maybeSelectorImplClass instanceof Throwable) { + Throwable t = (Throwable) maybeSelectorImplClass; + logger.log(Level.FINEST, "failed to instrument a special java.util.Set into: " + unwrappedSelector, t); + } + return new SelectorTuple(unwrappedSelector); + } + + final Class selectorImplClass = (Class) maybeSelectorImplClass; + final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet(); + + Object maybeException = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + try { + Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys"); + Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys"); + + if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) { + // Let us try to use sun.misc.Unsafe to replace the SelectionKeySet. + // This allows us to also do this in Java9+ without any extra flags. + long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField); + long publicSelectedKeysFieldOffset = + PlatformDependent.objectFieldOffset(publicSelectedKeysField); + + if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) { + PlatformDependent.putObject( + unwrappedSelector, selectedKeysFieldOffset, selectedKeySet); + PlatformDependent.putObject( + unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet); + return null; + } + // We could not retrieve the offset, lets try reflection as last-resort. + } + + Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true); + if (cause != null) { + return cause; + } + cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true); + if (cause != null) { + return cause; + } + + selectedKeysField.set(unwrappedSelector, selectedKeySet); + publicSelectedKeysField.set(unwrappedSelector, selectedKeySet); + return null; + } catch (NoSuchFieldException e) { + return e; + } catch (IllegalAccessException e) { + return e; + } + } + }); + + if (maybeException instanceof Exception) { + selectedKeys = null; + Exception e = (Exception) maybeException; + logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e); + return new SelectorTuple(unwrappedSelector); + } + selectedKeys = selectedKeySet; + logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector); + return new SelectorTuple(unwrappedSelector, + new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet)); + }*/ + + /** + * Returns the {@link SelectorProvider} used by this {@link NioEventLoop} to obtain the {@link Selector}. + */ + public SelectorProvider selectorProvider() { + return provider; + } + + @Override + protected Queue newTaskQueue(int maxPendingTasks) { + return newTaskQueue0(maxPendingTasks); + } + + private static Queue newTaskQueue0(int maxPendingTasks) { + // This event loop never calls takeTask() + return null; //maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue() : PlatformDependent.newMpscQueue(maxPendingTasks); + } + + /** + * Registers an arbitrary {@link SelectableChannel}, not necessarily created by Netty, to the {@link Selector} + * of this event loop. Once the specified {@link SelectableChannel} is registered, the specified {@code task} will + * be executed by this event loop when the {@link SelectableChannel} is ready. + */ + public void register(final SelectableChannel ch, final int interestOps, final NioTask task) { + Objects.requireNonNull(ch, "ch"); + if (interestOps == 0) { + throw new IllegalArgumentException("interestOps must be non-zero."); + } + if ((interestOps & ~ch.validOps()) != 0) { + throw new IllegalArgumentException( + "invalid interestOps: " + interestOps + "(validOps: " + ch.validOps() + ')'); + } + Objects.requireNonNull(task, "task"); + + if (isShutdown()) { + throw new IllegalStateException("event loop shut down"); + } + + if (inEventLoop()) { + register0(ch, interestOps, task); + } else { + try { + // Offload to the EventLoop as otherwise java.nio.channels.spi.AbstractSelectableChannel.register + // may block for a long time while trying to obtain an internal lock that may be hold while selecting. + submit(new Runnable() { + @Override + public void run() { + register0(ch, interestOps, task); + } + }).sync(); + } catch (InterruptedException ignore) { + // Even if interrupted we did schedule it so just mark the Thread as interrupted. + Thread.currentThread().interrupt(); + } + } + } + + private void register0(SelectableChannel ch, int interestOps, NioTask task) { + try { + ch.register(unwrappedSelector, interestOps, task); + } catch (Exception e) { + throw new EventLoopException("failed to register a channel", e); + } + } + + /** + * Returns the percentage of the desired amount of time spent for I/O in the event loop. + */ + public int getIoRatio() { + return ioRatio; + } + + /** + * Sets the percentage of the desired amount of time spent for I/O in the event loop. Value range from 1-100. + * The default value is {@code 50}, which means the event loop will try to spend the same amount of time for I/O + * as for non-I/O tasks. The lower the number the more time can be spent on non-I/O tasks. If value set to + * {@code 100}, this feature will be disabled and event loop will not attempt to balance I/O and non-I/O tasks. + */ + public void setIoRatio(int ioRatio) { + if (ioRatio <= 0 || ioRatio > 100) { + throw new IllegalArgumentException("ioRatio: " + ioRatio + " (expected: 0 < ioRatio <= 100)"); + } + this.ioRatio = ioRatio; + } + + /** + * Replaces the current {@link Selector} of this event loop with newly created {@link Selector}s to work + * around the infamous epoll 100% CPU bug. + */ + /*public void rebuildSelector() { + if (!inEventLoop()) { + execute(this::rebuildSelector0); + return; + } + rebuildSelector0(); + }*/ + + /*@Override + public int registeredChannels() { + return selector.keys().size() - cancelledKeys; + }*/ + + /*private void rebuildSelector0() { + final Selector oldSelector = selector; + final SelectorTuple newSelectorTuple; + + if (oldSelector == null) { + return; + } + + try { + newSelectorTuple = openSelector(); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to create a new Selector.", e); + return; + } + + // Register all channels to the new Selector. + int nChannels = 0; + for (SelectionKey key: oldSelector.keys()) { + Object a = key.attachment(); + try { + if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) { + continue; + } + + int interestOps = key.interestOps(); + key.cancel(); + SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a); + if (a instanceof AbstractNioChannel) { + // Update SelectionKey + ((AbstractNioChannel) a).selectionKey = newKey; + } + nChannels ++; + } catch (Exception e) { + logger.warn("Failed to re-register a Channel to the new Selector.", e); + if (a instanceof AbstractNioChannel) { + AbstractNioChannel ch = (AbstractNioChannel) a; + ch.unsafe().close(ch.unsafe().voidPromise()); + } else { + @SuppressWarnings("unchecked") + NioTask task = (NioTask) a; + invokeChannelUnregistered(task, key, e); + } + } + } + + selector = newSelectorTuple.selector; + unwrappedSelector = newSelectorTuple.unwrappedSelector; + + try { + // time to close the old selector as everything else is registered to the new one + oldSelector.close(); + } catch (Throwable t) { + if (logger.isWarnEnabled()) { + logger.warn("Failed to close the old Selector.", t); + } + } + + if (logger.isInfoEnabled()) { + logger.info("Migrated " + nChannels + " channel(s) to the new Selector."); + } + }*/ + + @Override + protected void run() { + int selectCnt = 0; + for (;;) { + try { + int strategy; + try { + strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks()); + switch (strategy) { + case SelectStrategy.CONTINUE: + continue; + + case SelectStrategy.BUSY_WAIT: + // fall-through to SELECT since the busy-wait is not supported with NIO + + case SelectStrategy.SELECT: + long curDeadlineNanos = nextScheduledTaskDeadlineNanos(); + if (curDeadlineNanos == -1L) { + curDeadlineNanos = NONE; // nothing on the calendar + } + nextWakeupNanos.set(curDeadlineNanos); + try { + if (!hasTasks()) { + strategy = select(curDeadlineNanos); + } + } finally { + // This update is just to help block unnecessary selector wakeups + // so use of lazySet is ok (no race condition) + nextWakeupNanos.lazySet(AWAKE); + } + // fall through + default: + } + } catch (IOException e) { + // If we receive an IOException here its because the Selector is messed up. Let's rebuild + // the selector and retry. https://github.com/netty/netty/issues/8566 + //rebuildSelector0(); + selectCnt = 0; + handleLoopException(e); + continue; + } + + selectCnt++; + cancelledKeys = 0; + needsToSelectAgain = false; + final int ioRatio = this.ioRatio; + boolean ranTasks; + if (ioRatio == 100) { + try { + if (strategy > 0) { + //processSelectedKeys(); + } + } finally { + // Ensure we always run tasks. + ranTasks = runAllTasks(); + } + } else if (strategy > 0) { + final long ioStartTime = System.nanoTime(); + try { + //processSelectedKeys(); + } finally { + // Ensure we always run tasks. + final long ioTime = System.nanoTime() - ioStartTime; + ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio); + } + } else { + ranTasks = runAllTasks(0); // This will run the minimum number of tasks + } + + if (ranTasks || strategy > 0) { + if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Selector.select() returned prematurely " + (selectCnt -1) + " times in a row for Selector " + selector); + } + selectCnt = 0; + } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case) + selectCnt = 0; + } + } catch (CancelledKeyException e) { + // Harmless exception - log anyway + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, CancelledKeyException.class.getSimpleName() + " raised by a Selector "+ selector + " - JDK bug?", e); + } + } catch (Error e) { + throw (Error) e; + } catch (Throwable t) { + handleLoopException(t); + } finally { + // Always handle shutdown even if the loop processing threw an exception. + try { + if (isShuttingDown()) { + //closeAll(); + if (confirmShutdown()) { + return; + } + } + } catch (Error e) { + throw (Error) e; + } catch (Throwable t) { + handleLoopException(t); + } + } + } + } + + // returns true if selectCnt should be reset + private boolean unexpectedSelectorWakeup(int selectCnt) { + if (Thread.interrupted()) { + // Thread was interrupted so reset selected keys and break so we not run into a busy loop. + // As this is most likely a bug in the handler of the user or it's client library we will + // also log it. + // + // See https://github.com/netty/netty/issues/2426 + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Selector.select() returned prematurely because " + + "Thread.currentThread().interrupt() was called. Use " + + "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop."); + } + return true; + } + if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 && + selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) { + // The selector returned prematurely many times in a row. + // Rebuild the selector to work around the problem. + logger.log(Level.WARNING, "Selector.select() returned prematurely " + selectCnt + " times in a row; rebuilding Selector " + selector); + //rebuildSelector(); + return true; + } + return false; + } + + private static void handleLoopException(Throwable t) { + logger.log(Level.WARNING, "Unexpected exception in the selector loop.", t); + + // Prevent possible consecutive immediate failures that lead to + // excessive CPU consumption. + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Ignore. + } + } + + /*private void processSelectedKeys() { + if (selectedKeys != null) { + processSelectedKeysOptimized(); + } else { + processSelectedKeysPlain(selector.selectedKeys()); + } + }*/ + + @Override + protected void cleanup() { + try { + selector.close(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to close a selector.", e); + } + } + + void cancel(SelectionKey key) { + key.cancel(); + cancelledKeys ++; + if (cancelledKeys >= CLEANUP_INTERVAL) { + cancelledKeys = 0; + needsToSelectAgain = true; + } + } + + /*private void processSelectedKeysPlain(Set selectedKeys) { + // check if the set is empty and if so just return to not create garbage by + // creating a new Iterator every time even if there is nothing to process. + // See https://github.com/netty/netty/issues/597 + if (selectedKeys.isEmpty()) { + return; + } + + Iterator i = selectedKeys.iterator(); + for (;;) { + final SelectionKey k = i.next(); + final Object a = k.attachment(); + i.remove(); + + if (a instanceof AbstractNioChannel) { + processSelectedKey(k, (AbstractNioChannel) a); + } else { + @SuppressWarnings("unchecked") + NioTask task = (NioTask) a; + processSelectedKey(k, task); + } + + if (!i.hasNext()) { + break; + } + + if (needsToSelectAgain) { + selectAgain(); + selectedKeys = selector.selectedKeys(); + + // Create the iterator again to avoid ConcurrentModificationException + if (selectedKeys.isEmpty()) { + break; + } else { + i = selectedKeys.iterator(); + } + } + } + }*/ + + /*private void processSelectedKeysOptimized() { + for (int i = 0; i < selectedKeys.size; ++i) { + final SelectionKey k = selectedKeys.keys[i]; + // null out entry in the array to allow to have it GC'ed once the Channel close + // See https://github.com/netty/netty/issues/2363 + selectedKeys.keys[i] = null; + + final Object a = k.attachment(); + + if (a instanceof AbstractNioChannel) { + processSelectedKey(k, (AbstractNioChannel) a); + } else { + @SuppressWarnings("unchecked") + NioTask task = (NioTask) a; + processSelectedKey(k, task); + } + + if (needsToSelectAgain) { + // null out entries in the array to allow to have it GC'ed once the Channel close + // See https://github.com/netty/netty/issues/2363 + selectedKeys.reset(i + 1); + + selectAgain(); + i = -1; + } + } + }*/ + + /*private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { + final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); + if (!k.isValid()) { + final EventLoop eventLoop; + try { + eventLoop = ch.eventLoop(); + } catch (Throwable ignored) { + // If the channel implementation throws an exception because there is no event loop, we ignore this + // because we are only trying to determine if ch is registered to this event loop and thus has authority + // to close ch. + return; + } + // Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop + // and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is + // still healthy and should not be closed. + // See https://github.com/netty/netty/issues/5125 + if (eventLoop == this) { + // close the channel if the key is not valid anymore + unsafe.close(unsafe.voidPromise()); + } + return; + } + + try { + int readyOps = k.readyOps(); + // We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise + // the NIO JDK channel implementation may throw a NotYetConnectedException. + if ((readyOps & SelectionKey.OP_CONNECT) != 0) { + // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking + // See https://github.com/netty/netty/issues/924 + int ops = k.interestOps(); + ops &= ~SelectionKey.OP_CONNECT; + k.interestOps(ops); + + unsafe.finishConnect(); + } + + // Process OP_WRITE first as we may be able to write some queued buffers and so free memory. + if ((readyOps & SelectionKey.OP_WRITE) != 0) { + // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write + ch.unsafe().forceFlush(); + } + + // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead + // to a spin loop + if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) { + unsafe.read(); + } + } catch (CancelledKeyException ignored) { + unsafe.close(unsafe.voidPromise()); + } + }*/ + + private static void processSelectedKey(SelectionKey k, NioTask task) { + int state = 0; + try { + task.channelReady(k.channel(), k); + state = 1; + } catch (Exception e) { + k.cancel(); + invokeChannelUnregistered(task, k, e); + state = 2; + } finally { + switch (state) { + case 0: + k.cancel(); + invokeChannelUnregistered(task, k, null); + break; + case 1: + if (!k.isValid()) { // Cancelled by channelReady() + invokeChannelUnregistered(task, k, null); + } + break; + default: + break; + } + } + } + + /*private void closeAll() { + selectAgain(); + Set keys = selector.keys(); + Collection channels = new ArrayList(keys.size()); + for (SelectionKey k: keys) { + Object a = k.attachment(); + if (a instanceof AbstractNioChannel) { + channels.add((AbstractNioChannel) a); + } else { + k.cancel(); + @SuppressWarnings("unchecked") + NioTask task = (NioTask) a; + invokeChannelUnregistered(task, k, null); + } + } + + for (AbstractNioChannel ch: channels) { + ch.unsafe().close(ch.unsafe().voidPromise()); + } + }*/ + + private static void invokeChannelUnregistered(NioTask task, SelectionKey k, Throwable cause) { + try { + task.channelUnregistered(k.channel(), cause); + } catch (Exception e) { + logger.log(Level.WARNING, "Unexpected exception while running NioTask.channelUnregistered()", e); + } + } + + @Override + protected void wakeup(boolean inEventLoop) { + if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) { + selector.wakeup(); + } + } + + @Override + protected boolean beforeScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + @Override + protected boolean afterScheduledTaskSubmitted(long deadlineNanos) { + // Note this is also correct for the nextWakeupNanos == -1 (AWAKE) case + return deadlineNanos < nextWakeupNanos.get(); + } + + Selector unwrappedSelector() { + return unwrappedSelector; + } + + int selectNow() throws IOException { + return selector.selectNow(); + } + + private int select(long deadlineNanos) throws IOException { + if (deadlineNanos == NONE) { + return selector.select(); + } + // Timeout will only be 0 if deadline is within 5 microsecs + long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L; + return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis); + } + + private void selectAgain() { + needsToSelectAgain = false; + try { + selector.selectNow(); + } catch (Throwable t) { + logger.log(Level.WARNING, "Failed to update SelectionKeys.", t); + } + } +} diff --git a/src/main/java/org/xbib/event/loop/nio/NioEventLoopGroup.java b/src/main/java/org/xbib/event/loop/nio/NioEventLoopGroup.java new file mode 100644 index 0000000..2ec4141 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/nio/NioEventLoopGroup.java @@ -0,0 +1,176 @@ +package org.xbib.event.loop.nio; + +import org.xbib.event.loop.selector.DefaultSelectStrategyFactory; +import org.xbib.event.loop.EventExecutor; +import org.xbib.event.loop.EventExecutorChooserFactory; +import org.xbib.event.loop.EventLoop; +import org.xbib.event.loop.EventLoopTaskQueueFactory; +import org.xbib.event.loop.MultithreadEventLoopGroup; +import org.xbib.event.loop.RejectedExecutionHandler; +import org.xbib.event.loop.RejectedExecutionHandlers; +import org.xbib.event.loop.selector.SelectStrategyFactory; +import org.xbib.event.loop.SingleThreadEventLoop; + +import java.nio.channels.Selector; +import java.nio.channels.spi.SelectorProvider; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * {@link MultithreadEventLoopGroup} implementations which is used for NIO {@link Selector}. + */ +public class NioEventLoopGroup extends MultithreadEventLoopGroup { + + /** + * Create a new instance using the default number of threads, the default {@link ThreadFactory} and + * the {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. + */ + public NioEventLoopGroup() { + this(0); + } + + /** + * Create a new instance using the specified number of threads, {@link ThreadFactory} and the + * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. + */ + public NioEventLoopGroup(int nThreads) { + this(nThreads, (Executor) null); + } + + /** + * Create a new instance using the default number of threads, the given {@link ThreadFactory} and the + * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. + */ + public NioEventLoopGroup(ThreadFactory threadFactory) { + this(0, threadFactory, SelectorProvider.provider()); + } + + /** + * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the + * {@link SelectorProvider} which is returned by {@link SelectorProvider#provider()}. + */ + public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory) { + this(nThreads, threadFactory, SelectorProvider.provider()); + } + + public NioEventLoopGroup(int nThreads, Executor executor) { + this(nThreads, executor, SelectorProvider.provider()); + } + + /** + * Create a new instance using the specified number of threads, the given {@link ThreadFactory} and the given + * {@link SelectorProvider}. + */ + public NioEventLoopGroup( + int nThreads, ThreadFactory threadFactory, final SelectorProvider selectorProvider) { + this(nThreads, threadFactory, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); + } + + public NioEventLoopGroup(int nThreads, ThreadFactory threadFactory, + final SelectorProvider selectorProvider, final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, threadFactory, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup( + int nThreads, Executor executor, final SelectorProvider selectorProvider) { + this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); + } + + public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, + RejectedExecutionHandlers.reject()); + } + + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory, + final RejectedExecutionHandler rejectedExecutionHandler) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, rejectedExecutionHandler); + } + + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + final SelectorProvider selectorProvider, + final SelectStrategyFactory selectStrategyFactory, + final RejectedExecutionHandler rejectedExecutionHandler, + final EventLoopTaskQueueFactory taskQueueFactory) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, + rejectedExecutionHandler, taskQueueFactory); + } + + /** + * @param nThreads the number of threads that will be used by this instance. + * @param executor the Executor to use, or {@code null} if default one should be used. + * @param chooserFactory the {@link EventExecutorChooserFactory} to use. + * @param selectorProvider the {@link SelectorProvider} to use. + * @param selectStrategyFactory the {@link SelectStrategyFactory} to use. + * @param rejectedExecutionHandler the {@link RejectedExecutionHandler} to use. + * @param taskQueueFactory the {@link EventLoopTaskQueueFactory} to use for + * {@link SingleThreadEventLoop#execute(Runnable)}, + * or {@code null} if default one should be used. + * @param tailTaskQueueFactory the {@link EventLoopTaskQueueFactory} to use for + * {@link SingleThreadEventLoop#executeAfterEventLoopIteration(Runnable)}, + * or {@code null} if default one should be used. + */ + public NioEventLoopGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, + SelectorProvider selectorProvider, + SelectStrategyFactory selectStrategyFactory, + RejectedExecutionHandler rejectedExecutionHandler, + EventLoopTaskQueueFactory taskQueueFactory, + EventLoopTaskQueueFactory tailTaskQueueFactory) { + super(nThreads, executor, chooserFactory, selectorProvider, selectStrategyFactory, + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } + + /** + * Sets the percentage of the desired amount of time spent for I/O in the child event loops. The default value is + * {@code 50}, which means the event loop will try to spend the same amount of time for I/O as for non-I/O tasks. + */ + public void setIoRatio(int ioRatio) { + for (EventExecutor e: this) { + ((NioEventLoop) e).setIoRatio(ioRatio); + } + } + + /** + * Replaces the current {@link Selector}s of the child event loops with newly created {@link Selector}s to work + * around the infamous epoll 100% CPU bug. + */ + /*public void rebuildSelectors() { + for (EventExecutor e: this) { + ((NioEventLoop) e).rebuildSelector(); + } + }*/ + + @Override + protected EventLoop newChild(Executor executor, Object... args) throws Exception { + SelectorProvider selectorProvider = (SelectorProvider) args[0]; + SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1]; + RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2]; + EventLoopTaskQueueFactory taskQueueFactory = null; + EventLoopTaskQueueFactory tailTaskQueueFactory = null; + + int argsLength = args.length; + if (argsLength > 3) { + taskQueueFactory = (EventLoopTaskQueueFactory) args[3]; + } + if (argsLength > 4) { + tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4]; + } + return new NioEventLoop(this, executor, selectorProvider, + selectStrategyFactory.newSelectStrategy(), + rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory); + } + + @Override + public List shutdownNow() { + throw new UnsupportedOperationException("use shutdownGracefully()"); + } +} diff --git a/src/main/java/org/xbib/event/loop/nio/NioTask.java b/src/main/java/org/xbib/event/loop/nio/NioTask.java new file mode 100644 index 0000000..181f30b --- /dev/null +++ b/src/main/java/org/xbib/event/loop/nio/NioTask.java @@ -0,0 +1,26 @@ +package org.xbib.event.loop.nio; + +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; + +/** + * An arbitrary task that can be executed by {@link NioEventLoop} when a {@link SelectableChannel} becomes ready. + * + * @see NioEventLoop#register(SelectableChannel, int, NioTask) + */ +public interface NioTask { + /** + * Invoked when the {@link SelectableChannel} has been selected by the {@link Selector}. + */ + void channelReady(C ch, SelectionKey key) throws Exception; + + /** + * Invoked when the {@link SelectionKey} of the specified {@link SelectableChannel} has been cancelled and thus + * this {@link NioTask} will not be notified anymore. + * + * @param cause the cause of the unregistration. {@code null} if a user called {@link SelectionKey#cancel()} or + * the event loop has been shut down. + */ + void channelUnregistered(C ch, Throwable cause) throws Exception; +} diff --git a/src/main/java/org/xbib/event/loop/nio/SelectedSelectionKeySet.java b/src/main/java/org/xbib/event/loop/nio/SelectedSelectionKeySet.java new file mode 100644 index 0000000..93021ae --- /dev/null +++ b/src/main/java/org/xbib/event/loop/nio/SelectedSelectionKeySet.java @@ -0,0 +1,86 @@ +package org.xbib.event.loop.nio; + +import java.nio.channels.SelectionKey; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + +final class SelectedSelectionKeySet extends AbstractSet { + + SelectionKey[] keys; + int size; + + SelectedSelectionKeySet() { + keys = new SelectionKey[1024]; + } + + @Override + public boolean add(SelectionKey o) { + if (o == null) { + return false; + } + + keys[size++] = o; + if (size == keys.length) { + increaseCapacity(); + } + + return true; + } + + @Override + public boolean remove(Object o) { + return false; + } + + @Override + public boolean contains(Object o) { + return false; + } + + @Override + public int size() { + return size; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private int idx; + + @Override + public boolean hasNext() { + return idx < size; + } + + @Override + public SelectionKey next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return keys[idx++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + void reset() { + reset(0); + } + + void reset(int start) { + Arrays.fill(keys, start, size, null); + size = 0; + } + + private void increaseCapacity() { + SelectionKey[] newKeys = new SelectionKey[keys.length << 1]; + System.arraycopy(keys, 0, newKeys, 0, size); + keys = newKeys; + } +} diff --git a/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategy.java b/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategy.java new file mode 100644 index 0000000..e2f6ae8 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategy.java @@ -0,0 +1,17 @@ +package org.xbib.event.loop.selector; + +import org.xbib.event.util.IntSupplier; + +/** + * Default select strategy. + */ +final class DefaultSelectStrategy implements SelectStrategy { + static final SelectStrategy INSTANCE = new DefaultSelectStrategy(); + + private DefaultSelectStrategy() { } + + @Override + public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception { + return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT; + } +} diff --git a/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategyFactory.java b/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategyFactory.java new file mode 100644 index 0000000..1f6a5a8 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/selector/DefaultSelectStrategyFactory.java @@ -0,0 +1,15 @@ +package org.xbib.event.loop.selector; + +/** + * Factory which uses the default select strategy. + */ +public final class DefaultSelectStrategyFactory implements SelectStrategyFactory { + public static final SelectStrategyFactory INSTANCE = new DefaultSelectStrategyFactory(); + + private DefaultSelectStrategyFactory() { } + + @Override + public SelectStrategy newSelectStrategy() { + return DefaultSelectStrategy.INSTANCE; + } +} diff --git a/src/main/java/org/xbib/event/loop/selector/SelectStrategy.java b/src/main/java/org/xbib/event/loop/selector/SelectStrategy.java new file mode 100644 index 0000000..adcc912 --- /dev/null +++ b/src/main/java/org/xbib/event/loop/selector/SelectStrategy.java @@ -0,0 +1,37 @@ +package org.xbib.event.loop.selector; + +import org.xbib.event.util.IntSupplier; + +/** + * Select strategy interface. + * + * Provides the ability to control the behavior of the select loop. For example a blocking select + * operation can be delayed or skipped entirely if there are events to process immediately. + */ +public interface SelectStrategy { + + /** + * Indicates a blocking select should follow. + */ + int SELECT = -1; + /** + * Indicates the IO loop should be retried, no blocking select to follow directly. + */ + int CONTINUE = -2; + /** + * Indicates the IO loop to poll for new events without blocking. + */ + int BUSY_WAIT = -3; + + /** + * The {@link SelectStrategy} can be used to steer the outcome of a potential select + * call. + * + * @param selectSupplier The supplier with the result of a select result. + * @param hasTasks true if tasks are waiting to be processed. + * @return {@link #SELECT} if the next step should be blocking select {@link #CONTINUE} if + * the next step should be to not select but rather jump back to the IO loop and try + * again. Any value >= 0 is treated as an indicator that work needs to be done. + */ + int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception; +} diff --git a/src/main/java/org/xbib/event/loop/selector/SelectStrategyFactory.java b/src/main/java/org/xbib/event/loop/selector/SelectStrategyFactory.java new file mode 100644 index 0000000..40907ad --- /dev/null +++ b/src/main/java/org/xbib/event/loop/selector/SelectStrategyFactory.java @@ -0,0 +1,12 @@ +package org.xbib.event.loop.selector; + +/** + * Factory that creates a new {@link SelectStrategy} every time. + */ +public interface SelectStrategyFactory { + + /** + * Creates a new {@link SelectStrategy}. + */ + SelectStrategy newSelectStrategy(); +} diff --git a/src/main/java/org/xbib/event/thread/DefaultThreadFactory.java b/src/main/java/org/xbib/event/thread/DefaultThreadFactory.java new file mode 100644 index 0000000..20f8b32 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/DefaultThreadFactory.java @@ -0,0 +1,108 @@ +package org.xbib.event.thread; + +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A {@link ThreadFactory} implementation with a simple naming rule. + */ +public class DefaultThreadFactory implements ThreadFactory { + + private static final AtomicInteger poolId = new AtomicInteger(); + + private final AtomicInteger nextId = new AtomicInteger(); + private final String prefix; + private final boolean daemon; + private final int priority; + protected final ThreadGroup threadGroup; + + public DefaultThreadFactory(Class poolType) { + this(poolType, false, Thread.NORM_PRIORITY); + } + + public DefaultThreadFactory(String poolName) { + this(poolName, false, Thread.NORM_PRIORITY); + } + + public DefaultThreadFactory(Class poolType, boolean daemon) { + this(poolType, daemon, Thread.NORM_PRIORITY); + } + + public DefaultThreadFactory(String poolName, boolean daemon) { + this(poolName, daemon, Thread.NORM_PRIORITY); + } + + public DefaultThreadFactory(Class poolType, int priority) { + this(poolType, false, priority); + } + + public DefaultThreadFactory(String poolName, int priority) { + this(poolName, false, priority); + } + + public DefaultThreadFactory(Class poolType, boolean daemon, int priority) { + this(toPoolName(poolType), daemon, priority); + } + + public static String toPoolName(Class poolType) { + Objects.requireNonNull(poolType, "poolType"); + + String poolName = poolType.getSimpleName(); + switch (poolName.length()) { + case 0 -> { + return "unknown"; + } + case 1 -> { + return poolName.toLowerCase(Locale.US); + } + default -> { + if (Character.isUpperCase(poolName.charAt(0)) && Character.isLowerCase(poolName.charAt(1))) { + return Character.toLowerCase(poolName.charAt(0)) + poolName.substring(1); + } else { + return poolName; + } + } + } + } + + public DefaultThreadFactory(String poolName, boolean daemon, int priority, ThreadGroup threadGroup) { + Objects.requireNonNull(poolName, "poolName"); + + if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) { + throw new IllegalArgumentException( + "priority: " + priority + " (expected: Thread.MIN_PRIORITY <= priority <= Thread.MAX_PRIORITY)"); + } + + prefix = poolName + '-' + poolId.incrementAndGet() + '-'; + this.daemon = daemon; + this.priority = priority; + this.threadGroup = threadGroup; + } + + public DefaultThreadFactory(String poolName, boolean daemon, int priority) { + this(poolName, daemon, priority, null); + } + + @Override + public Thread newThread(Runnable r) { + Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet()); + try { + if (t.isDaemon() != daemon) { + t.setDaemon(daemon); + } + + if (t.getPriority() != priority) { + t.setPriority(priority); + } + } catch (Exception ignored) { + // Doesn't matter even if failed to set. + } + return t; + } + + protected Thread newThread(Runnable r, String name) { + return new FastThreadLocalThread(threadGroup, r, name); + } +} diff --git a/src/main/java/org/xbib/event/thread/FastThreadLocal.java b/src/main/java/org/xbib/event/thread/FastThreadLocal.java new file mode 100644 index 0000000..e3fe5f3 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/FastThreadLocal.java @@ -0,0 +1,258 @@ +package org.xbib.event.thread; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * A special variant of {@link ThreadLocal} that yields higher access performance when accessed from a + * {@link FastThreadLocalThread}. + *

+ * Internally, a {@link FastThreadLocal} uses a constant index in an array, instead of using hash code and hash table, + * to look for a variable. Although seemingly very subtle, it yields slight performance advantage over using a hash + * table, and it is useful when accessed frequently. + *

+ * To take advantage of this thread-local variable, your thread must be a {@link FastThreadLocalThread} or its subtype. + * By default, all threads created by {@link DefaultThreadFactory} are {@link FastThreadLocalThread} due to this reason. + *

+ * Note that the fast path is only possible on threads that extend {@link FastThreadLocalThread}, because it requires + * a special field to store the necessary state. An access by any other kind of thread falls back to a regular + * {@link ThreadLocal}. + *

+ * + * @param the type of the thread-local variable + * @see ThreadLocal + */ +public class FastThreadLocal { + + /** + * Removes all {@link FastThreadLocal} variables bound to the current thread. This operation is useful when you + * are in a container environment, and you don't want to leave the thread local variables in the threads you do not + * manage. + */ + public static void removeAll() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap == null) { + return; + } + + try { + Object v = threadLocalMap.indexedVariable(InternalThreadLocalMap.VARIABLES_TO_REMOVE_INDEX); + if (v != null && v != InternalThreadLocalMap.UNSET) { + @SuppressWarnings("unchecked") + Set> variablesToRemove = (Set>) v; + FastThreadLocal[] variablesToRemoveArray = + variablesToRemove.toArray(new FastThreadLocal[0]); + for (FastThreadLocal tlv: variablesToRemoveArray) { + tlv.remove(threadLocalMap); + } + } + } finally { + InternalThreadLocalMap.remove(); + } + } + + /** + * Returns the number of thread local variables bound to the current thread. + */ + public static int size() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap == null) { + return 0; + } else { + return threadLocalMap.size(); + } + } + + /** + * Destroys the data structure that keeps all {@link FastThreadLocal} variables accessed from + * non-{@link FastThreadLocalThread}s. This operation is useful when you are in a container environment, and you + * do not want to leave the thread local variables in the threads you do not manage. Call this method when your + * application is being unloaded from the container. + */ + public static void destroy() { + InternalThreadLocalMap.destroy(); + } + + @SuppressWarnings("unchecked") + private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal variable) { + Object v = threadLocalMap.indexedVariable(InternalThreadLocalMap.VARIABLES_TO_REMOVE_INDEX); + Set> variablesToRemove; + if (v == InternalThreadLocalMap.UNSET || v == null) { + variablesToRemove = Collections.newSetFromMap(new IdentityHashMap, Boolean>()); + threadLocalMap.setIndexedVariable(InternalThreadLocalMap.VARIABLES_TO_REMOVE_INDEX, variablesToRemove); + } else { + variablesToRemove = (Set>) v; + } + + variablesToRemove.add(variable); + } + + private static void removeFromVariablesToRemove( + InternalThreadLocalMap threadLocalMap, FastThreadLocal variable) { + + Object v = threadLocalMap.indexedVariable(InternalThreadLocalMap.VARIABLES_TO_REMOVE_INDEX); + + if (v == InternalThreadLocalMap.UNSET || v == null) { + return; + } + + @SuppressWarnings("unchecked") + Set> variablesToRemove = (Set>) v; + variablesToRemove.remove(variable); + } + + private final int index; + + public FastThreadLocal() { + index = InternalThreadLocalMap.nextVariableIndex(); + } + + /** + * Returns the current value for the current thread + */ + @SuppressWarnings("unchecked") + public final V get() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; + } + + return initialize(threadLocalMap); + } + + /** + * Returns the current value for the current thread if it exists, {@code null} otherwise. + */ + @SuppressWarnings("unchecked") + public final V getIfExists() { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet(); + if (threadLocalMap != null) { + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; + } + } + return null; + } + + /** + * Returns the current value for the specified thread local map. + * The specified thread local map must be for the current thread. + */ + @SuppressWarnings("unchecked") + public final V get(InternalThreadLocalMap threadLocalMap) { + Object v = threadLocalMap.indexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + return (V) v; + } + + return initialize(threadLocalMap); + } + + private V initialize(InternalThreadLocalMap threadLocalMap) { + V v = null; + try { + v = initialValue(); + if (v == InternalThreadLocalMap.UNSET) { + throw new IllegalArgumentException("InternalThreadLocalMap.UNSET can not be initial value."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + threadLocalMap.setIndexedVariable(index, v); + addToVariablesToRemove(threadLocalMap, this); + return v; + } + + /** + * Set the value for the current thread. + */ + public final void set(V value) { + if (value != InternalThreadLocalMap.UNSET) { + InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get(); + setKnownNotUnset(threadLocalMap, value); + } else { + remove(); + } + } + + /** + * Set the value for the specified thread local map. The specified thread local map must be for the current thread. + */ + public final void set(InternalThreadLocalMap threadLocalMap, V value) { + if (value != InternalThreadLocalMap.UNSET) { + setKnownNotUnset(threadLocalMap, value); + } else { + remove(threadLocalMap); + } + } + + /** + * @see InternalThreadLocalMap#setIndexedVariable(int, Object). + */ + private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) { + if (threadLocalMap.setIndexedVariable(index, value)) { + addToVariablesToRemove(threadLocalMap, this); + } + } + + /** + * Returns {@code true} if and only if this thread-local variable is set. + */ + public final boolean isSet() { + return isSet(InternalThreadLocalMap.getIfSet()); + } + + /** + * Returns {@code true} if and only if this thread-local variable is set. + * The specified thread local map must be for the current thread. + */ + public final boolean isSet(InternalThreadLocalMap threadLocalMap) { + return threadLocalMap != null && threadLocalMap.isIndexedVariableSet(index); + } + /** + * Sets the value to uninitialized for the specified thread local map. + * After this, any subsequent call to get() will trigger a new call to initialValue(). + */ + public final void remove() { + remove(InternalThreadLocalMap.getIfSet()); + } + + /** + * Sets the value to uninitialized for the specified thread local map. + * After this, any subsequent call to get() will trigger a new call to initialValue(). + * The specified thread local map must be for the current thread. + */ + @SuppressWarnings("unchecked") + public final void remove(InternalThreadLocalMap threadLocalMap) { + if (threadLocalMap == null) { + return; + } + + Object v = threadLocalMap.removeIndexedVariable(index); + if (v != InternalThreadLocalMap.UNSET) { + removeFromVariablesToRemove(threadLocalMap, this); + try { + onRemoval((V) v); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Returns the initial value for this thread-local variable. + */ + protected V initialValue() throws Exception { + return null; + } + + /** + * Invoked when this thread local variable is removed by {@link #remove()}. Be aware that {@link #remove()} + * is not guaranteed to be called when the `Thread` completes which means you can not depend on this for + * cleanup of the resources in the case of `Thread` completion. + */ + protected void onRemoval(@SuppressWarnings("UnusedParameters") V value) throws Exception { } +} diff --git a/src/main/java/org/xbib/event/thread/FastThreadLocalRunnable.java b/src/main/java/org/xbib/event/thread/FastThreadLocalRunnable.java new file mode 100644 index 0000000..0b8e599 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/FastThreadLocalRunnable.java @@ -0,0 +1,24 @@ +package org.xbib.event.thread; + +import java.util.Objects; + +final class FastThreadLocalRunnable implements Runnable { + private final Runnable runnable; + + private FastThreadLocalRunnable(Runnable runnable) { + this.runnable = Objects.requireNonNull(runnable, "runnable"); + } + + @Override + public void run() { + try { + runnable.run(); + } finally { + FastThreadLocal.removeAll(); + } + } + + static Runnable wrap(Runnable runnable) { + return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable); + } +} diff --git a/src/main/java/org/xbib/event/thread/FastThreadLocalThread.java b/src/main/java/org/xbib/event/thread/FastThreadLocalThread.java new file mode 100644 index 0000000..a7baa08 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/FastThreadLocalThread.java @@ -0,0 +1,110 @@ +package org.xbib.event.thread; + + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A special {@link Thread} that provides fast access to {@link FastThreadLocal} variables. + */ +public class FastThreadLocalThread extends Thread { + + private static final Logger logger = Logger.getLogger(FastThreadLocalThread.class.getName()); + + // This will be set to true if we have a chance to wrap the Runnable. + private final boolean cleanupFastThreadLocals; + + private InternalThreadLocalMap threadLocalMap; + + public FastThreadLocalThread() { + cleanupFastThreadLocals = false; + } + + public FastThreadLocalThread(Runnable target) { + super(FastThreadLocalRunnable.wrap(target)); + cleanupFastThreadLocals = true; + } + + public FastThreadLocalThread(ThreadGroup group, Runnable target) { + super(group, FastThreadLocalRunnable.wrap(target)); + cleanupFastThreadLocals = true; + } + + public FastThreadLocalThread(String name) { + super(name); + cleanupFastThreadLocals = false; + } + + public FastThreadLocalThread(ThreadGroup group, String name) { + super(group, name); + cleanupFastThreadLocals = false; + } + + public FastThreadLocalThread(Runnable target, String name) { + super(FastThreadLocalRunnable.wrap(target), name); + cleanupFastThreadLocals = true; + } + + public FastThreadLocalThread(ThreadGroup group, Runnable target, String name) { + super(group, FastThreadLocalRunnable.wrap(target), name); + cleanupFastThreadLocals = true; + } + + public FastThreadLocalThread(ThreadGroup group, Runnable target, String name, long stackSize) { + super(group, FastThreadLocalRunnable.wrap(target), name, stackSize); + cleanupFastThreadLocals = true; + } + + /** + * Returns the internal data structure that keeps the thread-local variables bound to this thread. + * Note that this method is for internal use only, and thus is subject to change at any time. + */ + public final InternalThreadLocalMap threadLocalMap() { + if (this != Thread.currentThread() && logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "It's not thread-safe to get 'threadLocalMap' " + + "which doesn't belong to the caller thread"); + } + return threadLocalMap; + } + + /** + * Sets the internal data structure that keeps the thread-local variables bound to this thread. + * Note that this method is for internal use only, and thus is subject to change at any time. + */ + public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) { + if (this != Thread.currentThread() && logger.isLoggable(Level.WARNING)) { + logger.log(Level.WARNING, "It's not thread-safe to set 'threadLocalMap' " + + "which doesn't belong to the caller thread"); + } + this.threadLocalMap = threadLocalMap; + } + + /** + * Returns {@code true} if {@link FastThreadLocal#removeAll()} will be called once {@link #run()} completes. + */ + public boolean willCleanupFastThreadLocals() { + return cleanupFastThreadLocals; + } + + /** + * Returns {@code true} if {@link FastThreadLocal#removeAll()} will be called once {@link Thread#run()} completes. + */ + public static boolean willCleanupFastThreadLocals(Thread thread) { + return thread instanceof FastThreadLocalThread && + ((FastThreadLocalThread) thread).willCleanupFastThreadLocals(); + } + + /** + * Query whether this thread is allowed to perform blocking calls or not. + * {@link FastThreadLocalThread}s are often used in event-loops, where blocking calls are forbidden in order to + * prevent event-loop stalls, so this method returns {@code false} by default. + *

+ * Subclasses of {@link FastThreadLocalThread} can override this method if they are not meant to be used for + * running event-loops. + * + * @return {@code false}, unless overriden by a subclass. + */ + public boolean permitBlockingCalls() { + return false; + } +} diff --git a/src/main/java/org/xbib/event/thread/InternalThreadLocalMap.java b/src/main/java/org/xbib/event/thread/InternalThreadLocalMap.java new file mode 100644 index 0000000..08350dc --- /dev/null +++ b/src/main/java/org/xbib/event/thread/InternalThreadLocalMap.java @@ -0,0 +1,365 @@ +package org.xbib.event.thread; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * The internal data structure that stores the thread-local variables for all {@link FastThreadLocal}s. + * Note that this class is for internal use only and is subject to change at any time. + * Use {@link FastThreadLocal} unless you know what you are doing. + */ +public final class InternalThreadLocalMap { + private static final ThreadLocal slowThreadLocalMap = + new ThreadLocal<>(); + private static final AtomicInteger nextIndex = new AtomicInteger(); + // Internal use only. + public static final int VARIABLES_TO_REMOVE_INDEX = nextVariableIndex(); + + private static final int DEFAULT_ARRAY_LIST_INITIAL_CAPACITY = 8; + private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30; + // Reference: https://hg.openjdk.java.net/jdk8/jdk8/jdk/file/tip/src/share/classes/java/util/ArrayList.java#l229 + private static final int ARRAY_LIST_CAPACITY_MAX_SIZE = Integer.MAX_VALUE - 8; + + private static final int HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY = 4; + private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32; + + private static final int STRING_BUILDER_INITIAL_SIZE; + private static final int STRING_BUILDER_MAX_SIZE; + + private static final Logger logger; + /** Internal use only. */ + public static final Object UNSET = new Object(); + + /** Used by {@link FastThreadLocal} */ + private Object[] indexedVariables; + + // Core thread-locals + private int futureListenerStackDepth; + private int localChannelReaderStackDepth; + private Map, Boolean> handlerSharableCache; + private int counterHashCode; + private ThreadLocalRandom random; + private Map, TypeParameterMatcher> typeParameterMatcherGetCache; + private Map, Map> typeParameterMatcherFindCache; + + // String-related thread-locals + private StringBuilder stringBuilder; + private Map charsetEncoderCache; + private Map charsetDecoderCache; + + // ArrayList-related thread-locals + private ArrayList arrayList; + + private BitSet cleanerFlags; + + /** @deprecated These padding fields will be removed in the future. */ + public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8; + + static { + STRING_BUILDER_INITIAL_SIZE = + Integer.getInteger("org.xbib.threadLocalMap.stringBuilder.initialSize", 1024); + STRING_BUILDER_MAX_SIZE = + Integer.getInteger("org.xbib.threadLocalMap.stringBuilder.maxSize", 1024 * 4); + + // Ensure the InternalLogger is initialized as last field in this class as InternalThreadLocalMap might be used + // by the InternalLogger itself. For this its important that all the other static fields are correctly + // initialized. + // + // See https://github.com/netty/netty/issues/12931. + logger = Logger.getLogger(InternalThreadLocalMap.class.getName()); + logger.log(Level.FINE, "-Dorg.xbib.threadLocalMap.stringBuilder.initialSize: {}", STRING_BUILDER_INITIAL_SIZE); + logger.log(Level.FINE, "-Dorg.xbib.threadLocalMap.stringBuilder.maxSize: {}", STRING_BUILDER_MAX_SIZE); + } + + public static InternalThreadLocalMap getIfSet() { + Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + return ((FastThreadLocalThread) thread).threadLocalMap(); + } + return slowThreadLocalMap.get(); + } + + public static InternalThreadLocalMap get() { + Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + return fastGet((FastThreadLocalThread) thread); + } else { + return slowGet(); + } + } + + private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) { + InternalThreadLocalMap threadLocalMap = thread.threadLocalMap(); + if (threadLocalMap == null) { + thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap()); + } + return threadLocalMap; + } + + private static InternalThreadLocalMap slowGet() { + InternalThreadLocalMap ret = slowThreadLocalMap.get(); + if (ret == null) { + ret = new InternalThreadLocalMap(); + slowThreadLocalMap.set(ret); + } + return ret; + } + + public static void remove() { + Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + ((FastThreadLocalThread) thread).setThreadLocalMap(null); + } else { + slowThreadLocalMap.remove(); + } + } + + public static void destroy() { + slowThreadLocalMap.remove(); + } + + public static int nextVariableIndex() { + int index = nextIndex.getAndIncrement(); + if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) { + nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE); + throw new IllegalStateException("too many thread-local indexed variables"); + } + return index; + } + + public static int lastVariableIndex() { + return nextIndex.get() - 1; + } + + private InternalThreadLocalMap() { + indexedVariables = newIndexedVariableTable(); + } + + private static Object[] newIndexedVariableTable() { + Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE]; + Arrays.fill(array, UNSET); + return array; + } + + public int size() { + int count = 0; + + if (futureListenerStackDepth != 0) { + count ++; + } + if (localChannelReaderStackDepth != 0) { + count ++; + } + if (handlerSharableCache != null) { + count ++; + } + if (counterHashCode != 0) { + count ++; + } + if (random != null) { + count ++; + } + if (typeParameterMatcherGetCache != null) { + count ++; + } + if (typeParameterMatcherFindCache != null) { + count ++; + } + if (stringBuilder != null) { + count ++; + } + if (charsetEncoderCache != null) { + count ++; + } + if (charsetDecoderCache != null) { + count ++; + } + if (arrayList != null) { + count ++; + } + + Object v = indexedVariable(VARIABLES_TO_REMOVE_INDEX); + if (v != null && v != InternalThreadLocalMap.UNSET) { + @SuppressWarnings("unchecked") + Set> variablesToRemove = (Set>) v; + count += variablesToRemove.size(); + } + + return count; + } + + public StringBuilder stringBuilder() { + StringBuilder sb = stringBuilder; + if (sb == null) { + return stringBuilder = new StringBuilder(STRING_BUILDER_INITIAL_SIZE); + } + if (sb.capacity() > STRING_BUILDER_MAX_SIZE) { + sb.setLength(STRING_BUILDER_INITIAL_SIZE); + sb.trimToSize(); + } + sb.setLength(0); + return sb; + } + + public Map charsetEncoderCache() { + Map cache = charsetEncoderCache; + if (cache == null) { + charsetEncoderCache = cache = new IdentityHashMap(); + } + return cache; + } + + public Map charsetDecoderCache() { + Map cache = charsetDecoderCache; + if (cache == null) { + charsetDecoderCache = cache = new IdentityHashMap(); + } + return cache; + } + + public ArrayList arrayList() { + return arrayList(DEFAULT_ARRAY_LIST_INITIAL_CAPACITY); + } + + @SuppressWarnings("unchecked") + public ArrayList arrayList(int minCapacity) { + ArrayList list = (ArrayList) arrayList; + if (list == null) { + arrayList = new ArrayList(minCapacity); + return (ArrayList) arrayList; + } + list.clear(); + list.ensureCapacity(minCapacity); + return list; + } + + public int futureListenerStackDepth() { + return futureListenerStackDepth; + } + + public void setFutureListenerStackDepth(int futureListenerStackDepth) { + this.futureListenerStackDepth = futureListenerStackDepth; + } + + public ThreadLocalRandom random() { + ThreadLocalRandom r = random; + if (r == null) { + random = r = ThreadLocalRandom.current(); + } + return r; + } + + public Map, TypeParameterMatcher> typeParameterMatcherGetCache() { + Map, TypeParameterMatcher> cache = typeParameterMatcherGetCache; + if (cache == null) { + typeParameterMatcherGetCache = cache = new IdentityHashMap, TypeParameterMatcher>(); + } + return cache; + } + + public Map, Map> typeParameterMatcherFindCache() { + Map, Map> cache = typeParameterMatcherFindCache; + if (cache == null) { + typeParameterMatcherFindCache = cache = new IdentityHashMap, Map>(); + } + return cache; + } + + public Map, Boolean> handlerSharableCache() { + Map, Boolean> cache = handlerSharableCache; + if (cache == null) { + // Start with small capacity to keep memory overhead as low as possible. + handlerSharableCache = cache = new WeakHashMap, Boolean>(HANDLER_SHARABLE_CACHE_INITIAL_CAPACITY); + } + return cache; + } + + public int localChannelReaderStackDepth() { + return localChannelReaderStackDepth; + } + + public void setLocalChannelReaderStackDepth(int localChannelReaderStackDepth) { + this.localChannelReaderStackDepth = localChannelReaderStackDepth; + } + + public Object indexedVariable(int index) { + Object[] lookup = indexedVariables; + return index < lookup.length? lookup[index] : UNSET; + } + + /** + * @return {@code true} if and only if a new thread-local variable has been created + */ + public boolean setIndexedVariable(int index, Object value) { + Object[] lookup = indexedVariables; + if (index < lookup.length) { + Object oldValue = lookup[index]; + lookup[index] = value; + return oldValue == UNSET; + } else { + expandIndexedVariableTableAndSet(index, value); + return true; + } + } + + private void expandIndexedVariableTableAndSet(int index, Object value) { + Object[] oldArray = indexedVariables; + final int oldCapacity = oldArray.length; + int newCapacity; + if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) { + newCapacity = index; + newCapacity |= newCapacity >>> 1; + newCapacity |= newCapacity >>> 2; + newCapacity |= newCapacity >>> 4; + newCapacity |= newCapacity >>> 8; + newCapacity |= newCapacity >>> 16; + newCapacity ++; + } else { + newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE; + } + + Object[] newArray = Arrays.copyOf(oldArray, newCapacity); + Arrays.fill(newArray, oldCapacity, newArray.length, UNSET); + newArray[index] = value; + indexedVariables = newArray; + } + + public Object removeIndexedVariable(int index) { + Object[] lookup = indexedVariables; + if (index < lookup.length) { + Object v = lookup[index]; + lookup[index] = UNSET; + return v; + } else { + return UNSET; + } + } + + public boolean isIndexedVariableSet(int index) { + Object[] lookup = indexedVariables; + return index < lookup.length && lookup[index] != UNSET; + } + + public boolean isCleanerFlagSet(int index) { + return cleanerFlags != null && cleanerFlags.get(index); + } + + public void setCleanerFlag(int index) { + if (cleanerFlags == null) { + cleanerFlags = new BitSet(); + } + cleanerFlags.set(index); + } +} diff --git a/src/main/java/org/xbib/event/thread/ThreadExecutorMap.java b/src/main/java/org/xbib/event/thread/ThreadExecutorMap.java new file mode 100644 index 0000000..0addaf8 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/ThreadExecutorMap.java @@ -0,0 +1,81 @@ +package org.xbib.event.thread; + +import org.xbib.event.loop.EventExecutor; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +/** + * Allow to retrieve the {@link EventExecutor} for the calling {@link Thread}. + */ +public final class ThreadExecutorMap { + + private static final FastThreadLocal mappings = new FastThreadLocal<>(); + + private ThreadExecutorMap() { } + + /** + * Returns the current {@link EventExecutor} that uses the {@link Thread}, or {@code null} if none / unknown. + */ + public static EventExecutor currentExecutor() { + return mappings.get(); + } + + /** + * Set the current {@link EventExecutor} that is used by the {@link Thread}. + */ + private static void setCurrentEventExecutor(EventExecutor executor) { + mappings.set(executor); + } + + /** + * Decorate the given {@link Executor} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static Executor apply(final Executor executor, final EventExecutor eventExecutor) { + Objects.requireNonNull(executor, "executor"); + Objects.requireNonNull(eventExecutor, "eventExecutor"); + return new Executor() { + @Override + public void execute(final Runnable command) { + executor.execute(apply(command, eventExecutor)); + } + }; + } + + /** + * Decorate the given {@link Runnable} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static Runnable apply(final Runnable command, final EventExecutor eventExecutor) { + Objects.requireNonNull(command, "command"); + Objects.requireNonNull(eventExecutor, "eventExecutor"); + return new Runnable() { + @Override + public void run() { + setCurrentEventExecutor(eventExecutor); + try { + command.run(); + } finally { + setCurrentEventExecutor(null); + } + } + }; + } + + /** + * Decorate the given {@link ThreadFactory} and ensure {@link #currentExecutor()} will return {@code eventExecutor} + * when called from within the {@link Runnable} during execution. + */ + public static ThreadFactory apply(final ThreadFactory threadFactory, final EventExecutor eventExecutor) { + Objects.requireNonNull(threadFactory, "command"); + Objects.requireNonNull(eventExecutor, "eventExecutor"); + return new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return threadFactory.newThread(apply(r, eventExecutor)); + } + }; + } +} diff --git a/src/main/java/org/xbib/event/thread/ThreadInfo.java b/src/main/java/org/xbib/event/thread/ThreadInfo.java new file mode 100644 index 0000000..395b1a8 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/ThreadInfo.java @@ -0,0 +1,15 @@ +package org.xbib.event.thread; + +import java.util.concurrent.TimeUnit; + +public final class ThreadInfo { + + public long startTime; + public final TimeUnit maxExecTimeUnit; + public final long maxExecTime; + + public ThreadInfo(TimeUnit maxExecTimeUnit, long maxExecTime) { + this.maxExecTimeUnit = maxExecTimeUnit; + this.maxExecTime = maxExecTime; + } +} diff --git a/src/main/java/org/xbib/event/thread/ThreadPerTaskExecutor.java b/src/main/java/org/xbib/event/thread/ThreadPerTaskExecutor.java new file mode 100644 index 0000000..0fcae83 --- /dev/null +++ b/src/main/java/org/xbib/event/thread/ThreadPerTaskExecutor.java @@ -0,0 +1,18 @@ +package org.xbib.event.thread; + +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadFactory; + +public final class ThreadPerTaskExecutor implements Executor { + private final ThreadFactory threadFactory; + + public ThreadPerTaskExecutor(ThreadFactory threadFactory) { + this.threadFactory = Objects.requireNonNull(threadFactory, "threadFactory"); + } + + @Override + public void execute(Runnable command) { + threadFactory.newThread(command).start(); + } +} diff --git a/src/main/java/org/xbib/event/thread/ThreadProperties.java b/src/main/java/org/xbib/event/thread/ThreadProperties.java new file mode 100644 index 0000000..0fae7cd --- /dev/null +++ b/src/main/java/org/xbib/event/thread/ThreadProperties.java @@ -0,0 +1,46 @@ +package org.xbib.event.thread; + +/** + * Expose details for a {@link Thread}. + */ +public interface ThreadProperties { + /** + * @see Thread#getState() + */ + Thread.State state(); + + /** + * @see Thread#getPriority() + */ + int priority(); + + /** + * @see Thread#isInterrupted() + */ + boolean isInterrupted(); + + /** + * @see Thread#isDaemon() + */ + boolean isDaemon(); + + /** + * @see Thread#getName() + */ + String name(); + + /** + * @see Thread#getId() + */ + long id(); + + /** + * @see Thread#getStackTrace() + */ + StackTraceElement[] stackTrace(); + + /** + * @see Thread#isAlive() + */ + boolean isAlive(); +} diff --git a/src/main/java/org/xbib/event/thread/TypeParameterMatcher.java b/src/main/java/org/xbib/event/thread/TypeParameterMatcher.java new file mode 100644 index 0000000..daaf98a --- /dev/null +++ b/src/main/java/org/xbib/event/thread/TypeParameterMatcher.java @@ -0,0 +1,143 @@ +package org.xbib.event.thread; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashMap; +import java.util.Map; + +public abstract class TypeParameterMatcher { + + private static final TypeParameterMatcher NOOP = new TypeParameterMatcher() { + @Override + public boolean match(Object msg) { + return true; + } + }; + + public static TypeParameterMatcher get(final Class parameterType) { + final Map, TypeParameterMatcher> getCache = + InternalThreadLocalMap.get().typeParameterMatcherGetCache(); + + TypeParameterMatcher matcher = getCache.get(parameterType); + if (matcher == null) { + if (parameterType == Object.class) { + matcher = NOOP; + } else { + matcher = new ReflectiveMatcher(parameterType); + } + getCache.put(parameterType, matcher); + } + + return matcher; + } + + public static TypeParameterMatcher find(final Object object, final Class parametrizedSuperclass, final String typeParamName) { + + final Map, Map> findCache = + InternalThreadLocalMap.get().typeParameterMatcherFindCache(); + final Class thisClass = object.getClass(); + + Map map = findCache.computeIfAbsent(thisClass, k -> new HashMap<>()); + + TypeParameterMatcher matcher = map.get(typeParamName); + if (matcher == null) { + matcher = get(find0(object, parametrizedSuperclass, typeParamName)); + map.put(typeParamName, matcher); + } + + return matcher; + } + + private static Class find0(final Object object, Class parametrizedSuperclass, String typeParamName) { + + final Class thisClass = object.getClass(); + Class currentClass = thisClass; + for (;;) { + if (currentClass.getSuperclass() == parametrizedSuperclass) { + int typeParamIndex = -1; + TypeVariable[] typeParams = currentClass.getSuperclass().getTypeParameters(); + for (int i = 0; i < typeParams.length; i ++) { + if (typeParamName.equals(typeParams[i].getName())) { + typeParamIndex = i; + break; + } + } + + if (typeParamIndex < 0) { + throw new IllegalStateException( + "unknown type parameter '" + typeParamName + "': " + parametrizedSuperclass); + } + + Type genericSuperType = currentClass.getGenericSuperclass(); + if (!(genericSuperType instanceof ParameterizedType)) { + return Object.class; + } + + Type[] actualTypeParams = ((ParameterizedType) genericSuperType).getActualTypeArguments(); + + Type actualTypeParam = actualTypeParams[typeParamIndex]; + if (actualTypeParam instanceof ParameterizedType) { + actualTypeParam = ((ParameterizedType) actualTypeParam).getRawType(); + } + if (actualTypeParam instanceof Class) { + return (Class) actualTypeParam; + } + if (actualTypeParam instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) actualTypeParam).getGenericComponentType(); + if (componentType instanceof ParameterizedType) { + componentType = ((ParameterizedType) componentType).getRawType(); + } + if (componentType instanceof Class) { + return Array.newInstance((Class) componentType, 0).getClass(); + } + } + if (actualTypeParam instanceof TypeVariable) { + // Resolved type parameter points to another type parameter. + TypeVariable v = (TypeVariable) actualTypeParam; + if (!(v.getGenericDeclaration() instanceof Class)) { + return Object.class; + } + + currentClass = thisClass; + parametrizedSuperclass = (Class) v.getGenericDeclaration(); + typeParamName = v.getName(); + if (parametrizedSuperclass.isAssignableFrom(thisClass)) { + continue; + } + return Object.class; + } + + return fail(thisClass, typeParamName); + } + currentClass = currentClass.getSuperclass(); + if (currentClass == null) { + return fail(thisClass, typeParamName); + } + } + } + + private static Class fail(Class type, String typeParamName) { + throw new IllegalStateException( + "cannot determine the type of the type parameter '" + typeParamName + "': " + type); + } + + public abstract boolean match(Object msg); + + private static final class ReflectiveMatcher extends TypeParameterMatcher { + private final Class type; + + ReflectiveMatcher(Class type) { + this.type = type; + } + + @Override + public boolean match(Object msg) { + return type.isInstance(msg); + } + } + + TypeParameterMatcher() { } +} diff --git a/src/main/java/org/xbib/event/util/DefaultPriorityQueue.java b/src/main/java/org/xbib/event/util/DefaultPriorityQueue.java new file mode 100644 index 0000000..5a8a270 --- /dev/null +++ b/src/main/java/org/xbib/event/util/DefaultPriorityQueue.java @@ -0,0 +1,282 @@ +package org.xbib.event.util; + +import java.util.AbstractQueue; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +import static org.xbib.event.util.PriorityQueueNode.INDEX_NOT_IN_QUEUE; + +/** + * A priority queue which uses natural ordering of elements. Elements are also required to be of type + * {@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 { + private static final PriorityQueueNode[] EMPTY_ARRAY = new PriorityQueueNode[0]; + private final Comparator comparator; + private T[] queue; + private int size; + + @SuppressWarnings("unchecked") + public DefaultPriorityQueue(Comparator comparator, int initialSize) { + this.comparator = Objects.requireNonNull(comparator, "comparator"); + queue = (T[]) (initialSize != 0 ? new PriorityQueueNode[initialSize] : EMPTY_ARRAY); + } + + @Override + public int size() { + return size; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof PriorityQueueNode)) { + return false; + } + PriorityQueueNode node = (PriorityQueueNode) o; + return contains(node, node.priorityQueueIndex(this)); + } + + @Override + public boolean containsTyped(T node) { + return contains(node, node.priorityQueueIndex(this)); + } + + @Override + public void clear() { + for (int i = 0; i < size; ++i) { + T node = queue[i]; + if (node != null) { + node.priorityQueueIndex(this, INDEX_NOT_IN_QUEUE); + queue[i] = null; + } + } + size = 0; + } + + @Override + public void clearIgnoringIndexes() { + size = 0; + } + + @Override + public boolean offer(T e) { + if (e.priorityQueueIndex(this) != INDEX_NOT_IN_QUEUE) { + throw new IllegalArgumentException("e.priorityQueueIndex(): " + e.priorityQueueIndex(this) + + " (expected: " + INDEX_NOT_IN_QUEUE + ") + e: " + e); + } + + // Check that the array capacity is enough to hold values by doubling capacity. + if (size >= queue.length) { + // Use a policy which allows for a 0 initial capacity. Same policy as JDK's priority queue, double when + // "small", then grow by 50% when "large". + queue = Arrays.copyOf(queue, queue.length + ((queue.length < 64) ? + (queue.length + 2) : + (queue.length >>> 1))); + } + + bubbleUp(size++, e); + return true; + } + + @Override + public T poll() { + if (size == 0) { + return null; + } + T result = queue[0]; + result.priorityQueueIndex(this, INDEX_NOT_IN_QUEUE); + + T last = queue[--size]; + queue[size] = null; + if (size != 0) { // Make sure we don't add the last element back. + bubbleDown(0, last); + } + + return result; + } + + @Override + public T peek() { + return (size == 0) ? null : queue[0]; + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(Object o) { + final T node; + try { + node = (T) o; + } catch (ClassCastException e) { + return false; + } + return removeTyped(node); + } + + @Override + public boolean removeTyped(T node) { + int i = node.priorityQueueIndex(this); + if (!contains(node, i)) { + return false; + } + + node.priorityQueueIndex(this, INDEX_NOT_IN_QUEUE); + if (--size == 0 || size == i) { + // If there are no node left, or this is the last node in the array just remove and return. + queue[i] = null; + return true; + } + + // Move the last element where node currently lives in the array. + T moved = queue[i] = queue[size]; + queue[size] = null; + // priorityQueueIndex will be updated below in bubbleUp or bubbleDown + + // Make sure the moved node still preserves the min-heap properties. + if (comparator.compare(node, moved) < 0) { + bubbleDown(i, moved); + } else { + bubbleUp(i, moved); + } + return true; + } + + @Override + public void priorityChanged(T node) { + int i = node.priorityQueueIndex(this); + if (!contains(node, i)) { + return; + } + + // Preserve the min-heap property by comparing the new priority with parents/children in the heap. + if (i == 0) { + bubbleDown(i, node); + } else { + // Get the parent to see if min-heap properties are violated. + int iParent = (i - 1) >>> 1; + T parent = queue[iParent]; + if (comparator.compare(node, parent) < 0) { + bubbleUp(i, node); + } else { + bubbleDown(i, node); + } + } + } + + @Override + public Object[] toArray() { + return Arrays.copyOf(queue, size); + } + + @SuppressWarnings("unchecked") + @Override + public X[] toArray(X[] a) { + if (a.length < size) { + return (X[]) Arrays.copyOf(queue, size, a.getClass()); + } + System.arraycopy(queue, 0, a, 0, size); + if (a.length > size) { + a[size] = null; + } + return a; + } + + /** + * This iterator does not return elements in any particular order. + */ + @Override + public Iterator iterator() { + return new PriorityQueueIterator(); + } + + private final class PriorityQueueIterator implements Iterator { + private int index; + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public T next() { + if (index >= size) { + throw new NoSuchElementException(); + } + + return queue[index++]; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + } + + private boolean contains(PriorityQueueNode node, int i) { + return i >= 0 && i < size && node.equals(queue[i]); + } + + private void bubbleDown(int k, T node) { + final int half = size >>> 1; + while (k < half) { + // Compare node to the children of index k. + int iChild = (k << 1) + 1; + T child = queue[iChild]; + + // Make sure we get the smallest child to compare against. + int rightChild = iChild + 1; + if (rightChild < size && comparator.compare(child, queue[rightChild]) > 0) { + child = queue[iChild = rightChild]; + } + // If the bubbleDown node is less than or equal to the smallest child then we will preserve the min-heap + // property by inserting the bubbleDown node here. + if (comparator.compare(node, child) <= 0) { + break; + } + + // Bubble the child up. + queue[k] = child; + child.priorityQueueIndex(this, k); + + // Move down k down the tree for the next iteration. + k = iChild; + } + + // We have found where node should live and still satisfy the min-heap property, so put it in the queue. + queue[k] = node; + node.priorityQueueIndex(this, k); + } + + private void bubbleUp(int k, T node) { + while (k > 0) { + int iParent = (k - 1) >>> 1; + T parent = queue[iParent]; + + // If the bubbleUp node is less than the parent, then we have found a spot to insert and still maintain + // min-heap properties. + if (comparator.compare(node, parent) >= 0) { + break; + } + + // Bubble the parent down. + queue[k] = parent; + parent.priorityQueueIndex(this, k); + + // Move k up the tree for the next iteration. + k = iParent; + } + + // We have found where node should live and still satisfy the min-heap property, so put it in the queue. + queue[k] = node; + node.priorityQueueIndex(this, k); + } +} diff --git a/src/main/java/org/xbib/event/util/IntSupplier.java b/src/main/java/org/xbib/event/util/IntSupplier.java new file mode 100644 index 0000000..e237769 --- /dev/null +++ b/src/main/java/org/xbib/event/util/IntSupplier.java @@ -0,0 +1,14 @@ +package org.xbib.event.util; + +/** + * Represents a supplier of {@code int}-valued results. + */ +public interface IntSupplier { + + /** + * Gets a result. + * + * @return a result + */ + int get() throws Exception; +} diff --git a/src/main/java/org/xbib/event/util/PriorityQueue.java b/src/main/java/org/xbib/event/util/PriorityQueue.java new file mode 100644 index 0000000..cddefcb --- /dev/null +++ b/src/main/java/org/xbib/event/util/PriorityQueue.java @@ -0,0 +1,31 @@ +package org.xbib.event.util; + +import java.util.Queue; + +public interface PriorityQueue extends Queue { + /** + * Same as {@link #remove(Object)} but typed using generics. + */ + boolean removeTyped(T node); + + /** + * Same as {@link #contains(Object)} but typed using generics. + */ + boolean containsTyped(T node); + + /** + * Notify the queue that the priority for {@code node} has changed. The queue will adjust to ensure the priority + * queue properties are maintained. + * @param node An object which is in this queue and the priority may have changed. + */ + void priorityChanged(T node); + + /** + * Removes all of the elements from this {@link PriorityQueue} without calling + * {@link PriorityQueueNode#priorityQueueIndex(DefaultPriorityQueue)} or 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 PriorityQueue} and it is known that the {@link PriorityQueue} itself + * will be garbage collected after this call. + */ + void clearIgnoringIndexes(); +} diff --git a/src/main/java/org/xbib/event/util/PriorityQueueNode.java b/src/main/java/org/xbib/event/util/PriorityQueueNode.java new file mode 100644 index 0000000..0769f7e --- /dev/null +++ b/src/main/java/org/xbib/event/util/PriorityQueueNode.java @@ -0,0 +1,29 @@ +package org.xbib.event.util; + +/** + * Provides methods for {@link DefaultPriorityQueue} to maintain internal state. These methods should generally not be + * used outside the scope of {@link DefaultPriorityQueue}. + */ +public interface PriorityQueueNode { + /** + * This should be used to initialize the storage returned by {@link #priorityQueueIndex(DefaultPriorityQueue)}. + */ + int INDEX_NOT_IN_QUEUE = -1; + + /** + * Get the last value set by {@link #priorityQueueIndex(DefaultPriorityQueue, int)} for the value corresponding to + * {@code queue}. + *

+ * Throwing exceptions from this method will result in undefined behavior. + */ + int priorityQueueIndex(DefaultPriorityQueue queue); + + /** + * Used by {@link DefaultPriorityQueue} to maintain state for an element in the queue. + *

+ * Throwing exceptions from this method will result in undefined behavior. + * @param queue The queue for which the index is being set. + * @param i The index as used by {@link DefaultPriorityQueue}. + */ + void priorityQueueIndex(DefaultPriorityQueue queue, int i); +} diff --git a/src/test/java/org/xbib/event/async/test/AsyncFileReaderTest.java b/src/test/java/org/xbib/event/io/test/AsyncFileReaderTest.java similarity index 99% rename from src/test/java/org/xbib/event/async/test/AsyncFileReaderTest.java rename to src/test/java/org/xbib/event/io/test/AsyncFileReaderTest.java index 4840f7c..c6a8035 100644 --- a/src/test/java/org/xbib/event/async/test/AsyncFileReaderTest.java +++ b/src/test/java/org/xbib/event/io/test/AsyncFileReaderTest.java @@ -1,10 +1,10 @@ -package org.xbib.event.async.test; +package org.xbib.event.io.test; import io.reactivex.rxjava3.core.Observable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; -import org.xbib.event.async.AsyncFiles; +import org.xbib.event.io.AsyncFiles; import org.xbib.event.yield.AsyncQuery; import java.io.FileWriter; @@ -37,7 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import static org.xbib.event.async.Subscribers.doOnNext; +import static org.xbib.event.io.Subscribers.doOnNext; public class AsyncFileReaderTest { diff --git a/src/test/java/org/xbib/event/async/test/AsyncFileWriterTest.java b/src/test/java/org/xbib/event/io/test/AsyncFileWriterTest.java similarity index 97% rename from src/test/java/org/xbib/event/async/test/AsyncFileWriterTest.java rename to src/test/java/org/xbib/event/io/test/AsyncFileWriterTest.java index 925b112..148ddb3 100644 --- a/src/test/java/org/xbib/event/async/test/AsyncFileWriterTest.java +++ b/src/test/java/org/xbib/event/io/test/AsyncFileWriterTest.java @@ -1,7 +1,7 @@ -package org.xbib.event.async.test; +package org.xbib.event.io.test; import org.junit.jupiter.api.Test; -import org.xbib.event.async.AsyncFiles; +import org.xbib.event.io.AsyncFiles; import java.io.IOException; import java.net.URISyntaxException; diff --git a/src/test/java/org/xbib/event/async/test/AsyncFilesFailures.java b/src/test/java/org/xbib/event/io/test/AsyncFilesFailures.java similarity index 98% rename from src/test/java/org/xbib/event/async/test/AsyncFilesFailures.java rename to src/test/java/org/xbib/event/io/test/AsyncFilesFailures.java index 912956b..d2e452b 100644 --- a/src/test/java/org/xbib/event/async/test/AsyncFilesFailures.java +++ b/src/test/java/org/xbib/event/io/test/AsyncFilesFailures.java @@ -1,9 +1,9 @@ -package org.xbib.event.async.test; +package org.xbib.event.io.test; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.xbib.event.async.AsyncFiles; -import org.xbib.event.async.Subscribers; +import org.xbib.event.io.AsyncFiles; +import org.xbib.event.io.Subscribers; import java.io.IOException; import java.net.URISyntaxException;