Get started with Vert.x

This tutorial helps you integrate Vert.x with Hazelcast and use Hazelcast for distributed session management and other distributed data structures.

In this tutorial, you will

  • start with a simple Vert.x Hello World application

  • add vertx-hazelcast module and enable distributed session management

  • use io.vertx.core.shareddata.Counter data structure to implement a unique id generator

Prerequisites

  • Java 17 or newer

  • Maven 3.9+

  • httpie client

Create a new project

  1. Go to start.vertx.io, change the artifact id to messages, the version to 5.0.0, and generate a new project.

  2. Extract the project and build it using:

$ mvn clean package

and start the application using:

java -jar target/messages-1.0.0-SNAPSHOT-fat.jar

You should see output similar to the following:

HTTP server started on port 8888
Aug 29, 2024 2:22:38 PM io.vertx.launcher.application.VertxApplication
INFO: Succeeded in deploying verticle

Storing Data in Session

Go to the MainVerticle.java file and replace the contents of the start method with the following:

This tutorial uses 2-space indentation, which is customary for Vertx projects due to the high number of nested callbacks.
  public void start() {
    // Create a Router
    Router router = router(vertx);

    // Create local SessionStore
    SessionStore store = LocalSessionStore.create(vertx);

    // Use the SessionStore to handle all requests
    router.route()
      .handler(SessionHandler.create(store));

    router.route(HttpMethod.PUT, "/").handler(context -> {
      context.request().bodyHandler(body -> {
        List<String> messages = getMessagesFromSession(context);

        JsonObject json = body.toJsonObject();
        String message = json.getString("message");
        messages.add(message);

        putMessagesToSession(context, messages);

        context.json(
          new JsonObject()
            .put("messages", messages)
        );
      });
    });

    // Create the HTTP server
    vertx.createHttpServer()
      // Handle every request using the router
      .requestHandler(router)
      // Start listening
      .listen(8888)
      // Print the port
      .onSuccess(server ->
        System.out.println(
          "HTTP server started on port " + server.actualPort()
        )
      );
  }

  private static List<String> getMessagesFromSession(RoutingContext context) {
    String messages = context.session().get("messages");
    if (messages == null) {
      return new ArrayList<>();
    } else {
      return new ArrayList<>(Arrays.asList(messages.split(",")));
    }
  }

  private void putMessagesToSession(RoutingContext context, List<String> messages) {
    context.session().put("messages", String.join(",", messages));
  }
$ http put localhost:8888 message=Hello\ World!
HTTP/1.1 200 OK
content-length: 29
content-type: application/json
set-cookie: vertx-web.session=ed22f77473a7f613c9305431a62832a6; Path=/

{
    "messages": [
        "Hello World!"
    ]
}

Execute another request with the cookie:

$ http put localhost:8888 'Cookie:vertx-web.session=ed22f77473a7f613c9305431a62832a6' message=Hello\ World\ 2!
HTTP/1.1 200 OK
content-length: 46
content-type: application/json

{
    "messages": [
        "Hello World!",
        "Hello World 2!"
    ]
}

Distributed Sessions

Let’s modify the code, so we can start multiple instances easily - the application will start on the defined port, and when the port is not available it will search for another port:

Add the following method to the MainVerticle.java class:

  private int findFreePort(int from) {
    for (int port = from; port < from + 100; port++) {
      try {
        new ServerSocket(port).close();
        return port;
      } catch (IOException e) {
        // port not available, try next
      }
    }
    throw new RuntimeException("Could not find an available port");
  }

and use it in the start method:

    ...
    int port = findFreePort(8888);

    // Create the HTTP server
    vertx.createHttpServer()
      // Handle every request using the router
      .requestHandler(router)
      // Start listening
      .listen(port)
    ...

Now, we can start two instances:

$ java -jar target/vertx-hz-1.0.0-SNAPSHOT-fat.jar
HTTP server started on port 8888
Aug 30, 2024 9:09:44 AM io.vertx.launcher.application.VertxApplication
INFO: Succeeded in deploying verticle

...

$ java -jar target/vertx-hz-1.0.0-SNAPSHOT-fat.jar
HTTP server started on port 8889
Aug 30, 2024 9:09:47 AM io.vertx.launcher.application.VertxApplication
INFO: Succeeded in deploying verticle

and we can see the session is not shared between the instances. Here is the request to the first instance:

