/xenon-archetype

maven archetype project for xenon

Primary LanguageJavaApache License 2.0Apache-2.0

Overview

Xenon is a framework for writing small REST-based services. (Some people call them microservices.) The runtime is implemented in Java and acts as the host for the lightweight, asynchronous services. The programming model is language agnostic (does not rely on Java specific constructs) so implementations in other languages are encouraged. The services can run on a set of distributed nodes. Xenon provides replication, synchronization, ordering, and consistency for the state of the services. Because of the distributed nature of Xenon, the services scale well and are highly available.

Xenon is a "batteries included" framework. Unlike some frameworks that provide just consistent data replication or just a microservice framework, Xenon provides both. Xenon services have REST-based APIs and are backed by a consistent, replicated document store.

Each service has less than 500 bytes of overhead and can be paused/resumed, making Xenon able to host millions of service instances even on a memory constrained environment.

Service authors annotate their services with various service options, acting as requirements on the runtime, and the framework implements the appropriate algorithms to enforce them. The runtime exposes each service with a URI and provides utility services per instance, for stats, reflection, subscriptions and configuration. A built-in load balancer routes client requests between nodes, according to service options and plug-able node selection algorithms. Xenon supports multiple, independent node groups, maintaining node group state using a scalable gossip scheme.

A powerful index service, invoked as part of the I/O pipeline for persisted services, provides a multi version document store with a rich query language.

High availability and scale-out is enabled through the use of a consensus and replication algorithm and is also integrated in the I/O processing.

What can Xenon be used for?

The lightweight runtime enables the creation of highly available and scalable applications in the form of cooperating light weight services. The operation model for a cluster of Xenon nodes is the same for both on premise, and service deployments.

The photon controller project makes heavy use of Xenon to build a scalable and highly available Infrastructure-as-a-Service fabric, composed of stateful services, responsible for configuration (desired state), work flows (finite state machine tasks), grooming and scheduling logic. Xenon is also used by several teams building new products, services and features, within VMware.

Learning More

For a hands-on walkthrough of the sample code contained in this project, keep reading!

For more technical details including tutorials, please refer to the wiki.

Reporting Issues

Xenon uses a public pivotal tracker project for tracking and reporting issues: https://www.pivotaltracker.com/n/projects/1471320

Quickstart

This is the sample project for a Xenon Host and associated services. Here, we'll walk through getting the sample up and running and then provide some background reading for you to increase your understanding of what Xenon is and how it works.

The service contained here is the sample service from the Xenon repository. We'll walk you through getting that project up and then help you customize and build your first Xenon application. Later, this guide will provide some useful pointers to additional information and key information that will help you manage and debug your Xenon application.

There are multiple definitions of "services", so let's define what we mean by a Xenon service. (For now, we’ll assume we’re talking about Xenon running on a single host--we'll get to using multiple hosts in a moment.)

A single Xenon service implements a REST-based API for a single URI endpoint. For example, you might have a service running on:

https://myhost.example.com/example/service You have a lot of choices in how you implement the service, but a few things are true:

The service can support any of the standard REST operations: GET, POST, PUT, PATCH, DELETE

This service has a document associated with it. If you do a GET on the service, you’ll see the document. In most cases, the document is represented in JSON. There are a few standard JSON fields that every document has, such as the the "kind" of document, the version, the time it was last updated, etc.

Your service may have business logic associated with it. Perhaps when you do a POST, it creates a new VM, and a PATCH will modify the VM.

A service may have its state persisted on disk (a "stateful" service) or it may be generated on the fly (a “stateless” service). Persisted services can have an optional expiration time, after which they are removed from the datastore.

All services can communicate with all other services by using the same APIs that a client will use. Within a single host, communication is optimized (no need to use the network). API calls from clients or services are nearly identical and treated the same way. This makes for a very consistent communication pattern.

For stateful services, only one modification may happen at a time: modifications to a statefule service are serialized, while reads from a service can be made in parallel. If two modifications are attempted in parallel, there will be a conflict. One will succeed, and the other will receive an error. (Xenon is flexible, and allows you to disable this if you really want to.)

Building the Sample

Build the sample application with the following commands issued from your project root directory:

$ mvn clean

Starting the Xenon host

The sample host will listen on port 8000 by default; Let's start the sample host:

$ java -jar xenon-host/target/xenon-host-*-with-dependencies.jar

Sample Code Walkthrough

Now lets walk through the sample code and see what is happening underneath the hood.

The service factory

A client can create new instances of a service through another service, the service factory. A service factory mostly relies on the Xenon 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

Creating a factory

A developer can either derive from the FactoryService abstract class, or use a static helper to create a default instance: Below is a minimal, but still useful, factory:

    /**
     * Create a default factory service that starts instances of this service on POST.
     * This method is optional, {@code FactoryService.create} can be used directly
     */
    public static FactoryService createFactory() {
        return FactoryService.createIdempotent(ExampleService.class);
    }

Since factory code gives limited ability to change POST or GET processing, deriving from the factory class is not recommended. In particular, avoid any state side effects across services in replicated factory handlePost methods. Most of the logic that deals with service creation happens on POST completion, and on the owner node, so you will be violating key invariants.

Starting a Factory service

The factory service is a singleton that should be started on host start:

    @Override
    public ServiceHost start() throws Throwable {
        super.start();

        startDefaultCoreServicesSynchronously();

        setAuthorizationContext(this.getSystemAuthorizationContext());

        // Start the example service factory
        super.startFactory(ExampleService.class, ExampleService::createFactory);

       ...
       ...

Per service Utility URIs

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

POST Handling

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. 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.

Durability

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 xenonc

$ export XENON=http://localhost:8000
$ xenonc 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.

POST Example

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.

$ curl -X POST -H "Content-type: application/json" -d '{"documentSelfLink":"niki","name":"n"}' http://localhost:8000/core/examples

Same operation, using xenonc

$ xenonc 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:xenon: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"
}

GET Handling

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

The child or singleton service

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.

PATCH handler

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.

CURL PATCH Example

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:xenon: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.

Statistics

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

Example Service Code

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 final String FACTORY_LINK = ServiceUriPaths.CORE + "/examples";

    /**
     * Create a default factory service that starts instances of this service on POST.
     * This method is optional, {@code FactoryService.create} can be used directly
     */
    public static FactoryService createFactory() {
        return FactoryService.createIdempotent(ExampleService.class, ExampleServiceState.class);
    }

    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 = getBody(startPost);
            logFine("Initial name is %s", s.name);
        }
        startPost.complete();
    }

    @Override
    public void handlePut(Operation put) {
        ExampleServiceState newState = getBody(put);
        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 = getBody(update);
        ExampleServiceState currentState = getState(update);
        boolean hasStateChanged = Utils.mergeWithState(getStateDescription(),
                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.

Additional Reading