diff --git a/dependencies/pom.xml b/dependencies/pom.xml index efe38f1ef9d..f5cec37797e 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -59,6 +59,7 @@ 2.9.0 1.60.0 32.0.1-jre + 1.4.0 2.2.220 1.3 4.3.1 @@ -812,6 +813,11 @@ jakarta.validation-api ${version.lib.jakarta.validation-api} + + org.crac + crac + ${version.lib.crac} + com.h2database h2 diff --git a/examples/crac/Dockerfile.crac b/examples/crac/Dockerfile.crac new file mode 100644 index 00000000000..54791dbf4b3 --- /dev/null +++ b/examples/crac/Dockerfile.crac @@ -0,0 +1,48 @@ +# +# Copyright (c) 2022, 2024 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. +# +FROM container-registry.oracle.com/os/oraclelinux:9-slim as olinux-crac + +WORKDIR /usr/share + +ARG CHECKPOINT_DIR + +ENV JDK_NAME=zulu22.30.13-ca-crac-jdk22.0.1-linux_x64 +ENV JAVA_HOME=/usr/share/$JDK_NAME +ENV CR_DIR=${CONT_IMG_VER:-/crac-checkpoint/cr} + +# Install wrk +RUN microdnf -y update && microdnf -y install perl wget tar gzip curl git openssl-devel +RUN git clone https://github.com/wg/wrk.git && cd wrk && make && cp wrk /usr/local/bin/ + +# Install CRaC +RUN wget -O crac-jdk.tar.gz "https://cdn.azul.com/zulu/bin/${JDK_NAME}.tar.gz" +RUN tar zxf ./crac-jdk.tar.gz -C /usr/share && ln -s $JAVA_HOME/bin/java /bin/ +RUN ln -s $JAVA_HOME/bin/jcmd /bin/ && ln -s $JAVA_HOME/bin/jps /bin/ + +FROM olinux-crac +WORKDIR /helidon + +ADD target/*.jar . +ADD target/libs libs +ADD runtimeCRaC.sh . +ADD warmUp.sh . +ADD measure.sh . +RUN chmod +x ./*.sh + +CMD ["sh","./runtimeCRaC.sh"] + +EXPOSE 7001 + diff --git a/examples/crac/README.md b/examples/crac/README.md new file mode 100644 index 00000000000..edcd05c1e67 --- /dev/null +++ b/examples/crac/README.md @@ -0,0 +1,41 @@ +# Helidon MP on CRaC +[Coordinated Restore at Checkpoint](https://wiki.openjdk.org/display/crac) + + +## Runtime CRaC +Standard docker build doesn't support privileged access to the host machine kernel, +therefore CRaC checkpoint needs to be created in runtime. + +```bash +mvn clean package +docker build -t crac-helloworld . -f Dockerfile.crac +# First time ran, checkpoint is created, stop with Ctrl-C +docker run --privileged --network host --name crac-helloworld crac-helloworld +# Second time starting from checkpoint, stop with Ctrl-C +docker start -i crac-helloworld +``` + +### Exercise the app +``` +curl -X GET http://localhost:7001/helloworld +curl -X GET http://localhost:7001/helloworld/earth +curl -X GET http://localhost:7001/another +``` + +## Kubernetes CRaC + +```shell +minikube start +bash deploy-minikube.sh +curl $(minikube service crac-helloworld -n crac-helloworld --url)/helloworld/earth | jq +``` + +```shell +kubectl get pods +# Check first start - leghtly checkpoint creation +kubectl logs --previous --tail=100 -l app=crac-helloworld +# Check restart - fast checkpoint restoration +kubectl logs -l app=crac-helloworld +# Scale-up quickly +kubectl scale --replicas=3 deployment/crac-helloworld +``` \ No newline at end of file diff --git a/examples/crac/app.yaml b/examples/crac/app.yaml new file mode 100644 index 00000000000..ddcf9fcb85a --- /dev/null +++ b/examples/crac/app.yaml @@ -0,0 +1,79 @@ +# +# Copyright (c) 2022, 2024 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. +# +kind: Service +apiVersion: v1 +metadata: + name: crac-helloworld + labels: + app: crac-helloworld +spec: + type: NodePort + selector: + app: crac-helloworld + ports: + - port: 7001 + targetPort: 7001 + name: http +--- +apiVersion: v1 +kind: PersistentVolume +metadata: + name: crac-checkpoint +spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 1Gi + hostPath: + path: /data/crac-checkpoint/ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: crac-helloworld + labels: + app: crac-helloworld +spec: + replicas: 1 + selector: + matchLabels: + app: crac-helloworld + template: + metadata: + labels: + app: crac-helloworld + spec: + containers: + - name: crac-helloworld + image: crac-helloworld + imagePullPolicy: IfNotPresent + volumeMounts: + - mountPath: /crac-checkpoint + name: crac-checkpoint + ports: + - containerPort: 7001 + securityContext: + # TODO: be nicer + privileged: true + readinessProbe: + tcpSocket: + port: 7001 + initialDelaySeconds: 1 + periodSeconds: 1 + volumes: + - name: crac-checkpoint + hostPath: + path: /crac-checkpoint \ No newline at end of file diff --git a/examples/crac/deploy-minikube.sh b/examples/crac/deploy-minikube.sh new file mode 100644 index 00000000000..b01dbe4f228 --- /dev/null +++ b/examples/crac/deploy-minikube.sh @@ -0,0 +1,28 @@ +#!/bin/bash -e +# +# Copyright (c) 2022, 2024 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. +# +eval $(minikube docker-env) +NAMESPACE=crac-helloworld + +mvn package -DskipTests +docker build -t crac-helloworld . -f Dockerfile.crac + +kubectl delete namespace ${NAMESPACE} +# Cleanup any previous checkpoint +minikube ssh "sudo rm -rf /crac-checkpoint/cr" +kubectl create namespace ${NAMESPACE} +kubectl config set-context --current --namespace=${NAMESPACE} +kubectl apply -f . --namespace ${NAMESPACE} \ No newline at end of file diff --git a/examples/crac/measure.sh b/examples/crac/measure.sh new file mode 100644 index 00000000000..88dd891a76c --- /dev/null +++ b/examples/crac/measure.sh @@ -0,0 +1,21 @@ +#!/bin/bash -e + +# +# Copyright (c) 2024 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. +# + +curl --retry 10 --retry-all-errors --retry-delay 1 http://localhost:7001 +printf "\nMeasuring ..." +wrk -c 16 -t 16 -d 10s http://localhost:7001 \ No newline at end of file diff --git a/examples/crac/pom.xml b/examples/crac/pom.xml new file mode 100644 index 00000000000..72891ccfaa8 --- /dev/null +++ b/examples/crac/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.helidon.applications + helidon-mp + 4.0.0-SNAPSHOT + ../../../applications/mp/pom.xml + + io.helidon.examples.microprofile + helidon-examples-microprofile-hello-world-implicit + Helidon Examples Microprofile Implicit Hello World + + + Microprofile example with implicit bootstrapping (cdi.Main(new String[0]) + + + + + io.helidon.microprofile.bundles + helidon-microprofile + + + io.smallrye + jandex + runtime + true + + + io.helidon.logging + helidon-logging-jul + runtime + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-libs + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + diff --git a/examples/crac/runtimeCRaC.sh b/examples/crac/runtimeCRaC.sh new file mode 100644 index 00000000000..62e7eeb001d --- /dev/null +++ b/examples/crac/runtimeCRaC.sh @@ -0,0 +1,48 @@ +#!/bin/bash -e + +# +# Copyright (c) 2022, 2024 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. +# + +if [ ! -d "$CR_DIR" ]; +then + echo "==== Creating CRaC checkpoint ====" + echo "=== Checking CRIU compatibility(don't forget --privileged) ===" + $JAVA_HOME/lib/criu check + + echo "=== Checking glibc version ===" + # glibc version higher than 2.34.9000-29 are known to have problems with rseq + # on some kernels, workaround GLIBC_TUNABLES=glibc.pthread.rseq=0 + ldd --version | grep ldd + # Workaround for https://github.com/checkpoint-restore/criu/issues/1696 + # see https://github.com/checkpoint-restore/criu/pull/1706 + # export GLIBC_TUNABLES=glibc.pthread.rseq=0 + + echo "=== Pre-starting Helidon MP app ===" + set +e + mkdir -p "/crac-checkpoint/cr" + ./warmUp.sh & + $JAVA_HOME/bin/java -XX:CRaCCheckpointTo=$CR_DIR -jar ./*.jar + set -e + + echo "=== CRaC checkpoint created, checking log dump for errors ===" + cat $CR_DIR/dump*.log | grep "Warn\|Err\|succ" +else +echo "==== Starting directly from CRaC checkpoint ====" +./measure.sh & +exec $JAVA_HOME/bin/java -XX:CRaCRestoreFrom=$CR_DIR +fi + + diff --git a/examples/crac/src/main/java/io/helidon/microprofile/example/helloworld/implicit/CracResource.java b/examples/crac/src/main/java/io/helidon/microprofile/example/helloworld/implicit/CracResource.java new file mode 100644 index 00000000000..504d2780060 --- /dev/null +++ b/examples/crac/src/main/java/io/helidon/microprofile/example/helloworld/implicit/CracResource.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 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.microprofile.example.helloworld.implicit; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/") +public class CracResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String message() { + return "Hello World"; + } +} diff --git a/examples/crac/src/main/resources/META-INF/beans.xml b/examples/crac/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..ddb8316e3ab --- /dev/null +++ b/examples/crac/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/crac/src/main/resources/META-INF/microprofile-config.properties b/examples/crac/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..1a5d4c1be8f --- /dev/null +++ b/examples/crac/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2024 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. +# + +server.port=7001 + +# Enable the optional MicroProfile Metrics REST.request metrics +metrics.rest-request.enabled=true diff --git a/examples/crac/src/main/resources/logging.properties b/examples/crac/src/main/resources/logging.properties new file mode 100644 index 00000000000..5a5140e2e30 --- /dev/null +++ b/examples/crac/src/main/resources/logging.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2018, 2024 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. +# + +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=[%1$tc] %4$s: %2$s - %5$s %6$s%n +.level=INFO +io.helidon.microprofile.config.level=FINEST diff --git a/examples/crac/warmUp.sh b/examples/crac/warmUp.sh new file mode 100644 index 00000000000..35cfa73b58e --- /dev/null +++ b/examples/crac/warmUp.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e + +# +# Copyright (c) 2024 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. +# + + +curl --retry 10 --retry-all-errors --retry-delay 1 http://localhost:7001 +printf "\n==== Warming up ...\n" +wrk -c 16 -t 16 -d 10s http://localhost:7001 +printf "\n==== Warmup complete, creating checkpoint\n" +jcmd helidon-examples-microprofile-hello-world-implicit.jar JDK.checkpoint +#kill $(jps | grep jar | awk '{print $1}') \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index b8fe6d01211..0589d3ffbd2 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -62,6 +62,7 @@ translator-app webclient webserver + crac diff --git a/microprofile/server/pom.xml b/microprofile/server/pom.xml index 5724895cb05..1d23806d67e 100644 --- a/microprofile/server/pom.xml +++ b/microprofile/server/pom.xml @@ -51,6 +51,10 @@ io.helidon.webserver.observe helidon-webserver-observe + + org.crac + crac + io.helidon.microprofile.cdi helidon-microprofile-cdi 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 1866eb47d09..0aaa1f13cf9 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 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. @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -79,6 +80,10 @@ import jakarta.enterprise.inject.spi.ProcessProducerMethod; import jakarta.ws.rs.core.Application; import jakarta.ws.rs.ext.ParamConverterProvider; +import org.crac.CheckpointException; +import org.crac.Core; +import org.crac.Resource; +import org.crac.RestoreException; import org.eclipse.microprofile.config.ConfigProvider; import org.glassfish.jersey.internal.inject.Bindings; import org.glassfish.jersey.internal.inject.InjectionManager; @@ -92,7 +97,7 @@ /** * Extension to handle web server configuration and lifecycle. */ -public class ServerCdiExtension implements Extension { +public class ServerCdiExtension implements Extension, Resource { private static final System.Logger LOGGER = System.getLogger(ServerCdiExtension.class.getName()); private static final System.Logger STARTUP_LOGGER = System.getLogger("io.helidon.microprofile.startup.server"); private static final AtomicBoolean IN_PROGRESS_OR_RUNNING = new AtomicBoolean(); @@ -120,10 +125,15 @@ public class ServerCdiExtension implements Extension { private Context context; + private long cracRestoreTime = -1; + private final CompletableFuture> restored = new CompletableFuture<>(); + + /** * Default constructor required by {@link java.util.ServiceLoader}. */ public ServerCdiExtension() { + Core.getGlobalContext().register(this); } /** @@ -459,6 +469,18 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( } webserver = serverBuilder.build(); + if ("onStart".equalsIgnoreCase(System.getProperty("io.helidon.crac.checkpoint"))) { + try { + Core.checkpointRestore(); + } catch (UnsupportedOperationException e) { + LOGGER.log(Level.DEBUG, "CRaC feature is not available", e); + } catch (RestoreException e) { + LOGGER.log(Level.ERROR, "CRaC restore wasn't successful!", e); + } catch (CheckpointException e) { + LOGGER.log(Level.ERROR, "CRaC checkpoint creation wasn't successful!", e); + } + } + try { webserver.start(); started = true; @@ -474,9 +496,14 @@ private void startServer(@Observes @Priority(PLATFORM_AFTER + 100) @Initialized( String host = "0.0.0.0".equals(listenHost) ? "localhost" : listenHost; String note = "0.0.0.0".equals(listenHost) ? " (and all other host addresses)" : ""; + + String startupTimeReport = cracRestoreTime == -1 + ? " in " + initializationElapsedTime + " milliseconds (since JVM startup). " + : " in " + (System.currentTimeMillis() - cracRestoreTime) + " milliseconds (since CRaC restore)."; + LOGGER.log(Level.INFO, () -> "Server started on " + protocol + "://" + host + ":" + port - + note + " in " + initializationElapsedTime + " milliseconds (since JVM startup)."); + + note + startupTimeReport); // this is not needed at runtime, collect garbage serverBuilder = null; @@ -731,4 +758,17 @@ private HttpRouting.Builder findRouting(String className, return serverNamedRoutingBuilder(routingName); } + + @Override + public void beforeCheckpoint(org.crac.Context context) throws Exception { + LOGGER.log(Level.INFO, "Creating CRaC snapshot after " + + ManagementFactory.getRuntimeMXBean().getUptime() + + "ms of runtime."); + } + + @Override + public void afterRestore(org.crac.Context context) throws Exception { + cracRestoreTime = System.currentTimeMillis(); + LOGGER.log(Level.INFO, "CRaC snapshot restored!"); + } } diff --git a/microprofile/server/src/main/java/module-info.java b/microprofile/server/src/main/java/module-info.java index 20fe3655a7f..579804950e6 100644 --- a/microprofile/server/src/main/java/module-info.java +++ b/microprofile/server/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023 Oracle and/or its affiliates. + * Copyright (c) 2018, 2024 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. @@ -37,6 +37,8 @@ requires static io.helidon.common.features.api; requires static io.helidon.config.metadata; + requires crac; + requires transitive io.helidon.common.configurable; requires transitive io.helidon.common.context; diff --git a/webserver/webserver/pom.xml b/webserver/webserver/pom.xml index 19181012a4a..7b4da3ae981 100644 --- a/webserver/webserver/pom.xml +++ b/webserver/webserver/pom.xml @@ -88,6 +88,10 @@ io.helidon.config helidon-config + + org.crac + crac + io.helidon.config helidon-config-yaml diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java index cd8ce1c53fd..fe977686b33 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/LoomServer.java @@ -47,7 +47,10 @@ import io.helidon.webserver.http.DirectHandlers; import io.helidon.webserver.spi.ServerFeature; -class LoomServer implements WebServer { +import org.crac.Core; +import org.crac.Resource; + +class LoomServer implements WebServer, Resource { private static final System.Logger LOGGER = System.getLogger(LoomServer.class.getName()); private static final String EXIT_ON_STARTED_KEY = "exit.on.started"; private static final AtomicInteger WEBSERVER_COUNTER = new AtomicInteger(1); @@ -101,6 +104,7 @@ class LoomServer implements WebServer { }); listeners = Map.copyOf(listenerMap); + Core.getGlobalContext().register(this); } @Override @@ -272,6 +276,42 @@ private boolean parallel(String taskName, Consumer task) { return result; } + @Override + public void beforeCheckpoint(org.crac.Context context) throws Exception { + try { + lifecycleLock.lockInterruptibly(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted", e); + } + try { + if (running.get()) { + for (ServerListener listener : listeners.values()) { + listener.suspend(); + } + } + } finally { + lifecycleLock.unlock(); + } + } + + @Override + public void afterRestore(org.crac.Context context) throws Exception { + try { + lifecycleLock.lockInterruptibly(); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted", e); + } + try { + if (running.get()) { + for (ServerListener listener : listeners.values()) { + listener.resume(); + } + } + } finally { + lifecycleLock.unlock(); + } + } + private record ListenerFuture(ServerListener listener, Future future) { } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java index c5e0761f7a1..67ff1e14b22 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/ServerListener.java @@ -74,9 +74,9 @@ class ServerListener implements ListenerContext { private final Router router; private final HelidonTaskExecutor readerExecutor; private final ExecutorService sharedExecutor; - private final Thread serverThread; + private Thread serverThread; private final DirectHandlers directHandlers; - private final CompletableFuture closeFuture; + private CompletableFuture closeFuture; private final Tls tls; private final SocketOptions connectionOptions; private final InetSocketAddress configuredAddress; @@ -90,6 +90,7 @@ class ServerListener implements ListenerContext { private final Map activeConnections = new ConcurrentHashMap<>(); private volatile boolean running; + private volatile boolean inCheckpoint; private volatile int connectedPort; private volatile ServerSocket serverSocket; @@ -136,11 +137,7 @@ class ServerListener implements ListenerContext { .build()); this.gracePeriod = listenerConfig.shutdownGracePeriod(); - this.serverThread = Thread.ofPlatform() - .inheritInheritableThreadLocals(true) - .daemon(false) - .name("server-" + socketName + "-listener") - .unstarted(this::listen); + initServerThread(); // to read requests and execute tasks this.readerExecutor = ThreadPerTaskExecutor.create(Thread.ofVirtual() @@ -150,8 +147,6 @@ class ServerListener implements ListenerContext { this.sharedExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() .factory()); - this.closeFuture = new CompletableFuture<>(); - int port = listenerConfig.port(); if (port < 1) { port = 0; @@ -166,6 +161,15 @@ class ServerListener implements ListenerContext { ith.start(); } + private void initServerThread() { + this.closeFuture = new CompletableFuture<>(); + this.serverThread = Thread.ofPlatform() + .inheritInheritableThreadLocals(true) + .daemon(false) + .name("server-" + socketName + "-listener") + .unstarted(this::listen); + } + @Override public MediaContext mediaContext() { return mediaContext; @@ -214,43 +218,51 @@ void stop() { return; } running = false; + suspend(true); + router.afterStop(); + } + + private void suspend(boolean shutdownExecutors) { try { // Stop listening for connections serverSocket.close(); - // Shutdown reader executor - readerExecutor.terminate(gracePeriod.toMillis(), TimeUnit.MILLISECONDS); - if (!readerExecutor.isTerminated()) { - LOGGER.log(DEBUG, "Some tasks in reader executor did not terminate gracefully"); - readerExecutor.forceTerminate(); - } + if (shutdownExecutors) { + // Shutdown reader executor + readerExecutor.terminate(gracePeriod.toMillis(), TimeUnit.MILLISECONDS); + if (!readerExecutor.isTerminated()) { + LOGGER.log(DEBUG, "Some tasks in reader executor did not terminate gracefully"); + readerExecutor.forceTerminate(); + } - // Shutdown shared executor - try { - sharedExecutor.shutdown(); - boolean done = sharedExecutor.awaitTermination(gracePeriod.toMillis(), TimeUnit.MILLISECONDS); - if (!done) { - List running = sharedExecutor.shutdownNow(); - if (!running.isEmpty()) { - LOGGER.log(DEBUG, running.size() + " tasks in shared executor did not terminate gracefully"); + // Shutdown shared executor + try { + sharedExecutor.shutdown(); + boolean done = sharedExecutor.awaitTermination(gracePeriod.toMillis(), TimeUnit.MILLISECONDS); + if (!done) { + List running = sharedExecutor.shutdownNow(); + if (!running.isEmpty()) { + LOGGER.log(DEBUG, running.size() + " tasks in shared executor did not terminate gracefully"); + } } + } catch (InterruptedException e) { + // falls through } - } catch (InterruptedException e) { - // falls through } - } catch (IOException e) { LOGGER.log(INFO, "Exception thrown on socket close", e); } serverThread.interrupt(); closeFuture.join(); - router.afterStop(); } @SuppressWarnings("resource") void start() { router.beforeStart(); + startIt(); + } + private void startIt() { try { SSLServerSocket sslServerSocket = tls.enabled() ? tls.createServerSocket() : null; serverSocket = tls.enabled() ? sslServerSocket : new ServerSocket(); @@ -349,7 +361,7 @@ private void listen() { tls); readerExecutor.execute(handler); } catch (RejectedExecutionException e) { - LOGGER.log(ERROR, "Executor rejected handler for new connection"); + LOGGER.log(ERROR, "Executor rejected handler for new connection", e); // the socket was never handled try { @@ -379,12 +391,16 @@ private void listen() { if (!e.getMessage().contains("Socket closed")) { LOGGER.log(ERROR, "Got a socket exception while listening, this server socket is terminating now", e); } - if (running) { + if (inCheckpoint) { + break; + } else if (running) { stop(); } } catch (Throwable e) { LOGGER.log(ERROR, "Got a throwable while listening, this server socket is terminating now", e); - if (running) { + if (inCheckpoint) { + break; + } else if (running) { stop(); } } @@ -397,4 +413,17 @@ private void listen() { private List activeConnections() { return new ArrayList<>(activeConnections.values()); } + + void suspend() { + inCheckpoint = true; + suspend(false); + serverThread = null; + closeFuture = null; + } + + void resume() { + initServerThread(); + startIt(); + inCheckpoint = false; + } } diff --git a/webserver/webserver/src/main/java/module-info.java b/webserver/webserver/src/main/java/module-info.java index 2a05565181c..e793f686d4f 100644 --- a/webserver/webserver/src/main/java/module-info.java +++ b/webserver/webserver/src/main/java/module-info.java @@ -34,6 +34,7 @@ requires io.helidon.logging.common; requires java.management; requires io.helidon; + requires crac; requires transitive io.helidon.common.buffers; requires transitive io.helidon.common.context;