mn create-app example.micronaut.micronautguide \
--features=crac,management \
--build=gradle \
--lang=java \
--test=junit
Table of Contents
- 1. Getting Started
- 2. What is Coordinated Restore at Checkpoint (CRaC)?
- 3. What you will need
- 4. Solution
- 5. Writing the Application
- 6. Micronaut CRaC Dependency
- 7. Create Resources
- 8. Checkpoint Simulator
- 9. Refreshable beans and CRaC
- 10. Eagerly initialize Singletons
- 11. Info endpoint
- 12. Next Steps
- 13. License
Micronaut CRaC
Learn how to start using CRaC with a Micronaut Application
Authors: Sergio del Amo
Micronaut Version: 4.6.3
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
2. What is Coordinated Restore at Checkpoint (CRaC)?
Coordinated Restore at Checkpoint (CRaC) is a JDK project that can start projects - on Linux - with shorter time to first transaction and less time and resources to achieve full code speed. CRaC effectively takes a snapshot of the Java process when it is fully warmed up, then uses that snapshot to launch any number of JVMs from this captured state
3. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOME
configured appropriately
4. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
5. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
If you use Micronaut Launch, select Micronaut Application as application type and add crac
, and management
features.
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application. |
6. Micronaut CRaC Dependency
Add the Micronaut CRaC dependency:
implementation("io.micronaut.crac:micronaut-crac")
It has a transitive dependency to org.crac:crac.
7. Create Resources
One limitation of CRaC is that you cannot have open files or sockets when a checkpoint is created. To support this with Micronaut CRaC, you will write resources to close files and connections, dump cache, etc.
You will use the Micronaut CRaC API OrderedResource
.
public interface OrderedResource extends org.crac.Resource, io.micronaut.core.order.Ordered { }
Micronaut CRaC automatically registers beans of type OrderedResources
in the CRaC Context.
You could create such a bean:
package example.micronaut;
import io.micronaut.crac.OrderedResource;
import jakarta.inject.Singleton;
import org.crac.Context;
import org.crac.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton (1)
public class LoggingResource implements OrderedResource {
private static final Logger LOG = LoggerFactory.getLogger(LoggingResource.class);
@Override
public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
LOG.info("before checkpoint");
}
@Override
public void afterRestore(Context<? extends Resource> context) throws Exception {
LOG.info("after restore");
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
8. Checkpoint Simulator
CRaC only works with Linux.
To ease testing and development in operating systems which don’t support CRaC, Micronaut CRaC includes the CheckpointSimulator
API.
With CRaC, you run your application to a point and then "checkpoint" it. This calls the app to close all its sockets and file handles, and then dumps the memory to disk. When it restarts from this snapshot, it calls the app again to say it’s been restored, and one can re-open files and network connections.
The simulator allows you to synthesise these two calls (before checkpoint and after restore), so that under a test you can check your service works again after it was closed and recreated.
Given a controller such as:
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.util.Collections;
import java.util.Map;
@Controller (1)
class HelloWorldController {
@Get (2)
Map<String, String> index() {
return Collections.singletonMap("message", "Hello World");
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path / . |
2 | The @Get annotation maps the method to an HTTP GET request. |
To simplify testing, we create a utility class that allows you to run a test scenario before and after the checkpoint.
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.crac.test.CheckpointSimulator;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import java.util.function.Function;
public class CheckpointTestUtils {
public static void test(Function<BlockingHttpClient, Object> testScenario) {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class)) {
CheckpointSimulator checkpointSimulator =
server.getApplicationContext().getBean(CheckpointSimulator.class);
testApp(server, testScenario);
checkpointSimulator.runBeforeCheckpoint();
server.stop();
checkpointSimulator.runAfterRestore();
server.start();
testApp(server, testScenario);
}
}
public static Object testApp(EmbeddedServer embeddedServer, Function<BlockingHttpClient, Object> clientConsumer) {
try (HttpClient httpClient = embeddedServer.getApplicationContext().createBean(HttpClient.class, embeddedServer.getURL())) {
BlockingHttpClient client = httpClient.toBlocking();
return clientConsumer.apply(client);
}
}
}
You could write a test for the previous controller:
package example.micronaut;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class HelloWorldControllerTest {
@Test
void emulateCheckpoint() {
CheckpointTestUtils.test(this::testHelloWorld);
}
private Void testHelloWorld(BlockingHttpClient client) {
HttpResponse<Map<String, String>> response = client.exchange(HttpRequest.GET("/"), Argument.mapOf(String.class, String.class));
assertEquals(HttpStatus.OK, response.getStatus());
Optional<Map<String, String>> bodyOptional = response.getBody();
assertTrue(bodyOptional.isPresent());
assertEquals(Collections.singletonMap("message", "Hello World"), bodyOptional.get());
return null;
}
}
9. Refreshable beans and CRaC
Micronaut CRaC ships with several built-in beans of type OrderedResource
. One of them is the RefreshEventResource
. RefreshEventResource
emits a RefreshEvent
, causing beans in the @Refreshable
scope to be invalidated before a checkpoint.
Given a controller such as:
package example.micronaut;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.runtime.context.scope.Refreshable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Map;
@Refreshable (1)
@Controller("/time") (1)
class TimeController {
private final String time;
TimeController() {
time = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
}
@Get (3)
Map<String, Object> index() {
return Collections.singletonMap("time", time);
}
}
1 | @Refreshable scope is a custom scope that allows a bean’s state to be refreshed via the /refresh endpoint. |
2 | The class is defined as a controller with the @Controller annotation mapped to the path /time . |
3 | The @Get annotation maps the method to an HTTP GET request. |
The following test verifies the TimeController
is offloaded prior to a checkpoint.
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.crac.test.CheckpointSimulator;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TimeControllerTest {
@Test
void emulateCheckpoint() {
try (EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class)) {
CheckpointSimulator checkpointSimulator =
server.getApplicationContext().getBean(CheckpointSimulator.class);
Object time = CheckpointTestUtils.testApp(server, this::testTime);
assertEquals(time, CheckpointTestUtils.testApp(server, this::testTime));
checkpointSimulator.runBeforeCheckpoint();
server.stop();
checkpointSimulator.runAfterRestore();
server.start();
assertNotEquals(time, CheckpointTestUtils.testApp(server, this::testTime));
}
}
private String testTime(BlockingHttpClient client) {
HttpResponse<Map<String, String>> response = client.exchange(HttpRequest.GET("/time"), Argument.mapOf(String.class, String.class));
assertEquals(HttpStatus.OK, response.getStatus());
Optional<Map<String, String>> bodyOptional = response.getBody();
assertTrue(bodyOptional.isPresent());
Map<String, String> body = bodyOptional.get();
assertEquals(1, body.keySet().size() );
assertTrue(body.containsKey("time"));
return body.get("time");
}
}
10. Eagerly initialize Singletons
When you use CRaC, you should use eager initialization to ensure the application is fully loaded before the checkpoint is taken.
Modify your application class:
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.context.ApplicationContextBuilder;
import io.micronaut.context.ApplicationContextConfigurer;
import io.micronaut.context.annotation.ContextConfigurer;
import io.micronaut.runtime.Micronaut;
public class Application {
@ContextConfigurer
public static class Configurer implements ApplicationContextConfigurer {
@Override
public void configure(@NonNull ApplicationContextBuilder builder) {
builder.eagerInitSingletons(true);
}
}
public static void main(String[] args) {
Micronaut.run(Application.class, args);
}
}
You can test eager initialization:
package example.micronaut;
import jakarta.inject.Singleton;
import java.time.LocalDateTime;
@Singleton
class Clock {
private final LocalDateTime now;
Clock() {
now = LocalDateTime.now();
}
LocalDateTime getNow() {
return now;
}
}
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static java.lang.Thread.sleep;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EagerlyInitializedTest {
@Test
void singletonsAreEagerlyInitialized() throws InterruptedException {
ApplicationContext ctx = ApplicationContext.run();
sleep(5_000);
assertTrue(ctx.getBean(Clock.class).getNow().isBefore(LocalDateTime.now().minusSeconds(3)));
ctx.close();
}
}
11. Info endpoint
The info endpoint includes a crac
section, which shows the restore time and uptime since restore, both in milliseconds.
To expose the info endpoint, you need the following dependency on your classpath.
implementation("io.micronaut:micronaut-management")
The following test verifies the crac
section is present in the info endpoint.
package example.micronaut;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@Property(name = "endpoints.info.enabled", value = StringUtils.TRUE)
@Property(name = "endpoints.info.sensitive", value = StringUtils.FALSE)
@MicronautTest
class InfoEndpointTest {
@Test
void cracInformationIsExposedInTheInfoEndpointExposed(@Client("/") HttpClient client) {
Map<String, Map<String, Integer>> json = assertDoesNotThrow(() -> client.toBlocking().retrieve(HttpRequest.GET("/info"), Argument.mapOf(Argument.of(String.class), Argument.mapOf(String.class, Integer.class))));
assertNotNull(json);
assertEquals(Map.of("crac", Map.of("restore-time", -1, "uptime-since-restore", -1)), json);
}
}
The previous test shows -1 since the application has yet to be restored.
12. Next Steps
Explore more features with Micronaut Guides.
Learn more about:
-
https://micronaut-projects.github.io/micronaut-crac/latest/guide/[Micronaut CRaC (Coordinated Restore at checkpoint)
13. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |