Xenon Rest Service Framework

Overview

Simple rest service using VMWare Xenon Framework. We will deploy multi-node instance of the rest service and explore the xenon UI.

Xenon

Xenon is a framework for writing small REST-based services. When you write a rest service in other frameworks like spring you have to pick a data store like mongo db to persist your data, you have to maintain separate instances of mongo db to ensure replication works. You have to then deploy your rest service on distributed environment using docker swarm and ensure high availability of your service. Xenon framework does all this for you with just a single library. Here we will see how to create a simple rest service using xenon and then see how we can achieve asynchronous call,distributed node deployment, replication, synchronization, ordering, and consistency of data across those nodes. We will see how we can scale the application etc.

Create a simple maven project. If you are using vscode ctrl+shift+p 'Maven: Generate from maven Archetype' and select a folder, click on 'maven-archetype-quickstart' and enter the project details. You can also run the command and enter 'groupId': com.demo.xenon & 'artifactId': myxenon

1$ mvn archetype:generate -DarchetypeArtifactId="maven-archetype-quickstart" -DarchetypeGroupId="org.apache.maven.archetypes"

Add the xenon dependency to your pom.xml & update the plugins. The 'xenon-common' is the only core library you need, but to visualize things we have added 'xenon-ui'

 1<dependencies>
 2  <dependency>
 3    <groupId>com.vmware.xenon</groupId>
 4    <artifactId>xenon-common</artifactId>
 5    <version>1.4.0</version>
 6  </dependency>
 7  <dependency>
 8    <groupId>com.vmware.xenon</groupId>
 9    <artifactId>xenon-ui</artifactId>
10    <version>1.4.0</version>
11  </dependency>
12</dependencies>
13
14<build>
15  <plugins>
16    <plugin>
17      <groupId>org.apache.maven.plugins</groupId>
18      <artifactId>maven-compiler-plugin</artifactId>
19      <version>3.7.0</version>
20      <configuration>
21        <source>1.8</source>
22        <target>1.8</target>
23      </configuration>
24    </plugin>
25    <plugin>
26      <groupId>org.apache.maven.plugins</groupId>
27      <artifactId>maven-assembly-plugin</artifactId>
28      <version>2.4.1</version>
29      <configuration>
30        <descriptorRefs>
31          <descriptorRef>jar-with-dependencies</descriptorRef>
32        </descriptorRefs>
33        <archive>
34          <manifest>
35            <mainClass>com.demo.xenon.App</mainClass>
36          </manifest>
37        </archive>
38      </configuration>
39      <executions>
40        <execution>
41          <id>make-assembly</id>
42          <phase>package</phase>
43          <goals>
44            <goal>single</goal>
45          </goals>
46        </execution>
47      </executions>
48    </plugin>
49    <plugin>
50      <groupId>org.codehaus.mojo</groupId>
51      <artifactId>exec-maven-plugin</artifactId>
52      <version>1.6.0</version>
53      <configuration>
54        <mainClass>com.demo.xenon.App</mainClass>
55      </configuration>
56    </plugin>
57  </plugins>
58</build>

Modify your App.java, Your App class extends the ServiceHost class. If you want you nodes to have specific name modify 'defaultArgs.id'. Note the 'defaultArgs.sandbox' path, this is where the data files of the data store will reside. On windows it will be 'C:/tmp/xenondb'.

 1package com.demo.xenon;
 2
 3import java.nio.file.Paths;
 4import java.util.UUID;
 5
 6import com.vmware.xenon.common.ServiceHost;
 7import com.vmware.xenon.services.common.RootNamespaceService;
 8import com.vmware.xenon.ui.UiService;
 9
10public class App extends ServiceHost {
11  public static void main(String[] args) throws Throwable {
12    App appHost = new App();
13    Arguments defaultArgs = new Arguments();
14    defaultArgs.id = "host:" + UUID.randomUUID();
15    defaultArgs.sandbox = Paths.get("/tmp/xenondb");
16    appHost.initialize(args, defaultArgs);
17    appHost.start();
18    Runtime.getRuntime().addShutdownHook(new Thread(appHost::stop));
19  }
20
21  @Override
22  public ServiceHost start() throws Throwable {
23    super.start();
24    startDefaultCoreServicesSynchronously();
25    super.startService(new RootNamespaceService());
26    super.startService(new UiService());
27    return this;
28  }
29}

