Interface MessagingService

  • All Superinterfaces:
    Service

    @DefaultServiceFactory(MessagingServiceFactory.class)
    public interface MessagingService
    extends Service
    « start hereMain entry point to messaging API.

    Overview

    Messaging service provides support for building message-oriented communications in the cluster of Hekate nodes. Message exchange is based on the concept of messaging channels. Such channels hide all the complexities of managing resources (like socket and threads) and provide a high level API for implementing various messaging patterns.

    Messaging Channels

    Messaging channel is a communication unit that can act as a sender, as a receiver or perform both of those roles simultaneously. Channels provide support for unicast messaging (node to node communication) and broadcast messaging (node to many nodes communication). Note that "unicast" and "broadcast" in this context are NOT related to UDP (all communications are TCP-based) and merely outline the communication patterns.

    Configuring Channels

    Configuration of a messaging channel is represented by the MessagingChannelConfig class.

    Instances of this class can be registered via the MessagingServiceFactory.withChannel(MessagingChannelConfig) method.

    Example:

    
    // Configure channel that will support messages of String type (for simplicity).
    MessagingChannelConfig<String> channelCfg = MessagingChannelConfig.of(String.class)
        .withName("example.channel") // Channel name.
        // Message receiver (optional - if not specified then channel will act as a sender only)
        .withReceiver(msg -> {
            System.out.println("Request received: " + msg.payload());
    
            // Reply (if this is a request and not a unidirectional notification).
            if (msg.mustReply()) {
                msg.reply("some response");
            }
        });
    
    // Start node.
    Hekate hekate = new HekateBootstrap()
        // Register channel to the messaging service.
        .withMessaging(messaging ->
            messaging.withChannel(channelCfg)
        )
        .join();
    
    Note: This example requires Spring Framework integration (see HekateSpringBootstrap).
    
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:h="http://www.hekate.io/spring/hekate-core"
        xmlns="http://www.springframework.org/schema/beans"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.hekate.io/spring/hekate-core
            http://www.hekate.io/spring/hekate-core.xsd">
    
        <h:node id="hekate">
            <!-- Messaging service. -->
            <h:messaging>
                <h:channel name="example.channel">
                    <h:receiver>
                        <bean class="foo.bar.SomeMessageReceiver"/>
                    </h:receiver>
    
                    <!-- ...other options... -->
                </h:channel>
            </h:messaging>
    
            <!-- ...other services... -->
        </h:node>
    </beans>
    
    Note: This example requires Spring Framework integration (see HekateSpringBootstrap).
    
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.springframework.org/schema/beans"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="hekate" class="io.hekate.spring.bean.HekateSpringBootstrap">
            <property name="services">
                <list>
                    <!-- Messaging service. -->
                    <bean class="io.hekate.messaging.MessagingServiceFactory">
                        <property name="channels">
                            <list>
                                <bean id="example.channel" class="io.hekate.messaging.MessagingChannelConfig">
                                    <property name="name" value="example.channel"/>
                                    <property name="receiver">
                                        <bean class="foo.bar.SomeMessageReceiver"/>
                                    </property>
    
                                    <!-- ...other options... -->
                                </bean>
                            </list>
                        </property>
                    </bean>
    
                    <!-- ...other services... -->
                </list>
            </property>
        </bean>
    </beans>
    

    For more details about the configuration options please see the documentation of MessagingChannelConfig class.

    Accessing Channels

    Channel can be accessed via the channel(String, Class) method, with the first parameter being the channel name and the second parameter being the base type of messages that can be transferred over that channel:

    
    MessagingChannel<String> channel = hekate.messaging().channel("example.channel", String.class);
    

    Sending Messages

    MessagingChannel provides API for the following communication patterns:

    • Request - Submit a request and get a response
    • Send - Submit a one-way message that doesn't need a response
    • Subscribe - Submit a request (subscribe) and get multiple response chunks (updates)
    • Aggregate - Submit a one-way message to multiple nodes simultaneously
    • Broadcast - Submit a request to multiple nodes simultaneously and aggregate their responses

    Request

    Request interface can be used for bidirectional communications with remote nodes using the request-response pattern:

    
    // Submit request and synchronously await for the response.
    String response = channel.newRequest("example request").response();
    

    ... or using a completely asynchronous callback-based approach:

    
    // Submit request and process response in the asynchronous callback.
    channel.newRequest("example request").submit((err, rsp) -> {
        if (err == null) {
            System.out.println("Got response: " + rsp.payload());
        } else {
            System.out.println("Request failed: " + err);
        }
    });
    

    For more details please see the documentation of Request interface.

    Send

    Send interface provides support for unidirectional communications (i.e. when remote node doesn't need to send back a response) using the fire and forget approach:

    
    // Send message and synchronously await for the operation's result.
    channel.newSend("example message").sync();
    

    ... or using a completely asynchronous callback-based approach:

    
    // Send message and process results in the asynchronous callback.
    channel.newSend("example message").submit(err -> {
        if (err == null) {
            System.out.println("Message sent.");
        } else {
            System.out.println("Sending failed: " + err);
        }
    });
    

    For more details please see the documentation of Send interface.

    Subscribe

    Subscribe interface can be used for bidirectional communications with remote nodes using the request-response pattern. Unlike the basic Request operation, this operation doesn't end with the first response and continues receiving updates unless the very final response is received:

    
    // Submit request and synchronously await for the response.
    channel.newSubscribe("example request").submit((err, rsp) -> {
        if (rsp.isLastPart()) {
            System.out.println("Done: " + rsp.payload());
        } else {
            System.out.println("Update: " + rsp.payload());
        }
    });
    

    For more details please see the documentation of Subscribe interface.

    Aggregate

    Aggregate interface can be used for bidirectional communications by submitting a message to multiple nodes and gathering (aggregating) replies from those nodes. Results of such aggregation are represented by the AggregateResult interface. This interface provides methods for analyzing responses from remote nodes and checking for possible failures.

    Below is the example of synchronous aggregation:

    
    // Submit aggregation request.
    channel.newAggregate("example message").results().forEach(rslt ->
        System.out.println("Got results: " + rslt)
    );
    

    ... or using a completely asynchronous callback-based approach:

    
    // Asynchronously submit aggregation request.
    channel.newAggregate("example message").submit((err, rslts) -> {
        if (err == null) {
            rslts.forEach(rslt ->
                System.out.println("Got results: " + rslt)
            );
        } else {
            System.out.println("Aggregation failure: " + err);
        }
    });
    

    For more details please see the documentation of Aggregate interface.

    Broadcast

    Broadcast interface provides support for unidirectional broadcasting (i.e. when remote nodes do not need to send a reply and no aggregation should take place) using the fire and forget approach.

    Below is the example of synchronous broadcast:

    
    // Broadcast message.
    channel.newBroadcast("example message").sync();
    

    ... or using a completely asynchronous callback-based approach:

    
    // Asynchronously broadcast message.
    channel.newBroadcast("example message").submit((err, rslt) -> {
        if (err == null) {
            System.out.println("Broadcast success.");
        } else {
            System.out.println("Broadcast failure: " + err);
        }
    });
    

    For more details please see the documentation of Broadcast interface.

    Receiving Messages

    Messaging channel can receive messages from remote nodes by registering an instance of MessageReceiver interface via the MessagingChannelConfig.setReceiver(MessageReceiver) method.

    Important: Only one receiver can be registered per each messaging channel.

    Received messages are represented by the Message interface. This interface provides methods for getting the payload of a received message as well as methods for replying to that message.

    Below is the example of MessageReceiver implementation:

    
    public class ExampleReceiver implements MessageReceiver<String> {
        @Override
        public void receive(Message<String> msg) {
            // Get payload.
            String payload = msg.payload();
    
            // Check if the sender expects a response.
            if (msg.mustReply()) {
                System.out.println("Request received: " + payload);
    
                // Send back a response.
                msg.reply("...some response...");
            } else {
                // No need to send a response since this is a unidirectional message.
                System.out.println("Notification received: " + payload);
            }
        }
    }
    

    Routing and Load Balancing

    Every messaging channel uses an instance of LoadBalancer interface to perform routing of unicast operations (like send(...) and request(...)). Load balancer can be pre-configured via the MessagingChannelConfig.setLoadBalancer(LoadBalancer) method or specified dynamically via the MessagingChannel.withLoadBalancer(LoadBalancer) method. If load balancer is not specified then messaging channel will fall back to the DefaultLoadBalancer.

    Note that load balancing does not get applied to broadcast operations (like MessagingChannel.newBroadcast(Object) and MessagingChannel.newAggregate(Object)). Such operations are submitted to all nodes within the channel's cluster topology. Please see the "Cluster topology filtering" section for details of how to control the channel's cluster topology.

    Consistent Routing

    Applications can provide an affinity key to the LoadBalancer so that it could perform consistent routing based on some application-specific criteria. For example, if the DefaultLoadBalancer is being used by the messaging channel then it will make sure that all messages with the same affinity key will always be routed to the same cluster node (unless the cluster topology doesn't change) by using the channel's partition mapper. Custom implementations of the LoadBalancer interface can use their own algorithms for consistent routing.

    Affinity key can for unicast operations can be specified via the following methods:

    Affinity key can for broadcast operations can be specified via the following methods:

    Note: If affinity key is specified for a broadcast operation then messaging channel will use its partition mapper to select the target Partition for that key. Once the partition is selected then all of its nodes will be used for broadcast (i.e. primary node + backup nodes).

    Thread Affinity

    Besides providing a hint to the LoadBalancer, specifying an affinity key also instructs the messaging channel to process all messages of the same affinity key on the same thread. This applies both to sending a message (see SendCallback or RequestCallback) and to receiving a message (see MessageReceiver.receive(Message)).

    Cluster Topology Filtering

    It is possible to narrow down the list of nodes that are visible to the MessagingChannel by setting a ClusterNodeFilter. Such filter can be pre-configured via the MessagingChannelConfig.setClusterFilter(ClusterNodeFilter) method or set dynamically via the ClusterFilterSupport.filter(ClusterNodeFilter) method.

    Note that the MessagingChannel interface extends the ClusterFilterSupport interface, which gives it a number of shortcut methods for dynamic filtering of the cluster topology:

    If filter is specified then all messaging operations will be distributed among only those nodes that match the filter's criteria.

    Thread Pooling

    Messaging service manages a pool of threads for each of its registered channels. The following thread pools are managed:

    • NIO thread pool - thread pool for managing NIO socket channels. The size of this thread pool is controlled by the MessagingConfigBase.setNioThreads(int) configuration option.
    • Worker thread pool - Optional thread pool to offload messages processing work from NIO threads. The size of this pool is controlled by the MessagingChannelConfig.setWorkerThreads(int) configuration option. It is recommended to set this parameter in case if message processing is a heavy operation that can block NIO thread for a long time.
    See Also:
    MessagingServiceFactory