Services | Ignite Documentation
Edit

Services

Overview

A service is a piece of functionality that can be deployed to an Ignite cluster and execute specific operations. You can have multiple instances of a service on one or multiple nodes.

Ignite services have the following features:

Load balancing

In all cases, other than singleton service deployment, Ignite automatically makes sure that about an equal number of services are deployed on each node within the cluster. Whenever the cluster topology changes, Ignite re-evaluates service deployments and may re-deploy an already deployed service to another node for better load balancing.

Fault tolerance

Ignite always guarantees that services are continuously available, and are deployed according to the specified configuration, regardless of any topology changes or node crashes.

Hot Redeployment

You can use Ignite’s DeploymentSpi configuration to re-deploy services without restarting the cluster. See Re-deploying Services.

Ignite services can be used as a backbone of a micro-services based solution or application. Learn more about this use case from the following series of articles:

Refer to a service example implementation in the Apache Ignite code base.

Implementing a Service

A service implements the Service interface. The Service interface has three methods:

  • init(): this method is called by Ignite before the service is deployed (and before the execute() method is called)

  • execute(): starts execution of the service

  • cancel(): cancels service execution

Deploying Services

You can deploy your service either programmatically at runtime, or by providing a service configuration as part of the node configuration. In the latter case, the service is deployed when the cluster starts.

Deploying Services at Runtime

You can deploy services at runtime via the instance of IgniteServices, which can be obtained from an instance of Ignite by calling the Ignite.services() method.

The IgniteServices interface has a number of methods for deploying services:

  • deploy(ServiceConfiguration) deploys a service defined by a given configuration.

  • deployNodeSingleton(…​) ensures that an instance of the service is running on each server node.

  • deployClusterSingleton(…​) deploys a single instance of the service per cluster. If the cluster node on which the service is deployed stops, Ignite automatically redeploys the service on another node.

  • deployKeyAffinitySingleton(…​) deploys a single instance of the service on the primary node for a given cache key.

  • deployMultiple(…​) deploys the given number of instances of the service.

This is an example of cluster singleton deployment:

Ignite ignite = Ignition.start();

//get the services interface associated with all server nodes
IgniteServices services = ignite.services();

//start a node singleton
services.deployClusterSingleton("myCounterService", new MyCounterServiceImpl());

And here is how to deploy a cluster singleton using ServiceConfiguration:

Ignite ignite = Ignition.start();

ServiceConfiguration serviceCfg = new ServiceConfiguration();

serviceCfg.setName("myCounterService");
serviceCfg.setMaxPerNodeCount(1);
serviceCfg.setTotalCount(1);
serviceCfg.setService(new MyCounterServiceImpl());

ignite.services().deploy(serviceCfg);

Deploying Services at Node Startup

You can specify your service as part of the node configuration and start the service together with the node. If your service is a node singleton, the service is started on each node of the cluster. If the service is a cluster singleton, it is started in the first cluster node, and is redeployed to one of the other nodes if the first node terminates. The service must be available on the classpath of each node.

Below is an example of configuring a cluster singleton service:

<bean class="org.apache.ignite.configuration.IgniteConfiguration">

    <property name="serviceConfiguration">
        <list>
            <bean class="org.apache.ignite.services.ServiceConfiguration">
                <property name="name" value="myCounterService"/>
                <property name="maxPerNodeCount" value="1"/>
                <property name="totalCount" value="1"/>
                <property name="service">
                    <bean class="org.apache.ignite.snippets.services.MyCounterServiceImpl"/>
                </property>
            </bean>
        </list>
    </property>

</bean>
ServiceConfiguration serviceCfg = new ServiceConfiguration();

serviceCfg.setName("myCounterService");
serviceCfg.setMaxPerNodeCount(1);
serviceCfg.setTotalCount(1);
serviceCfg.setService(new MyCounterServiceImpl());

IgniteConfiguration igniteCfg = new IgniteConfiguration()
        .setServiceConfiguration(serviceCfg);

// Start the node.
Ignite ignite = Ignition.start(igniteCfg);

Deploying to a Subset of Nodes

When you obtain the IgniteServices interface by calling ignite.services(), the IgniteServices instance is associated with all server nodes. It means that Ignite chooses where to deploy the service from the set of all server nodes. You can change the set of nodes considered for service deployment by using various approaches describe below.