Build the project and execute the code. Note: Do not try the approach mvn exec:exec as this will detach the java process on ctrl+c and you will have to kill the java process manually.

1$ mvn clean install
2$ mvn exec:java

Your xenon server should now be up. You should view http://localhost:8000/core/ui/default

You just hosted the xenon server. You have not yet written a rest service. At this point you already get 13 core services which are transactions,resource-groups,roles,local-query-tasks,credentials,sync-tasks,graph-queries,users,user-groups,node-groups,tenants,processes,query-tasks. The custom services card at the bottom shows a count of 0. Now lets write our first xenon rest service.

Create a new class BookStoreService.java

 1package com.demo.xenon;
 2
 3import com.vmware.xenon.common.Operation;
 4import com.vmware.xenon.common.ServiceDocument;
 5import com.vmware.xenon.common.ServiceDocumentDescription.PropertyUsageOption;
 6import com.vmware.xenon.common.StatefulService;
 7import com.vmware.xenon.common.Utils;
 8
 9public class BookStoreService extends StatefulService {
10
11  public static final String FACTORY_LINK = "/myservice/books";
12
13  public static class Book extends ServiceDocument {
14
15    @UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
16    @UsageOption(option = PropertyUsageOption.REQUIRED)
17    public String bookName;
18
19    @UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
20    @UsageOption(option = PropertyUsageOption.REQUIRED)
21    public Double bookPrice;
22
23  }
24
25  public BookStoreService() {
26    super(Book.class);
27    toggleOption(ServiceOption.PERSISTENCE, true);
28    toggleOption(ServiceOption.REPLICATION, true);
29    toggleOption(ServiceOption.INSTRUMENTATION, true);
30    toggleOption(ServiceOption.OWNER_SELECTION, true);
31  }
32
33  @Override
34  public void handleCreate(Operation startPost) {
35    Book book = getBody(startPost);
36    Utils.validateState(getStateDescription(), book);
37    startPost.complete();
38  }
39
40  @Override
41  public void handlePut(Operation put) {
42    Book book = getBody(put);
43    Utils.validateState(getStateDescription(), book);
44    setState(put, book);
45    put.complete();
46  }
47
48  @Override
49  public void handlePatch(Operation patch) {
50    Book bookState = getState(patch);
51    Book book = getBody(patch);
52    Utils.mergeWithState(getStateDescription(), bookState, book);
53    patch.setBody(bookState);
54    patch.complete();
55  }
56}

Add this line to the App.java start method & Run the program

1super.startFactory(new BookStoreService());
1$ mvn exec:java

You can now create a book.

1curl -X POST -H 'Content-Type: application/json' -i http://localhost:8000/myservice/books --data '{
2bookName: "book1",
3bookPrice: 2.0
4}'
5
6curl -X GET -H 'Content-Type: application/json' -i http://localhost:8000/myservice/books
7
8curl -X GET -H 'Content-Type: application/json' -i http://localhost:8000/myservice/books/b5533c1dd2d595c557181891d2dc0

Response:

 1{
 2  "documentLinks": [
 3    "/myservice/books/b5533c1dd2d595c557181891d2dc0"
 4  ],
 5  "documentCount": 1,
 6  "queryTimeMicros": 17999,
 7  "documentVersion": 0,
 8  "documentUpdateTimeMicros": 0,
 9  "documentExpirationTimeMicros": 0,
10  "documentOwner": "host:e413831a-3247-4dfb-aecf-24d319591a84"
11}
 1{
 2  "bookName": "book1",
 3  "bookPrice": 2.0,
 4  "documentVersion": 0,
 5  "documentEpoch": 0,
 6  "documentKind": "com:demo:xenon:BookStoreService:Book",
 7  "documentSelfLink": "/myservice/books/b5533c1dd2d595c557181891d2dc0",
 8  "documentUpdateTimeMicros": 1532181069064001,
 9  "documentUpdateAction": "POST",
10  "documentExpirationTimeMicros": 0,
11  "documentOwner": "host:e413831a-3247-4dfb-aecf-24d319591a84"
12}

