Creating an API Module Project
Use the scaleout-api-module
archetype to create an API Module project.
Background
In this example, we will use the com.scaleoutsoftware.archetypes:scaleout-api-module archetype to create and customize an API module for a simple shopping cart. The API processor in an API module is responsible for persisting, analyzing, and reacting to API module invocations.
Prerequisites
Java 8 or higher.
Maven
Procedure
1. Create API module project
To create an API module Maven project, run the following command in your terminal:
mvn archetype:generate \
-DarchetypeGroupId=com.scaleoutsoftware.archetypes \
-DarchetypeArtifactId=scaleout-api-module \
-DarchetypeVersion=1.0.0 \
-DgroupId=com.mycompany.shopping \
-DartifactId=ShoppingCart \
-DsossObjectName=ShoppingCart \
-DinteractiveMode=false
What These Options Do
Property |
Description |
---|---|
|
Your organization or namespace |
|
Name of the generated project directory + artifact Id |
|
The class representing each ShoppingCart |
This command generates a new folder (ShoppingCart
) containing:
Java class stubs for your module classes
A sample client that performs a hashmap operation on the ShoppingCart object
A sample unit test that reads/writes to the hashmap in the ShoppingCart object
A pre-configured
scaleoutPackage.json
metadata fileA Maven project file (
pom.xml
) with preconfigured dependenciesA build pipeline for packaging your project for deployment
2. Customize state object
Customize the SOSS object used to store data for your module. A skeleton class named “ShoppingCart” was created by the archetype and must be modified to hold state for the module (in this case, a list of items in the shopping cart). Instances of this object will be stored in the ScaleOut StateServer cluster.
public class ShoppingCart { private List<CartItem> cartItemList; // For JSON serialized objects, an Id field is required to enable // queries from the ActiveCaching UI. private String Id; public ShoppingCart(String id) { Id = id; cartItemList = new LinkedList<>(); } public boolean addCartItem(CartItem item) { if(item != null) return cartItemList.add(item); return false; } public boolean removeCartItem(String id) { if(!id.isEmpty()) { Iterator<CartItem> it = cartItemList.iterator(); while (it.hasNext()) { if (it.next().getId().compareTo(id) == 0) { it.remove(); return true; } } return false; } return false; } public double getTotalPrice() { double runningTotal = 0.0d; for(CartItem item : cartItemList) { runningTotal+=item.getPrice()*item.getQuantity(); } return runningTotal; } }
SOSS objects should also contain a string property named
Id
. While not strictly required, the Id property is used by the ScaleOut Active Caching UI to identify objects in the cluster for queries.
3. Implement the CreateObject() method
Implement the CreateObject method in your API Processor. A skeleton class named “ShoppingCartApiProcessor” was precreated by the archetype and must be modified with a concrete CreateObject() implementation. This method is called when an API invocation is received and the associated SOSS object does not yet exist.
@Override public CreateResult<ShoppingCart> createObject(String moduleName, String id) { return new CreateResult<ShoppingCart>() { @Override public ShoppingCart getValue() { return new ShoppingCart(id); } }; }
The CreateResult instance returned by the createObject() method will have the following three properties set:
Value
: The newly created SOSS object (in this case, a shopping cart whose Items property is an empty list).
Expiration
: A Duration indicating how long the object should be kept in the cluster before being automatically removed. By default, Duration.ofSeconds(0) – infinite timeout.
ExpirationType
: An enum value indicating the expiration behavior for the object. Set to Sliding to reset the expiration every time the SossObject is accessed, or Absolute to keep it for a fixed duration. By default, ExpirationType.Sliding.
4. Implement the API module’s endpoints
The API module’s endpoints are implemented in the ShoppingCartApiProcessor class, decorated with @SossApiMethod annotations
. This class contains methods for handling incoming API requests and interacting with the SOSS objects.
Endpoints accept an arbitrary argument value as a byte array and can return a byte array as a response.
@SossApiMethod(operationId = "addCartItem", useLocking = true) public InvokeResult addItemHandler(ApiProcessingContext<ShoppingCart> processingContext, ShoppingCart shoppingCart, byte[] payload) { Gson gson = new Gson(); CartItem item = gson.fromJson(new String(payload, StandardCharsets.UTF_8), CartItem.class); boolean added = shoppingCart.addCartItem(item) ; return new InvokeResult() { @Override public byte[] getResult() { return new byte[] { (byte)(added ? 1 : 0)}; } @Override public ProcessingResult getProcessingResult() { return ProcessingResult.DoUpdate; } }; } @SossApiMethod(operationId = "removeCartItem", useLocking = true) public InvokeResult removeItemHandler(ApiProcessingContext<ShoppingCart> processingContext, ShoppingCart shoppingCart, byte[] payload) { String itemId = new String(payload, StandardCharsets.UTF_8); boolean removed = shoppingCart.removeCartItem(itemId); return new InvokeResult() { @Override public byte[] getResult() { return new byte[] { (byte)(removed ? 1 : 0)}; } @Override public ProcessingResult getProcessingResult() { return ProcessingResult.DoUpdate; } }; } @SossApiMethod(operationId = "getCartTotalPrice", useLocking = false) public InvokeResult getTotalHandler(ApiProcessingContext<ShoppingCart> processingContext, ShoppingCart shoppingCart, byte[] payload) { // payload is expected to be null, ignore it. double cartTotalPrice = shoppingCart.getTotalPrice(); return new InvokeResult() { @Override public byte[] getResult() { return ByteBuffer.allocate(Double.BYTES) .putDouble(cartTotalPrice) .array(); } @Override public ProcessingResult getProcessingResult() { return ProcessingResult.DoUpdate; } }; }
Endpoints must be marked with the annotation
@SossApiMethod
, indicating the string name of the API method and its locking behavior.Use useLocking=true in the @SossApiMethod annotation if you need to ensure that only one request can modify the SOSS object at a time.
API endpoints must return an InvokeResult object. The following methods should be implemented to return concrete values from the invocation:
getResult
: The byte array to return to the caller (if any).getProcessingResult
: Indicates what should be done with the object in the ScaleOut service after the API method has returned:DoUpdate: Indicates that the object was (or may have been) modified and must be updated in the ScaleOut service.
NoUpdate: Indicates the object was not modified and does not need to be updated in the ScaleOut service. (If you are unsure of whether the object was modified, always return DoUpdate.)
Remove: Remove the associated SossObject from the ScaleOut cluster.
5. Invoke the API module’s endpoints
Clients should extend the ApiModuleClient
base class to call invoke directly or instantiate a default ApiModuleClient and call invoke on that class instance to execute one of the API modules endpoints.
The ExampleClient
demonstrates extending the ApiModuleClient and invoking the addItem
, removeItem
, and getTotalPrice
endpoints.
public class ExampleClient extends ApiModuleClient { public ExampleClient(GridConnection connection, String moduleName) { super(connection, moduleName); } public boolean addCartItem(String cartId, CartItem item) throws ApiModuleException { Gson gson = new Gson(); byte[] serializedCartItem = gson.toJson(item, CartItem.class).getBytes(StandardCharsets.UTF_8); byte[] ret = invoke(cartId, "addCartItem", serializedCartItem); return ret[0] == 0x01; } public boolean removeCartItem(String cartId, String itemId) throws ApiModuleException { Gson gson = new Gson(); byte[] serializedId = itemId.getBytes(StandardCharsets.UTF_8); byte[] ret = invoke(cartId, "removeCartItem", serializedId); return ret[0] == 0x01; } public double getCartTotalPrice(String cartId) throws ApiModuleException { byte[] ret = invoke(cartId, "getCartTotalPrice", null); return ByteBuffer.wrap(ret).getDouble(); } }
To invoke an endpoint, supply the endpoints name, the target Id, and the arbitrary byte[] payload.
The invoke should handle exceptional behavior – an API module exception is thrown when:
The invoke request times out
The invoke handler throws an exception (the causal exception will be provided inside the API module exception)
The invoke handler threw an exception while creating the object.
The invoke handler did not exist.
6. Testing the API module
Developers can test their module in a local development environment. The project’s generated unit test demonstrates how to create and run the ModulePackage
locally and how to create a local development ApiClient
for invoking the API module’s endpoints.
The TestModule
class demonstrates unit tests invoking the addItem
, removeItem
, and getTotalPrice
endpoints.
// instantiate the module package ModulePackage modulePackage = new ModulePackage(); // define the ApiModuleOptions ApiModuleOptions<ShoppingCart> apiModuleOptions = new ApiModuleOptionsBuilder<ShoppingCart>(ShoppingCart.class).build(); // add the API module to the package modulePackage.addApiModule("ShoppingCart", new ShoppingCartApiProcessor(), apiModuleOptions); // run a local development package modulePackage.runLocalDevelopmentEnvironment(); // instantiate the example client String shoppingCartId = "ShoppingCart"+UUID.randomUUID().toString(); String cartItemId = "ItemId"+UUID.randomUUID().toString(); int cartItemQuantity = 15; double cartItemPrice = 17.99; double expectedTotal = cartItemQuantity * cartItemPrice; ExampleClient exampleClient = new ExampleClient(GridConnection.connect(Constants.DEVELOPMENT_CONNECTION_STRING), "ShoppingCart"); CartItem newCartItem = new CartItem(cartItemId, cartItemQuantity, cartItemPrice); // add the cart item Assert.assertTrue(exampleClient.addCartItem(shoppingCartId, newCartItem)); // check the cart total price Assert.assertEquals(expectedTotal, exampleClient.getCartTotalPrice(shoppingCartId), 1e-9); // remove the cart item Assert.assertTrue(exampleClient.removeCartItem(shoppingCartId, cartItemId)); // check the cart total price Assert.assertEquals(0.0, exampleClient.getCartTotalPrice(shoppingCartId), 1e-9);
To invoke an endpoint, supply the endpoint’s name, the target Id, and the arbitrary byte[] payload.
The invoke should handle exceptional behavior – an API module exception is thrown when:
The invoke request times out
The invoke handler throws an exception (the causal exception will be provided inside the API module exception)
The invoke handler threw an exception while creating the object.
The invoke handler did not exist.
7. State Object Serialization
In Java, by default the SOSS state object is serialized using JSON serialization with the GSON serialization library.
To use a custom serializer, create an implementation of the CacheSerializer
and CacheDeserializer
. The following code snippets demonstrate using the Kryo
serialization protocol for the Flight
state object.
public class KryoSerializer extends CacheSerializer<ShoppingCart> {
private final Kryo kryo;
public KryoSerializer() {
kryo = new Kryo();
kryo.register(ShoppingCart.class);
}
@Override
public byte[] serialize(ShoppingCart flight) throws SerializationException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (Output output = new Output(bos)) {
kryo.writeObject(output, flight);
}
return bos.toByteArray();
}
}
public class KryoDeserializer extends CacheDeserializer<Flight> {
private final Kryo kryo;
public KryoDeserializer() {
kryo = new Kryo();
kryo.register(Flight.class);
}
@Override
public Flight deserialize(byte[] bytes) throws DeserializationException {
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
try (Input input = new Input(bis)) {
return kryo.readObject(input, Flight.class);
}
}
}
Configure the ModuleOptions
to use the newly defined custom serializer classes by instantiating the KryoSerializer
and KryoDeserializer
and setting them on the ModuleOptionsBuilder
. Then, associate the ModuleOptions
with the ModulePackage
when adding the Flight
module.
ApiModuleOptions<ShoppingCart> apiModuleOptions = new ApiModuleOptionsBuilder<ShoppingCart>(ShoppingCart.class)
.setSerialization(new KryoSerializer(), new KryoDeserializer(), false)
.build();
// add the API module to the package
modulePackage.addApiModule("ShoppingCart", new ShoppingCartApiProcessor(), apiModuleOptions);