Cluster Singleton

A cluster singleton is a deployment strategy where there is only one instance of the service in the cluster, and Ignite guarantees that the instance is always available. In case the cluster node on which the service is deployed crashes or stops, Ignite automatically redeploys the instance to another node.

ClusterGroup

You can use the ClusterGroup interface to deploy services to a subset of nodes. If the service is a node singleton, the service is deployed on all nodes from the subset. If the service is a cluster singleton, it is deployed on one of the nodes from the subset.

Ignite ignite = Ignition.start();

//deploy the service to the nodes that host the cache named "myCache"
ignite.services(ignite.cluster().forCacheNodes("myCache"));

Node Filter

You can use node attributes to define the subset of nodes meant for service deployment. This is achieved by using a node filter. A node filter is an IgnitePredicate<ClusterNode> that Ignite calls for each node associated with the IgniteService interface. If the predicate returns true for a given node, the node is included.

Caution
The class of the node filter must be present in the classpath of all nodes.

Here is an example of a node filter. The filter includes the server nodes that have the "west.coast.node" attribute.

public static class ServiceFilter implements IgnitePredicate<ClusterNode> {
    @Override
    public boolean apply(ClusterNode node) {
        // The service will be deployed on the server nodes
        // that have the 'west.coast.node' attribute.
        return !node.isClient() && node.attributes().containsKey("west.coast.node");
    }
}

Deploy the service using the node filter:

Ignite ignite = Ignition.start();

ServiceConfiguration serviceCfg = new ServiceConfiguration();

// Setting service instance to deploy.
serviceCfg.setService(new MyCounterServiceImpl());
serviceCfg.setName("serviceName");
serviceCfg.setMaxPerNodeCount(1);

// Setting the nodes filter.
serviceCfg.setNodeFilter(new ServiceFilter());

// Getting an instance of IgniteService.
IgniteServices services = ignite.services();

// Deploying the service.
services.deploy(serviceCfg);

Cache Key

Affinity-based deployment allows you to deploy a service to the primary node for a specific key in a specific cache. Refer to the Affinity Colocation section for details. For an affinity-base deployment, specify the desired cache and key in the service configuration. The cache does not have to contain the key. The node is determined by the affinity function. If the cluster topology changes in a way that the key is re-assigned to another node, the service is redeployed to that node as well.

Ignite ignite = Ignition.start();

// Making sure the cache exists.
ignite.getOrCreateCache("orgCache");

ServiceConfiguration serviceCfg = new ServiceConfiguration();

// Setting service instance to deploy.
serviceCfg.setService(new MyCounterServiceImpl());

// Setting service name.
serviceCfg.setName("serviceName");
serviceCfg.setTotalCount(1);

// Specifying the cache name and key for the affinity based deployment.
serviceCfg.setCacheName("orgCache");
serviceCfg.setAffinityKey(123);

IgniteServices services = ignite.services();

// Deploying the service.
services.deploy(serviceCfg);

Accessing Services

You can access the service at runtime via a service proxy. Proxies can be either sticky or non-sticky. A sticky proxy always connects to the same cluster node to access a remotely deployed service. A non-sticky proxy load-balances remote service invocations among all cluster nodes on which the service is deployed.

The following code snippet obtains a non-sticky proxy to the service and calls a service method:

//access the service by name
MyCounterService counterService = ignite.services().serviceProxy("myCounterService",
        MyCounterService.class, false); //non-sticky proxy

//call a service method
counterService.increment();

Service Awareness

For Java Thin Client you can activate Service Awareness. To do that, enable Partition Awareness.

Without Service Awareness, the invocation requests are sent to a random node. If it has no service instance deployed, the request is redirected to a different node. This additional network hop adds overhead.

With Service Awareness, the thin client knows where service instances are deployed and sends the request to the correct node.

Note

The service topology is updated asynchronously starting with the first service invocation. Thus, some invocation redirects are still possible.

Un-deploying Services

To undeploy a service, use the IgniteServices.cancel(serviceName) or IgniteServices.cancelAll() methods.

services.cancel("myCounterService");

Re-deploying Services

If you want to update the implementation of a service without stopping the cluster, you can do it if you use the Ignite’s DeploymentSPI configuration.

