diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/ThreadPoolSupplier.java b/common/configurable/src/main/java/io/helidon/common/configurable/ThreadPoolSupplier.java index 858af0cb4f8..4e2421a9499 100644 --- a/common/configurable/src/main/java/io/helidon/common/configurable/ThreadPoolSupplier.java +++ b/common/configurable/src/main/java/io/helidon/common/configurable/ThreadPoolSupplier.java @@ -58,6 +58,7 @@ public final class ThreadPoolSupplier implements Supplier { private final int growthRate; private final ThreadPool.RejectionHandler rejectionHandler; private final LazyValue lazyValue = LazyValue.create(() -> Contexts.wrap(getThreadPool())); + private final boolean useVirtualThreads; private ThreadPoolSupplier(Builder builder) { this.corePoolSize = builder.corePoolSize; @@ -71,6 +72,7 @@ private ThreadPoolSupplier(Builder builder) { this.growthThreshold = builder.growthThreshold; this.growthRate = builder.growthRate; this.rejectionHandler = builder.rejectionHandler == null ? DEFAULT_REJECTION_POLICY : builder.rejectionHandler; + this.useVirtualThreads = builder.useVirtualThreads || builder.virtualThreadsEnforced; } /** @@ -102,7 +104,14 @@ public static ThreadPoolSupplier create() { return builder().build(); } - ThreadPool getThreadPool() { + ExecutorService getThreadPool() { + if (useVirtualThreads) { + if (VirtualExecutorUtil.isVirtualSupported()) { + LOGGER.fine("Using unbounded virtual executor service for pool " + name); + return VirtualExecutorUtil.executorService(); + } + } + ThreadPool result = ThreadPool.create(name, corePoolSize, maxPoolSize, @@ -149,6 +158,8 @@ public static final class Builder implements io.helidon.common.Builder { warnExperimental("growth-rate"); growthRate(value); - + }); + config.get("virtual-threads").asBoolean().ifPresent(value -> { + warnExperimental("virtual-threads"); + virtualIfAvailable(value); + }); + config.get("virtual-enforced").asBoolean().ifPresent(value -> { + warnExperimental("virtual-enforced"); + virtualEnforced(value); }); return this; } @@ -398,5 +423,33 @@ public Builder config(Config config) { private void warnExperimental(String key) { LOGGER.warning(String.format("Config key \"executor-service.%s\" is EXPERIMENTAL and subject to change.", key)); } + + /** + * When configured to {@code true}, virtual thread executor service must be available, otherwise the built + * executor would fail to start. + * + * @param enforceVirtualThreads whether to enforce virtual threads, defaults to {@code false} + * @return updated builder instance + * @see #virtualIfAvailable(boolean) + */ + public Builder virtualEnforced(boolean enforceVirtualThreads) { + this.virtualThreadsEnforced = enforceVirtualThreads; + return this; + } + + /** + * When configured to {@code true}, an unbounded virtual executor service (project Loom) will be used + * if available. + * This is an experimental feature. + *

+ * If enabled and available, all other configuration options of this executor service are ignored! + * + * @param useVirtualThreads whether to use virtual threads or not, defaults to {@code false} + * @return updated builder instance + */ + public Builder virtualIfAvailable(boolean useVirtualThreads) { + this.useVirtualThreads = useVirtualThreads; + return this; + } } } diff --git a/common/configurable/src/main/java/io/helidon/common/configurable/VirtualExecutorUtil.java b/common/configurable/src/main/java/io/helidon/common/configurable/VirtualExecutorUtil.java new file mode 100644 index 00000000000..7becf3c008a --- /dev/null +++ b/common/configurable/src/main/java/io/helidon/common/configurable/VirtualExecutorUtil.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2021 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.configurable; + +import java.lang.reflect.Method; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.common.LazyValue; + +/** + * A utility class to handle virtual threads (project Loom). + */ +final class VirtualExecutorUtil { + private static final Logger LOGGER = Logger.getLogger(VirtualExecutorUtil.class.getName()); + private static final LazyValue SUPPORTED = LazyValue.create(VirtualExecutorUtil::findSupported); + private static final LazyValue EXECUTOR_SERVICE = LazyValue.create(VirtualExecutorUtil::findExecutor); + + private VirtualExecutorUtil() { + } + + static boolean isVirtualSupported() { + return SUPPORTED.get(); + } + + static ExecutorService executorService() { + ExecutorService result = EXECUTOR_SERVICE.get(); + if (result == null) { + throw new IllegalStateException("Virtual executor service is not supported on this JVM"); + } + return result; + } + + private static boolean findSupported() { + try { + // the method is intentionally NOT CACHED in static context, to support differences between build + // and runtime environments (support for GraalVM native image) + findMethod(); + return true; + } catch (final ReflectiveOperationException e) { + LOGGER.log(Level.FINEST, "Loom virtual executor service not available", e); + } + + return false; + } + + private static ExecutorService findExecutor() { + try { + // the method is intentionally NOT CACHED in static context, to support differences between build + // and runtime environments (support for GraalVM native image) + return (ExecutorService) findMethod().invoke(null); + } catch (final ReflectiveOperationException e) { + LOGGER.log(Level.FINEST, "Loom virtual executor service not available", e); + } + + return null; + } + + private static Method findMethod() throws ReflectiveOperationException { + return Executors.class.getDeclaredMethod("newVirtualThreadExecutor"); + } +} diff --git a/common/configurable/src/test/java/io/helidon/common/configurable/ThreadPoolSupplierTest.java b/common/configurable/src/test/java/io/helidon/common/configurable/ThreadPoolSupplierTest.java index ab092553e5d..94cd609dafd 100644 --- a/common/configurable/src/test/java/io/helidon/common/configurable/ThreadPoolSupplierTest.java +++ b/common/configurable/src/test/java/io/helidon/common/configurable/ThreadPoolSupplierTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.startsWith; @@ -50,19 +51,25 @@ class ThreadPoolSupplierTest { @BeforeAll static void initClass() { - defaultInstance = ThreadPoolSupplier.create().getThreadPool(); + defaultInstance = ensureOurExecutor(ThreadPoolSupplier.create().getThreadPool()); - builtInstance = ThreadPoolSupplier.builder() + builtInstance = ensureOurExecutor(ThreadPoolSupplier.builder() .threadNamePrefix("thread-pool-unit-test-") .corePoolSize(2) .daemon(true) .prestart(true) .queueCapacity(10) .build() - .getThreadPool(); + .getThreadPool()); - configuredInstance = ThreadPoolSupplier.create(Config.create().get("unit.thread-pool")) - .getThreadPool(); + configuredInstance = ensureOurExecutor(ThreadPoolSupplier.create(Config.create().get("unit.thread-pool")) + .getThreadPool()); + } + + private static ThreadPoolExecutor ensureOurExecutor(ExecutorService threadPool) { + // thread pool should be our implementation, unless Loom virtual threads are used + assertThat(threadPool, instanceOf(ThreadPoolExecutor.class)); + return (ThreadPoolExecutor) threadPool; } @Test diff --git a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java index 3c304188060..23a4eb490a7 100644 --- a/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java +++ b/microprofile/server/src/main/java/io/helidon/microprofile/server/ServerCdiExtension.java @@ -31,7 +31,6 @@ import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -56,7 +55,6 @@ import io.helidon.common.configurable.ServerThreadPoolSupplier; import io.helidon.common.http.Http; import io.helidon.config.Config; -import io.helidon.config.DeprecatedConfig; import io.helidon.microprofile.cdi.BuildTimeStart; import io.helidon.microprofile.cdi.RuntimeStart; import io.helidon.webserver.KeyPerformanceIndicatorSupport; @@ -170,34 +168,12 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( // make sure all configuration is in place if (null == jaxRsExecutorService) { Config serverConfig = config.get("server"); - final java.lang.reflect.Method m; - if (DeprecatedConfig.get(serverConfig, "executor-service.virtual-threads", "virtual-threads") - .asBoolean().orElse(false)) { - java.lang.reflect.Method temp = null; - try { - temp = Executors.class.getDeclaredMethod("newVirtualThreadExecutor"); - } catch (final ReflectiveOperationException notLoomEarlyAccess) { - temp = null; - } finally { - m = temp; - } - } else { - m = null; - } - if (m != null) { - jaxRsExecutorService = () -> { - try { - return (ExecutorService) m.invoke(null); - } catch (final ReflectiveOperationException reflectiveOperationException) { - throw new IllegalStateException(reflectiveOperationException.getMessage(), reflectiveOperationException); - } - }; - } else { - jaxRsExecutorService = ServerThreadPoolSupplier.builder() - .name("server") - .config(serverConfig.get("executor-service")) - .build(); - } + + // support for Loom is built into the thread pool supplier + jaxRsExecutorService = ServerThreadPoolSupplier.builder() + .name("server") + .config(serverConfig.get("executor-service")) + .build(); } // redirect to the first page when root is accessed (if configured) diff --git a/webserver/jersey/src/main/java/io/helidon/webserver/jersey/AsyncExecutorProvider.java b/webserver/jersey/src/main/java/io/helidon/webserver/jersey/AsyncExecutorProvider.java index a1236cd9670..79c085c0028 100644 --- a/webserver/jersey/src/main/java/io/helidon/webserver/jersey/AsyncExecutorProvider.java +++ b/webserver/jersey/src/main/java/io/helidon/webserver/jersey/AsyncExecutorProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Oracle and/or its affiliates. + * Copyright (c) 2020, 2021 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.Objects; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.function.Supplier; import io.helidon.common.configurable.ThreadPoolSupplier; @@ -37,36 +36,15 @@ class AsyncExecutorProvider implements ExecutorServiceProvider { static ExecutorServiceProvider create(Config config) { Config asyncExecutorServiceConfig = config.get("async-executor-service"); - final java.lang.reflect.Method m; - if (asyncExecutorServiceConfig.get("virtual-threads").asBoolean().orElse(false)) { - java.lang.reflect.Method temp = null; - try { - temp = Executors.class.getDeclaredMethod("newVirtualThreadExecutor"); - } catch (final ReflectiveOperationException notLoom) { - temp = null; - } finally { - m = temp; - } - } else { - m = null; - } - if (m != null) { - return new AsyncExecutorProvider(() -> { - try { - return (ExecutorService) m.invoke(null); - } catch (final ReflectiveOperationException reflectiveOperationException) { - throw new IllegalStateException(reflectiveOperationException.getMessage(), reflectiveOperationException); - } - }); - } else { - return new AsyncExecutorProvider(ThreadPoolSupplier.builder() + + // Loom support is moved to thread pool supplier + return new AsyncExecutorProvider(ThreadPoolSupplier.builder() .corePoolSize(1) .maxPoolSize(10) .prestart(false) .threadNamePrefix("helidon-jersey-async") .config(asyncExecutorServiceConfig) .build()); - } } static ExecutorServiceProvider create(ExecutorService executor) {