-
Notifications
You must be signed in to change notification settings - Fork 0
Example Service Tutorial
This simple tutorial describes a very simple service, and its factory (they way to create new instances). Use it as a starting point in creating your own service. It also describes how to start the default Xenon host, or multiple hosts that synchronize with each other.
Please refer to the programming model page for an overview of the Xenon API surface and model.
Please refer to the debugging and interaction page for more details on starting Xenon, including starting multiple, replicated hosts.
Please refer to the custom host and service creation page if you want to create and host custom services.
Please refer to the developer guide for instructions on how to build, pre reqs, etc
Start the example host, which listens on port 8000 (in the dcp-host directory):
$ java -jar dcp-host/target/dcp-host-*-with-dependencies.jar
Note that Xenon is packaged also a container (about 70MB, which includes JR8), that starts the default dcp-host.jar. Load it (replace the XXYZ with a build number, like 1780):
$ curl <Xenon JOB URL>/dcp-jenkins:XXYZ.tgz | gzip -d | docker load
Run it:
$ docker run -t -i --net=host dcp:jenkins-XXYZ bin/run.sh --bindAddress=0.0.0.0
A client can create new instances of a micro service through another service, the service factory. A service factory mostly relies on framework for doing all the work and has the following properties:
- it is stateless - A factory relies 100% on the document index or service host for keeping track of any child service instances. The lifecycle of a child is controlled by DELETE requests directly to the child.
- GET requests are queries - The factory simply forwards a GET to its URI, to the document index, which returns all documents that their selfLink is prefixed by the factory URI
Below is a minimal, but still useful, factory:
public class ExampleFactoryService extends FactoryService {
public static String SELF_LINK = ServiceUriPaths.CORE + "/examples";
public ExampleFactoryService() {
super(ExampleServiceState.class);
}
@Override
public Service createServiceInstance() throws Throwable {
return new ExampleService();
}
}
Each service comes with a set of Xenon provided utility services, listening under the service/* suffix. Use them to gather stats, subscribe, or interact with your service through a browser
The factory service does not need to implement any handlers, if no additional logic or validation is required for processing POST requests. The FactoryService super class takes care of POST handling, and also GET. What it does need to do however is know how to construct the service object. Every factory service must implement the createServiceInstance method. The child service URI will be the composite of the factory URI (the prefix) and whatever selfLink path was supplied in the POST body. If no body was supplied, a random UUID will be used for the child.
If the service instance state must survive host restarts, the child service instance must enable the ServiceOption.PERSISTENCE in its constructor. The runtime will then make sure every state change, including service creation is indexed on disk, and on restart, it will automatically re-create all child services, per factory and set the version numbers to the latest known version per child service instance
Assuming the service host is running locally (see the developer guide and debugging page), first do a GET on the example factory, to verify no example service instances are created:
$ curl http://localhost:8000/core/examples
Alternatively you can use the Xenon cli dcpc
$ export Xenon=http://localhost:8000
$ dcpc get /core/examples
The factory responds with:
{
"documentLinks": [],
"documentVersion": 0,
"documentUpdateTimeMicros": 0,
"documentExpirationTimeMicros": 0,
"documentOwner": "168c9af7-193d-4b6c-92c5-409df5b27752"
}
The response above means no documents / services are created yet.
Create a new instance by issuing a POST. If no body is supplied the child service will have a default initial state and a random selflink. See multi node deployment for the complete example.
$ curl -X POST -H "Content-type: application/json" -d '{"documentSelfLink":"niki","name":"n"}' http://localhost:8000/core/examples
Same operation, using dcpc
$ dcpc post /core/examples --documentSelfLink=niki --name=n
The example factory will respond with the initial state of the new service:
{
"keyValues": {},
"name": "n",
"documentVersion": 0,
"documentEpoch": 0,
"documentKind": "com:vmware:dcp:services:common:ExampleService:ExampleServiceState",
"documentSelfLink": "/core/examples/niki",
"documentSignature": "fdaccd2541efc842e42fb9693be00dbc731dea07",
"documentUpdateTimeMicros": 1432745225186007,
"documentExpirationTimeMicros": 0,
"documentOwner": "31716c5e-393b-4ca7-a15c-af02af0d6d5e"
}
Doing another GET on the factory URI shows the new child service
$ curl http://localhost:8000/core/examples
{
"documentLinks": [
"/core/examples/niki"
],
"documentVersion": 0,
"documentUpdateTimeMicros": 0,
"documentExpirationTimeMicros": 0,
"documentOwner": "31716c5e-393b-4ca7-a15c-af02af0d6d5e"
}
The super class handles GET requests to the factory URI and converts them to a document index query. The client can add the expand URI parameter to request embedding the child service state as part of the result
A service is called a singleton if its has a fixed URI and only one instance can exist per host. Otherwise, it just a child service, where many instances can co-exist under some shared URI prefix.
The example below is of a minimal service that represents a specific document (the ExampleServiceState PODO). The service implements a PUT and a PATCH handler for processing complete or partial updates to its state. Each time the state updates the framework indexes the new state, increments the version, content signature and timestamp. It also sends notifications, replicates if the service is replicated, etc
public class ExampleService extends StatefulService {
public static class ExampleServiceState extends ServiceDocument {
public static final String FIELD_NAME_KEY_VALUES = "keyValues";
public Map<String, String> keyValues = new HashMap<>();
public Long counter;
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public String name;
}
public ExampleService() {
super(ExampleServiceState.class);
super.toggleOption(ServiceOption.PERSISTENCE, true);
super.toggleOption(ServiceOption.REPLICATION, true);
super.toggleOption(ServiceOption.INSTRUMENTATION, true);
super.toggleOption(ServiceOption.OWNER_SELECTION, true);
}
@Override
public void handleStart(Operation startPost) {
if (startPost.hasBody()) {
ExampleServiceState s = startPost.getBody(ExampleServiceState.class);
logFine("Initial name is %s", s.name);
}
startPost.complete();
}
@Override
public void handlePut(Operation put) {
ExampleServiceState newState = put.getBody(ExampleServiceState.class);
ExampleServiceState currentState = super.getState(put);
// example of structural validation: check if the new state is acceptable
if (currentState.name != null && newState.name == null) {
put.fail(new IllegalArgumentException("name must be set"));
return;
}
updateCounter(newState, currentState, false);
// replace current state, with the body of the request, in one step
super.setState(put, newState);
put.complete();
}
@Override
public void handlePatch(Operation patch) {
ExampleServiceState updateState = updateState(patch);
if (updateState == null) {
return;
}
// updateState method already set the response body with the merged state
patch.complete();
}
private ExampleServiceState updateState(Operation update) {
// A Xenon service handler is state-less: Everything it needs is provided as part of the
// of the operation. The body and latest state associated with the service are retrieved
// below.
ExampleServiceState body = update.getBody(ExampleServiceState.class);
ExampleServiceState currentState = getState(update);
boolean hasStateChanged = Utils.mergeWithState(getDocumentTemplate().documentDescription,
currentState, body);
// update state
updateCounter(body, currentState, hasStateChanged);
if (body.keyValues != null && !body.keyValues.isEmpty()) {
for (Entry<String, String> e : body.keyValues.entrySet()) {
currentState.keyValues.put(e.getKey(), e.getValue());
}
}
if (body.documentExpirationTimeMicros != 0) {
currentState.documentExpirationTimeMicros = body.documentExpirationTimeMicros;
}
// response has latest, updated state
update.setBody(currentState);
return currentState;
}
private boolean updateCounter(ExampleServiceState body,
ExampleServiceState currentState, boolean hasStateChanged) {
if (body.counter != null) {
if (currentState.counter == null) {
currentState.counter = body.counter;
}
// deal with possible operation re-ordering by simply always
// moving the counter up
currentState.counter = Math.max(body.counter, currentState.counter);
body.counter = currentState.counter;
hasStateChanged = true;
}
return hasStateChanged;
}
/**
* Provides a default instance of the service state and allows service author to specify
* indexing and usage options, per service document property
*/
@Override
public ServiceDocument getDocumentTemplate() {
ServiceDocument template = super.getDocumentTemplate();
PropertyDescription pd = template.documentDescription.propertyDescriptions.get(
ExampleServiceState.FIELD_NAME_KEY_VALUES);
// instruct the index to deeply index the map
pd.indexingOptions.add(PropertyIndexingOption.EXPAND);
PropertyDescription pdName = template.documentDescription.propertyDescriptions.get(
ExampleServiceState.FIELD_NAME_NAME);
// instruct the index to enable SORT on this field.
pdName.indexingOptions.add(PropertyIndexingOption.SORT);
// instruct the index to only keep the most recent N versions
template.documentDescription.versionRetentionLimit = ExampleServiceState.VERSION_RETENTION_LIMIT;
return template;
}
}
For an explanation of the options a service can declare, please see the programming model page.
The PATCH handler above will use the body associated with the request (patch.getBody()) and the latest state of the service, associated with the patch (getState(patch)). Using fields in the patch body, it will update the current state and the complete the operation.
The service handlers are 100% asynchronous. This means that the handler method can exit, and the client will not see the operation complete. An operation is only completed when the service code calls
operation.complete();
The service handler method can issue an asynchronous request to another service, return from the method, and only one the secondary request completes, complete its operation.
Using curl once again, create a minimal JSON body with a name property and send it to the child service. Use the URI returned from the POST when creating the service, in the documentSelfLink field, or simply do a GET on the factory to get a list of example service instances
$ curl -X PATCH -H "Content-type: application/json" -d '{"name":"george"}' http://localhost:8000/core/examples/94639609-e989-4ffd-ade6-0a5f2841a421
Service responds with:
{
"keyValues": {},
"name": "george",
"documentVersion": 1,
"documentKind": "com:vmware:dcp:services:common:ExampleService:ExampleServiceState",
"documentSelfLink": "/core/examples/755c582b-eef4-4e96-8450-09e7227355af",
"documentUpdateTimeMicros": 1411077167181000
}
The response has the new state, but you can also do a GET on the child service, to confirm the PATCH took effect. Notice the version is now 1.
Xenon tracks per service instance, per operation statistics so you can determine latency, throughput, aid with debugging of your handlers. Please refer to the REST API page