$ http PUT localhost:8888 message="Hello world"
HTTP/1.1 200 OK
content-length: 28
content-type: application/json
set-cookie: vertx-web.session=00f219c166ca50727d23eaaf9fe54229; Path=/

{
    "messages": [
        "Hello world"
    ]
}

and here is the request to the 2nd instance. Notice the different port and that we use the cookie we received, but the data does not contain the previous message.

$ http PUT localhost:8889 message="Hello world 2" 'Cookie: vertx-web.session=00f219c166ca50727d23eaaf9fe54229'
HTTP/1.1 200 OK
content-length: 30
content-type: application/json
set-cookie: vertx-web.session=a1486c5ed6416972fdc356e4d91d2397; Path=/

{
    "messages": [
        "Hello world 2"
    ]
}

We will fix that by using a Hazelcast Cluster Manager. This resides in the io.vertx:vertx-hazelcast module, which is maintained by the Vert.x team with contributions from Hazelcast and is based on Hazelcast Community Edition.

Change the following part of the start method:

// Create local SessionStore
SessionStore store = LocalSessionStore.create(vertx);

to the following:

// Create clustered SessionStore
SessionStore store = ClusteredSessionStore.create(vertx);

and from now on we will start the application with -server parameter, which tells Vert.x to look for a cluster manager implementation.

We also need to provide a Hazelcast configuration file, and create a file cluster.xml in the src/main/resources directory:

<?xml version="1.0" encoding="UTF-8"?>

<hazelcast xmlns="http://www.hazelcast.com/schema/config"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.hazelcast.com/schema/config
           https://www.hazelcast.com/schema/config/hazelcast-config-5.5.xsd">

  <network>
    <join>
      <multicast enabled="true"/>
    </join>
  </network>

  <multimap name="__vertx.subs">
    <backup-count>1</backup-count>
    <value-collection-type>SET</value-collection-type>
  </multimap>

  <map name="__vertx.haInfo">
    <backup-count>1</backup-count>
  </map>

  <map name="__vertx.nodeInfo">
    <backup-count>1</backup-count>
  </map>

Now rebuild the project and start the application. You will see more verbose output as Hazelcast prints its own startup logs:

$ java -jar target/vertx-hz-1.0.0-SNAPSHOT-fat.jar -cluster
...
HTTP server started on port 8888
...
Members {size:2, ver:2} [
	Member [192.168.0.10]:5701 - e29f0362-f9a9-4708-b6e5-1a6067b5aa39 this
	Member [192.168.0.10]:5702 - 74014573-a18a-44f2-9ca7-fd90b70dcb43
]
...

and

$ java -jar target/vertx-hz-1.0.0-SNAPSHOT-fat.jar -cluster
...
HTTP server started on port 8889
...
Members {size:2, ver:2} [
	Member [192.168.0.10]:5701 - e29f0362-f9a9-4708-b6e5-1a6067b5aa39
	Member [192.168.0.10]:5702 - 74014573-a18a-44f2-9ca7-fd90b70dcb43 this
]
...

Putting two messages into different instances while using the same cookie, we see that the session is shared between the instances.

$ http PUT localhost:8888 message="Hello world"
HTTP/1.1 200 OK
content-length: 31
content-type: application/json
set-cookie: vertx-web.session=1ab47cb96731123135f25ec7b67efd64; Path=/

{
    "messages": [
        "",
        "Hello world"
    ]
}
$ http PUT localhost:8889 message="Hello world 2" 'Cookie: vertx-web.session=674806546c690674962f279670abefcf'
HTTP/1.1 200 OK
content-length: 44
content-type: application/json

{
    "messages": [
        "Hello world",
        "Hello world 2"
    ]
}

Using Counter

Replace this part of the code at the end of the start() method:

context.json(
  new JsonObject()
    .put("messages", messages)
);

with the following:

context.vertx()
  .sharedData()
  .getCounter("requestId")
  .onSuccess(counter -> {
    counter.incrementAndGet()
      .onSuccess(requestId -> {
        context.json(
          new JsonObject()
            .put("requestId", requestId)
            .put("messages", messages)
        );
      });
  });

When you now try the application, you can see the response contains an additional field named requestId and its value increments for every request.

$ http PUT localhost:8888 message="Hello world"
HTTP/1.1 200 OK
content-length: 42
content-type: application/json
set-cookie: vertx-web.session=d9fb4cada5c0fc625089a38f3de13e3c; Path=/

{
    "messages": [
        "Hello world"
    ],
    "requestId": 1
}