Owner Selection

Now lets assume you have 1 million book entries, will the data be replicated across all nodes? The data replication will be shared among the nodes that have ServiceOption.OWNER_SELECTION enabled. By enabling this you are telling the service that this node will take ownership of storing the data. So if there are 3 nodes in the poll then each node will store 1/3 of the 1 million records in the data store, if one of the node goes down then rebalancing happens and now the 2 nodes each have 1/2 of the 1 million records. When you request hits a server which doesnt have that data stored locally the request then gets forwarded to the node which is the owner of that data. You can have few nodes within the pool with ServiceOption.OWNER_SELECTION disabled they will internally forward the requests to the OWNER nodes.

Mutlinode

Multi node capability in xenon provides high availability & scalability in terms of storage & request processing.

Stateful Service vs Stateless Service

What we wrote above is a stateful rest service. A stateful service invovles data that needs persistence. An example of a stateless rest service would be a proxy service.

Replication factor & Quorum

The replication factor tells xenon how many nodes the stateful service needs to be replicated over. Default is all. Quroum tells xenon on how many nodes the persist operation should be successful before considering something as persisted. If you have defined quorum of 3 nodes and 1 node fails then all future write requests to will fail as there arent enough members to validate the quorum. Default is majority quorum [n/2+1] where n is replication factor. On a 3 node with replication factor all, quorum is [3/2+1] = 2 which means 2 nodes have to agree for a write to be committed.

ServiceOptionDescription
PERSISTENCEpersists data
REPLICATIONreplicates data across nodes
INSTRUMENTATIONprovides stats about service
OWNER_SELECTIONtakes ownership of storing data
PropertyUsageOption AnnotationDescription
IDid field
AUTO_MERGE_IF_NOT_NULLhelper method will merge current state with state supplied in body in case of updates
OPTIONALnot optional
REQUIREDmandatory field
SERVICE_USEused internally

Now let us deploy our service in a distributed environment. Delete the test folder as we wont be covering it here. Run the command

1$ mvn clean install
2
3$ cd target;
4
5$ java -jar myxenon-1.0-SNAPSHOT-jar-with-dependencies.jar

This should start the single node instance of your book store rest service.

Now lets add 2 more nodes to the quorum. All the 3 nodes ("Service Host") will form what is called a node group. Replication happens within this node group.

1$ java -jar myxenon-1.0-SNAPSHOT-jar-with-dependencies.jar --port=8001 --peerNodes=http://localhost:8000
2$ java -jar myxenon-1.0-SNAPSHOT-jar-with-dependencies.jar --port=8002 --peerNodes=http://localhost:8000

It will take a few seconds for the nodes to synchronize/converge, the nodes use gossip to detect changes and identify new members in group.. Note: The node name should be unique in the quorum. There must be minimum 3 nodes. You should be able to see all 3 nodes in the UI. You can try shutting down the primary node and see if data is still accessible. A node group identifies which node is the owner of the data and forwards request to the owner to retrieve that.

You can also add a node to a group after its started, you can do this by invoking a rest call (JoinPeerRequest). For now we will use the option of adding node to node group at startup time by providing --peerNodes. Such an ability to join a node group will be very useful in IOT based devices.

1curl -X GET -H 'Content-Type: application/json' -i http://localhost:8001/myservice/books/e948dec6a69bdd3f57182b45a6740

Clicking on the http://localhost:8000/core/ui/default/#/main/service/id__myservice_books should show up more details on the record. You can edit/delete records from the UI as well.

You can also query for your data in the query tab.

References

Xenon

Xenon Github

comments powered by Disqus