Java / Gradle

Micronaut CRaC

Learn how to start using CRaC with a Micronaut application.

Sergio del Amo
On this guide
In this section

Getting Started

In this guide, we will create a Micronaut application written in Java.

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

What you will need

To complete this guide, you will need the following:

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.

Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
    --features=crac,management \
    --build=gradle \
    --lang=java \
    --test=junit
Note
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.

Note
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.

Micronaut CRaC Dependency

Add the Micronaut CRaC dependency:

build.gradle
implementation("io.micronaut.crac:micronaut-crac")

It has a transitive dependency on org.crac:crac.

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 OrderedResource in the CRaC Context.

You could create such a bean:

java/src/main/java/example/micronaut/LoggingResource.java

Checkpoint Simulator

CRaC only works with Linux. To ease testing and development in operating systems that 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:

java/src/main/java/example/micronaut/HelloWorldController.java

To simplify testing, we create a utility class that allows you to run a test scenario before and after the checkpoint.

java/src/test/java/example/micronaut/CheckpointTestUtils.java
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:

java/src/test/java/example/micronaut/HelloWorldControllerTest.java
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;
    }
}

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:

java/src/main/java/example/micronaut/TimeController.java

The following test verifies the TimeController is invalidated before a checkpoint.

java/src/test/java/example/micronaut/TimeControllerTest.java
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");
    }
}

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:

src/main/java/example/micronaut/Application.java
package example.micronaut;

import org.jspecify.annotations.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:

java/src/test/java/example/micronaut/Clock.java
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;
    }
}
java/src/test/java/example/micronaut/EagerlyInitializedTest.java
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();
    }
}

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.

build.gradle
implementation("io.micronaut:micronaut-management")

The following test verifies the crac section is present in the info endpoint.

java/src/test/java/example/micronaut/InfoEndpointTest.java
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 because the application has yet to be restored.

Next Steps

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).