Micronaut CRaC

Learn how to start using CRaC with a Micronaut Application

Authors: Sergio del Amo

Micronaut Version: 4.3.8

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:

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.

5. 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
If you don’t specify the --build argument, Gradle 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:

build.gradle
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:

src/main/java/example/micronaut/LoggingResource.java
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:

src/main/java/example/micronaut/HelloWorldController.java
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.

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:

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;
    }
}

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:

src/main/java/example/micronaut/TimeController.java
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.

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");
    }
}

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:

src/main/java/example/micronaut/Application.java
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:

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;
    }
}
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();
    }
}

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.

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

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

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 since the application has yet to be restored.

12. Next steps

Explore more features with Micronaut Guides.

Learn more about:

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…​).