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.
ServiceOption | Description |
---|---|
PERSISTENCE | persists data |
REPLICATION | replicates data across nodes |
INSTRUMENTATION | provides stats about service |
OWNER_SELECTION | takes ownership of storing data |
PropertyUsageOption Annotation | Description |
---|---|
ID | id field |
AUTO_MERGE_IF_NOT_NULL | helper method will merge current state with state supplied in body in case of updates |
OPTIONAL | not optional |
REQUIRED | mandatory field |
SERVICE_USE | used 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.