Use the following procedure to redeploy the service:

  1. Update the JAR file(s) in the location where the service is stored (pointed to by your UriDeploymentSpi.uriList property). Ignite will reload the new classes after the configured update period.

  2. Add the service implementation to the classpass of a client node and start the client.

  3. Call the Ignite.services().cancel() method on the client node to stop the service.

  4. Deploy the service from the client node.

  5. Stop the client node.

In this way, you don’t have to stop the server nodes, so you don’t interrupt the operation of your cluster.

Service Statistics

You can measure durations of your service’s methods. If you want this analytics, enable service statistics in the service configuration. Service statistics are collected under name "Services" in metrics, in system views, and in JMX.

Ignite ignite = Ignition.start();

ServiceConfiguration serviceCfg = new ServiceConfiguration();

serviceCfg.setName("myService");
serviceCfg.setMaxPerNodeCount(1);
serviceCfg.setService(new MyCounterServiceImpl());

// Enable service statistics.
serviceCfg.setStatisticsEnabled(true);

ignite.services().deploy(serviceCfg);

// NOTE: work via proxy. Direct references like 'IgniteServices#service()' corrupt the statistics.
MyCounterService svc = ignite.services().serviceProxy("myService", MyCounterService.class, true);
Note

You should be aware:

  1. Direct references to the services like IgniteServices.service(name) corrupt the statistics. Use the proxies instead (IgniteServices.serviceProxy(…​)).

  2. Overloaded service methods have the same metric by the method name.

  3. Service statistics do not take in account serialization and network issues.

  4. Service statistics slows down the invocations of service methods. It’s not an issue for real jobs like working with a DB or a cache.

Service Middleware

Warning
This feature is experimental and may change in future releases.
Caution

This feature may affect performance of service execution.

Apache Ignite allows users to implement custom middleware logic for their services and provides the following features:

  1. Ability to implicitly pass custom immutable parameters from proxy to service (similar to request headers).

  2. Ability to define custom interceptor for service method calls.

Service Call Context

This feature allows users to implicilty pass parameters to any service method without service re-deployment.

The user can create context and bind it to the service proxy. After that, each call to the proxy method will also implicitly pass context parameters to the service.

try (Ignite ignite = Ignition.start()) {
    ignite.services().deployClusterSingleton("EchoService", new EchoServiceImpl());

    // Create context.
    ServiceCallContext johnCtx = ServiceCallContext.builder().put("user", "John").build();
    ServiceCallContext maryCtx = ServiceCallContext.builder().put("user", "Mary").build();

    // Bind it to the service proxy.
    EchoService johnProxy = ignite.services().serviceProxy("EchoService", EchoService.class, false, johnCtx);
    EchoService maryProxy = ignite.services().serviceProxy("EchoService", EchoService.class, false, maryCtx);

    // Prints "Hello John!".
    System.out.println(johnProxy.hello());

    // Prints "Hello Mary!".
    System.out.println(maryProxy.hello());
}

The user can read the call context using the currentCallContext method of ServiceContext.

Note
Service call context is only accessible from the current thread during the execution of a service method.
@ServiceContextResource
private ServiceContext ctx;

/** {@inheritDoc} */
@Override public String hello() {
    ServiceCallContext callCtx = ctx.currentCallContext();

    String proxyUser = callCtx != null ? callCtx.attribute("user") : null;

    return String.format("Hello %s!", (proxyUser == null || proxyUser.isEmpty() ? "anonymous" : proxyUser));
}

Service Interceptor

This feature allows users to intercept the call to any service method except lifecycle methods (init(), execute() and cancel()).

The user can specify one or more interceptors in the ServiceConfiguration. They are deployed with the service using the same class loader.

Each interceptor invokes the next interceptor in the chain using a delegated call, the last interceptor calls the service method. So, the interceptor specified first in the configuration is the last interceptor processing the result of the service method execution.

invocation order
Caution

An incorrect interceptor implementation can lead to undefined behaviour of the service execution.

public class EchoServiceAccessInterceptor implements ServiceCallInterceptor {
    /** {@inheritDoc} */
    @Override public Object invoke(String mtd, Object[] args, ServiceContext ctx, Callable<Object> next) throws Exception {
        ServiceCallContext callCtx = ctx.currentCallContext();

        if (callCtx == null || callCtx.attribute("user") == null)
            throw new SecurityException("Anonymous access is restricted.");

        return next.call();
    }
}