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 extends Resource> context) throws Exception {
+ LOGGER.log(Level.INFO, "Creating CRaC snapshot after "
+ + ManagementFactory.getRuntimeMXBean().getUptime()
+ + "ms of runtime.");
+ }
+
+ @Override
+ public void afterRestore(org.crac.Context extends Resource> 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 extends Resource> 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 extends Resource> 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;