GrainContext API
Action Handler API
All Action Handlers are executed within the context of a Grain, allowing for quick access to a Grain's state. As a result, Action Handlers provide access to a GrainContext
object which can be used to perform various actions like logging messages, getting counters/gauges, accessing the Grain's state, etc.
Types of Action Handlers
Action Handler methods may be per-action, or batch-oriented. The following example is that of a per-action Action Handler method.
Batch-oriented Action Handlers methods are useful when each action requires a heavy or expensive operation. For example, if an Action Handler initializes a heavy/expensive object, to analyze data, a batch-oriented Action Handler can be advantageous. Instead of initializing that object for each action individually, a batch-oriented Action Handler allows developers to initialize it just once for entire batches of actions. The following example shows a batch-oriented handler.
Accessing Grain State
Accessing the Grain's state within an Action Handler is as easy as invoking getKey()
, getValue()
or mapGet()
on the provided GrainContext
object. The following example accesses and modifies the Grain's value and a map entry for the "history" map (with map ID = 0).
context.mapQuery(MapQuery)
(Java) and context.map_get_range(Map ID, StartKey, EndKey)
(Python) also allow scanning for data in a map, similar to how they are used in GrainiteClient API.
In the Java GrainContext API, maps can be accessed by both name (eg: "history") and id (eg: 0). In the Python GrainContext API, maps can be accessed by id only.
Logger
The logger provided by GrainContext
via getLogger()
should be used for application logs. These logs can then be viewed by running tail -f server
in the server's meta directory.
Using standard printing methods (like System.out.println
in Java or print
in Python) to log information is not recommended as the output won't show up as expected in the server log.
The following example uses the logger for logging application information.
Sending Messages to Grains
Action Handlers also have the ability to send messages to other Grains using sendToGrain(TableName, Key, Operation, ExpectGrainResponse)
. This method takes a TableName
and a Key
of the receiving Grain, an Operation
which refers to the type of operation to perform on the grain, and an optional ExpectResponse
signaling whether a response is expected back for the operation. (Java, Python)
Some of the supported operations are:
GrainOp.Invoke
- invokes an Action Handler for the receiving Grain.GrainOp.Put
- inserts the specified value in receiving Grain's value state.GrainOp.MapPut
- inserts the specified value in the specified map of the receiving Grain.
The following example invokes an Action Handler called handleOrders
, with the payload "Burger", for #123
Grain in the orders_table
using GrainOp.Invoke
.
To send messages to Grains in another application, simply pass the app name to sendToGrain()
.
Note that a Grain key can be up to 4KB in size and the value can be up to 512KB in size. The same limits apply to map keys (4KB maximum) and map values (512KB maximum) Future updates to Grainite will increase these limits.
Sending Events to Topics
Similar to sendToGrain
, Action Handlers also have the ability to push events to topics using sendToTopic(TopicName, Key, Event, ExpectResponse)
. This method takes a TopicName
, a Key
, an Event
for the payload, and an optional ExpectResponse
signaling whether a response is expected back for the operation.
The following example sends an Event comprising of the key - #123
, and the payload "Burger", to orders_topic
.
Note that an event key can be up to 4KB in size and the payload can be up to 512KB in size. Future updates to Grainite will increase these limits.
Looking up Grains
It is common for the need to access other Grains' states from within an Action Handler. This is made easy using lookupGrain(AppName, TableName, Key)
which takes parameters for the location of the Grain. This method synchronously returns a GrainView
which can be used to look up the Grain's state but can not be used to invoke handlers or change its state. To change another Grain's state, use sendToGrain
.
The following example accesses another Grain's state in an Action Handler.
Building Secondary Indexes with Grainite
As an early access capability, we have introduced the ability to create indexes that are strongly consistent with the data being updated in tables. Indexes created as part of this feature are updated at the same time as the data values are persisted in the Grainite database.
When to use Secondary Indexes
Grains within Grainite tables may be looked up using their key - using the Table.getGrain() API. Secondary indexes allow looking up Grains using other properties of the data.
Let us consider that Grains in a table contain a 'city'
and 'zipcode'
property that's saved in the value (or in one of the maps). Grain 'A'
has city = 'Los Angeles'
and zipcode = '90001'
, while Grain 'B'
has city = 'Santa Clara'
and zipcode = '95054'.
By indexing these properties, one can then search for all Grains in the table with zipcode = '90001'
(resulting in Grain 'A'
) or zipcode > '90000'
(fetching both Grain 'A'
and Grain 'B'
). See Querying Secondary Indexes in the GrainiteClient API.
How to create and delete indexes
Indexes may be created and updated using the GrainContext APIs available to the Action Handlers. As part of updating the data within the Grain, one may invoke the GrainContext.insertIndexEntry()
and GrainContext.updateIndexEntry()
APIs to add a new entry or to update a previous index entry. One may invoke this API multiple times during a single invocation of an Action Handler to record multiple index entries for the Grain.
Additionally, one may use GrainContext.deleteIndexEntry()
to delete a previously created index.
This API was introduced in 2326.1.
The context.indexQuery()
API allows for querying secondary indexes within an Action Handler.
This is the GrainContext
equivalent of the client API - table.find()
.This API has similar usage and semantics to the client-side API:
One difference is that the client API returns Iterator<Grain>
, while the context API returns Iterator<GrainView>
- a read-only state of the corresponding Grains.
For optimal performance, we strongly advise against using conjunctions such as AND
or OR
in the queryIndex API.
Using Counters and Gauges
Grainite makes it convenient to create counters and gauges that can be used by Prometheus out-of-the-box via counter(MetricName, Params)
and gauge(MetricName, Params)
. Both of these methods take a MetricName
, as well as optional Params
, which are a key-value pair of String
to store additional data about particular counters. These params are used as labels by Prometheus. See the monitoring section for documentation on how these counters and gauges can be exposed for external consumption.
The following example increments counters to keep track of some metrics.
Exception Handlers
exception handlers
simplify exception handling for Action Handlers. Exception handlers are invoked when an Action Handler throws a specified exception. Exception handlers can be defined in the exception_handler section and then attached to an action in the app config.
An exception handler takes three arguments:
Action
Throwable
GrainContext
Object Pooling
Granite offers an API to reuse objects in Action Handlers. This is beneficial when certain objects are costly to create and are used frequently in Action Handlers.
For instance, consider an Action Handler making frequent HTTP requests to a Webhook. Each time it is called, it initializes an HTTP client, makes the request, then destroys the client. This process is inefficient and it lengthens the execution time for each Action.
Instead, the Handler can use object pooling to share objects, to conserve resources and reduce execution time. This approach is more efficient than creating new objects for each Handler.
There are three parameters that the context.getPooledObject
method takes -
The key for the pool
The allocator to construct a new object
The object type.
The object pool can store multiple pools of object instances, and the key is used to distinguish each pool. Each pool can contain up to 20 instances of an object and if an object instance hasn't been used for 10 minutes, it will be removed from the pool.
Here is an example:
When there is no instance of an object in the pool, the allocate
method is called to build the instance. Similarly, the deallocate
method is called when an object is removed from the pool.
Optionally, context.deallocateObject(Object)
can be called to manually remove an object from the pool. This can be useful in scenarios where the resource is deemed outdated. For example, if the underlying http connection is broken, deallocateObject
can be called on the http client instance.
Last updated