/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.atomix.group;
import io.atomix.catalyst.annotations.Beta;
import io.atomix.catalyst.concurrent.Listener;
import io.atomix.catalyst.serializer.Serializer;
import io.atomix.catalyst.util.Assert;
import io.atomix.group.election.Election;
import io.atomix.group.election.Term;
import io.atomix.group.messaging.Message;
import io.atomix.group.messaging.MessageClient;
import io.atomix.group.messaging.MessageService;
import io.atomix.resource.Resource;
import io.atomix.resource.ResourceTypeInfo;
import java.time.Duration;
import java.util.Collection;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* Generic group abstraction for managing group membership, service discovery, leader election, and remote
* scheduling and execution.
* <p>
* The distributed group resource facilitates managing group membership within an Atomix cluster. Membership is
* managed by nodes {@link #join() joining} and {@link LocalMember#leave() leaving} the group, and instances
* of the group throughout the cluster are notified on changes to the structure of the group. Groups can elect a
* leader, and members can communicate directly with one another or through persistent queues.
* <p>
* Groups membership is managed in a replicated state machine. When a member joins the group, the join request
* is replicated, the member is added to the group, and the state machine notifies instances of the
* {@code DistributedGroup} of the membership change. In the event that a group instance becomes disconnected from
* the cluster and its session times out, the replicated state machine will automatically remove the member
* from the group and notify the remaining instances of the group of the membership change.
* <p>
* To create a membership group resource, use the {@code DistributedGroup} class or constructor:
* <pre>
* {@code
* atomix.getGroup("my-group").thenAccept(group -> {
* ...
* });
* }
* </pre>
* <h2>Joining the group</h2>
* When a new instance of the resource is created, it is initialized with an empty {@link #members()} list
* as it is not yet a member of the group. Once the instance has been created, the user must join the group
* via {@link #join()}:
* <pre>
* {@code
* group.join().thenAccept(member -> {
* System.out.println("Joined with member ID: " + member.id());
* });
* }
* </pre>
* Once the group has been joined, the {@link #members()} list provides an up-to-date view of the group which will
* be automatically updated as members join and leave the group. To be explicitly notified when a member joins or
* leaves the group, use the {@link #onJoin(Consumer)} or {@link #onLeave(Consumer)} event consumers respectively:
* <pre>
* {@code
* group.onJoin(member -> {
* System.out.println(member.id() + " joined the group!");
* });
* }
* </pre>
* <h2>Listing the members in the group</h2>
* Users of the distributed group do not have to join the group to interact with it. For instance, while a server
* may participate in the group by joining it, a client may interact with the group just to get a list of available
* members. To access the list of group members, use the {@link #members()} getter:
* <pre>
* {@code
* DistributedGroup group = atomix.getGroup("foo").get();
* for (GroupMember member : group.members()) {
* ...
* }
* }
* </pre>
* Once the group instance has been created, the group membership will be automatically updated each time the structure
* of the group changes. However, in the event that the client becomes disconnected from the cluster, it may not receive
* notifications of changes in the group structure.
* <h2>Persistent members</h2>
* {@code DistributedGroup} supports a concept of persistent members that requires members to <em>explicitly</em>
* {@link LocalMember#leave() leave} the group to be removed from it. Persistent member {@link Message tasks} will remain
* in a failed member's queue until the member recovers.
* <p>
* In order to support recovery, persistent members must be configured with a user-provided {@link GroupMember#id() member ID}.
* The member ID is provided when the member {@link #join(String) joins} the group, and providing a member ID is
* all that's required to create a persistent member.
* <pre>
* {@code
* DistributedGroup group = atomix.getGroup("persistent-members").get();
* LocalGroupMember memberA = group.join("a").get();
* LocalGroupMember memberB = group.join("b").get();
* }
* </pre>
* Persistent members are not limited to a single node. If a node crashes, any persistent members that existed
* on that node may rejoin the group on any other node. Persistent members rejoin simply by calling {@link #join(String)}
* with the unique member ID. Once a persistent member has rejoined the group, its session will be updated and any
* tasks remaining in the member's {@link MessageService} will be published to the member.
* <p>
* Persistent member state is retained <em>only</em> inside the group's replicated state machine and not on clients.
* From the perspective of {@code DistributedGroup} instances in a cluster, in the event that the node on which
* a persistent member is running fails, the member will {@link #onLeave(Consumer) leave} the group. Once the persistent
* member rejoins the group, {@link #onJoin(Consumer)} will be called again on each group instance in the cluster.
* <h2>Leader election</h2>
* The {@code DistributedGroup} resource facilitates leader election which can be used to coordinate a group by
* ensuring only a single member of the group performs some set of operations at any given time. Leader election
* is a core concept of membership groups, and because leader election is a low-overhead process, leaders are
* elected for each group automatically.
* <p>
* Leaders are elected using a fair policy. The first member to {@link #join() join} a group will always become the
* initial group leader. Each unique leader in a group is associated with a {@link Election#term() term}. The term
* represents a globally unique, monotonically increasing token that can be used for fencing. Users can listen for
* changes in group terms and leaders with event listeners:
* <pre>
* {@code
* DistributedGroup group = atomix.getGroup("election-group").get();
* group.election().onElection(term -> {
* ...
* });
* }
* </pre>
* The {@link Term#term() term} is guaranteed to be unique for each {@link Term#leader() leader} and is
* guaranteed to be monotonically increasing. Each instance of a group is guaranteed to see the same leader for the
* same term, and no two leaders can ever exist in the same term. In that sense, the terminology and constraints of
* leader election in Atomix borrow heavily from the Raft consensus algorithm that underlies it.
* <h2>Messaging</h2>
* Members of a group and group instances can communicate with one another through the messaging API,
* {@link MessageService}. Direct messaging between group members is reliable and is done as writes to the Atomix cluster.
* Messages are held in memory within the Atomix cluster and are published to consumers using Copycat's session event
* framework. Messages are guaranteed to be delivered to consumers in the order in which they were sent by a producer.
* Because each message is dependent on at least one or more writes to the Atomix cluster, messaging is not intended
* to support high-throughput use cases. Group messaging is designed for coordinating group behaviors. For example,
* a leader can instruct a random member to perform a task through the messaging API.
* <h3>Direct messaging</h3>
* To send messages directly to a specific member of the group, use the associated {@link GroupMember}'s
* {@link MessageClient}.
* <pre>
* {@code
* GroupMember member = group.member("foo");
* MessageProducer<String> producer = member.messaging().producer("bar");
* producer.send("baz").thenRun(() -> {
* // Message acknowledged
* });
* }
* </pre>
* Users can specify the criteria by which a producer determines when a message is completed by configuring the
* producer's {@link io.atomix.group.messaging.MessageProducer.Execution Execution} policy. To configure the execution
* policy, pass {@link io.atomix.group.messaging.MessageProducer.Options MessageProducer.Options} when creating a
* {@link io.atomix.group.messaging.MessageProducer}.
* <pre>
* {@code
* MessageProducer.Options options = new MessageProducer.Options()
* .withExecution(MessageProducer.Execution.SYNC);
* MessageProducer<String> producer = member.messaging().producer("bar", options);
* }
* </pre>
* Producers can be configured to send messages using three execution policies:
* <ul>
* <li>{@link io.atomix.group.messaging.MessageProducer.Execution#SYNC SYNC} sends messages to consumers
* and awaits acknowledgement from the consumer side of the queue. If a producer is producing to an entire group,
* synchronous producers will await acknowledgement from all members of the group.</li>
* <li>{@link io.atomix.group.messaging.MessageProducer.Execution#ASYNC ASYNC} awaits acknowledgement of
* persistence in the cluster but not acknowledgement that messages have been received and processed by consumers.</li>
* <li>{@link io.atomix.group.messaging.MessageProducer.Execution#REQUEST_REPLY REQUEST_REPLY} awaits
* arbitrary responses from all consumers to which a message is sent. If a message is sent to a group of consumers,
* message reply futures will be completed with a list of reply values.</li>
* </ul>
* When the {@link io.atomix.group.messaging.MessageProducer MessageProducer} is configured with the
* {@link io.atomix.group.messaging.MessageProducer.Execution#ASYNC ASYNC} execution policy, the {@link CompletableFuture}
* returned by the {@link io.atomix.group.messaging.MessageProducer#send(Object)} method will be completed as soon as
* the message is persisted in the cluster.
* <h3>Broadcast messaging</h3>
* Groups also provide a group-wide {@link MessageClient} that allows users to broadcast messages to all members of a
* group or send a direct message to a random member of a group. To use the group-wide message client, use the
* {@link #messaging()} getter.
* <pre>
* {@code
* MessageProducer<String> producer = group.messaging().producer("foo");
* producer.send("Hello world!").thenRun(() -> {
* // Message delivered to all group members
* });
* }
* </pre>
* By default, messages sent through the group-wide message producer will be sent to <em>all</em> members of the group.
* But just as {@link io.atomix.group.messaging.MessageProducer.Execution Execution} policies can be used to define the
* criteria by which message operations are completed, the {@link io.atomix.group.messaging.MessageProducer.Delivery Delivery}
* policy can be used to define how messages are delivered when using a group-wide producer.
* <pre>
* {@code
* MessageProducer.Options options = new MessageProducer.Options()
* .withDelivery(MessageProducer.Delivery.RANDOM);
* MessageProducer<String> producer = member.messaging().producer("bar", options);
* }
* </pre>
* Group-wide producers can be configured with the following {@link io.atomix.group.messaging.MessageProducer.Delivery Delivery}
* policies:
* <ul>
* <li>{@link io.atomix.group.messaging.MessageProducer.Delivery#RANDOM} producers send each message to a random
* member of the group. In the event that a message is not successfully {@link Message#ack() acknowledged} by a
* member and that member fails or leaves the group, random messages will be redelivered to remaining members
* of the group.</li>
* <li>{@link io.atomix.group.messaging.MessageProducer.Delivery#BROADCAST} producers send messages to all available
* members of a group. This option applies only to producers constructed from {@link io.atomix.group.DistributedGroup}
* messaging clients.</li>
* </ul>
* Delivery policies work in tandem with {@link io.atomix.group.messaging.MessageProducer.Execution Execution} policies
* described above. For example, a group-wide producer configured with the
* {@link io.atomix.group.messaging.MessageProducer.Execution#REQUEST_REPLY REQUEST_REPLY} execution policy and
* the {@link io.atomix.group.messaging.MessageProducer.Delivery#BROADCAST BROADCAST} delivery policy will send each
* message to all members of the group and aggregate replies into a {@link Collection} once all consumers have replied
* to the message.
* <p>
* <h3>Message consumers</h3>
* Messages delivered to a group member must be received by listeners registered on the {@link LocalMember}'s
* {@link MessageService}. Only the node to which a member belongs can listen for messages sent to that member. Thus,
* to listen for messages, join a group and create a {@link io.atomix.group.messaging.MessageConsumer}.
* <pre>
* {@code
* LocalMember localMember = group.join().join();
* MessageConsumer<String> consumer = localMember.messaging().consumer("foo");
* consumer.onMessage(message -> {
* message.ack();
* });
* }
* </pre>
* When a message is received, consumers must always {@link Message#ack()} or {@link Message#reply(Object)} to the message.
* Failure to ack or reply to a message will result in a memory leak in the cluster and failure to deliver any additional
* messages to the consumer. When a consumer acknowledges a message, the message will be removed from memory in the cluster
* and the producer that sent the message will be notified according to its configuration.
* <h3>Persistent messaging</h3>
* Messages sent directly to specific members of a group are typically delivered only while that member is connected to
* the group. In the event that a member to which a message is sent fails, the message is failed. This can result in
* transparent failures when using the {@link io.atomix.group.messaging.MessageProducer.Execution#ASYNC ASYNC} execution
* policy. A message can be persisted but may never actually be delivered and acknowledged. To ensure that direct messages
* are eventually delivered, persistent members must be used.
* <pre>
* {@code
* LocalMember member = group.join("member-1").join();
* MessageConsumer<String> consumer = member.messaging().consumer("foo");
* consumer.onMessage(message -> {
* ...
* });
* }
* </pre>
* When a message is sent to a persistent member, the message will be persisted in the cluster until it can be delivered
* to that member regardless of whether the member is actively connected to the cluster. If the persistent member crashes,
* once the member rejoins the group pending messages will be delivered. Persistent members are also free to switch nodes
* to rejoin the group on live nodes, and pending messages will still be redelivered.
* <p>
* Users must take care, however, when using persistent members. {@link io.atomix.group.messaging.MessageProducer.Delivery#BROADCAST BROADCAST}
* messages sent to groups with persistent members that are not connected to the cluster will be persisted in memory in the
* cluster until they can be delivered. If the producer that broadcasts the message is configured to await acknowledgement
* or replies from members, producer {@link io.atomix.group.messaging.MessageProducer#send(Object) send} operations cannot
* be completed until dead members rejoin the group.
* <h3>Serialization</h3>
* Users are responsible for ensuring the serializability of tasks, messages, and properties set on the group
* and members of the group. Serialization is controlled by the group's {@link io.atomix.catalyst.serializer.Serializer}
* which can be access via {@link #serializer()} or on the parent {@code Atomix} instance. Because objects are
* typically replicated throughout the cluster, <em>it's critical that any object sent from any node should be
* serializable by all other nodes</em>.
* <p>
* Users should register serializable types before performing any operations on the group.
* <pre>
* {@code
* DistributedGroup group = atomix.getGroup("group").get();
* group.serializer().register(User.class, UserSerializer.class);
* }
* </pre>
* For the best performance from serialization, it is recommended that serializable types be registered with
* unique type IDs. This allows the Catalyst {@link io.atomix.catalyst.serializer.Serializer} to identify the
* type by its serialization ID rather than its class name. It's essential that the ID for a given type is
* the same all all nodes in the cluster.
* <pre>
* {@code
* group.serializer().register(User.class, 1, UserSerializer.class);
* }
* </pre>
* Users can also serialize {@link java.io.Serializable} types by simply registering the class without any
* other serializer. Catalyst will attempt to use the optimal serializer based on the interfaces implemented
* by the class. Alternatively, type registration can be disabled altogether via {@link Serializer#disableWhitelist()},
* however this is not recommended as arbitrary deserialization of class names is slow and is a security risk.
* <h3>Implementation</h3>
* Group state is managed in a Copycat replicated {@link io.atomix.copycat.server.StateMachine}. When a
* {@code DistributedGroup} is created, an instance of the group state machine is created on each replica in
* the cluster. The state machine instance manages state for the specific membership group. When a member
* {@link #join() joins} the group, a join request is sent to the cluster and logged and replicated before
* being applied to the group state machine. Once the join request has been committed and applied to the
* state machine, the group state is updated and existing group members are notified by
* {@link io.atomix.copycat.server.session.ServerSession#publish(String, Object) publishing} state change
* notifications to open instances of the group. Membership change event notifications are received by all
* open instances of the resource.
* <p>
* Leader election is performed by the group state machine. When the first member joins the group, that
* member will automatically be assigned as the group member. Each time an additional member joins the group,
* the new member will be placed in a leader queue. In the event that the current group leader's
* {@link io.atomix.copycat.session.Session} expires or is closed, the group state machine will assign a new
* leader by pulling from the leader queue and will publish an {@code elect} event to all remaining group
* members. Additionally, for each new leader of the group, the state machine will publish a {@code term} change
* event, providing a globally unique, monotonically increasing token uniquely associated with the new leader.
* <p>
* To track group membership, the group state machine tracks the state of the {@link io.atomix.copycat.session.Session}
* associated with each open instance of the group. In the event that the session expires or is closed, the group
* member associated with that session will automatically be removed from the group and remaining instances
* of the group will be notified.
* <p>
* The group state machine facilitates direct and broadcast messaging through writes to the Atomix cluster. Each message
* sent to a group or a member of a group is committed as a single write to the cluster. Once persisted in the cluster,
* messages are delivered to clients through the state machine's session events API. The group state machine delivers
* messages to sessions based on the configured per-message delivery policy, and client-side group instances are responsible
* for dispatching received messages to the appropriate consumers. When a consumer acknowledges or replies to a message,
* another write is commited to the Atomix cluster, and the group state machine completes the associated message.
* <p>
* The group state machine manages compaction of the replicated log by tracking which state changes contribute to
* the state of the group at any given time. For instance, when a member joins the group, the commit that added the
* member to the group contributes to the group's state as long as the member remains a part of the group. Once the
* member leaves the group or its session is expired, the commit that created and remove the member no longer contribute
* to the group's state and are therefore released from the state machine and will be removed from the log during
* compaction.
*
* @see GroupMember
*
* @author <a href="http://github.com/kuujo>Jordan Halterman</a>
*/
@Beta
@ResourceTypeInfo(id=-20, factory=DistributedGroupFactory.class)
public interface DistributedGroup extends Resource<DistributedGroup> {
/**
* Configuration for cluster-wide {@link DistributedGroup}s.
*/
class Config extends Resource.Config {
public Config() {
}
public Config(Properties defaults) {
super(defaults);
}
/**
* Sets the duration after which to remove persistent members from the group.
*
* @param expiration The duration after which to remove persistent members from the group.
* @return The group configuration.
* @throws NullPointerException if the expiration is {@code null}
*/
public Config withMemberExpiration(Duration expiration) {
setProperty("expiration", String.valueOf(Assert.notNull(expiration, "expiration").toMillis()));
return this;
}
}
/**
* Distributed group options.
*/
class Options extends Resource.Options {
private boolean autoRecover = true;
public Options() {
}
public Options(Properties defaults) {
super(defaults);
}
/**
* Sets whether to automatically recover sessions and client-side state.
*
* @param autoRecover Whether to automatically recover sessions and client-side state.
* @return The group options.
*/
public Options withAutoRecover(boolean autoRecover) {
setProperty("recover", String.valueOf(autoRecover));
return this;
}
}
/**
* Returns the group election.
* <p>
* The returned election is specific to this group's set of members. The {@link Term} defined by the returned
* election will not necessarily be reflected in any subgroups of this group.
*
* @return The group election.
*/
Election election();
/**
* Returns the group message client.
* <p>
* The returned message client is group-wide and can be used to broadcast messages to all members of the group
* or to random members of the group.
*
* @return The group message client.
*/
MessageClient messaging();
/**
* Gets a group member by ID.
* <p>
* If the member with the given ID has not {@link #join() joined} the membership group, the resulting
* {@link GroupMember} will be {@code null}.
*
* @param memberId The member ID for which to return a {@link GroupMember}.
* @return The member with the given {@code memberId} or {@code null} if it is not a known member of the group.
*/
GroupMember member(String memberId);
/**
* Gets the collection of all members in the group.
* <p>
* The group members are fetched from the cluster. If any {@link GroupMember} instances have been referenced
* by this membership group instance, the same object will be returned for that member.
* <p>
* This method returns a {@link CompletableFuture} which can be used to block until the operation completes
* or to be notified in a separate thread once the operation completes. To block until the operation completes,
* use the {@link CompletableFuture#join()} method to block the calling thread:
* <pre>
* {@code
* Collection<GroupMember> members = group.members().get();
* }
* </pre>
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
* thread, use one of the many completable future callbacks:
* <pre>
* {@code
* group.members().thenAccept(members -> {
* members.forEach(member -> {
* member.send("test", "Hello world!");
* });
* });
* }
* </pre>
*
* @return The collection of all members in the group.
*/
Collection<GroupMember> members();
/**
* Joins the instance to the membership group.
* <p>
* Joining the group results in a <em>new</em> member being created and joining the group. Each {@link DistributedGroup}
* instance may represent multiple members of a group. The returned {@link CompletableFuture} will be completed
* with the joined {@link LocalMember} object once the member has joined the group, but does not guarantee that
* all other instances of the group have seen the newly joined member.
* <p>
* This method returns a {@link CompletableFuture} which can be used to block until the operation completes
* or to be notified in a separate thread once the operation completes. To block until the operation completes,
* use the {@link CompletableFuture#join()} method to block the calling thread:
* <pre>
* {@code
* group.join().join();
* }
* </pre>
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
* thread, use one of the many completable future callbacks:
* <pre>
* {@code
* group.join().thenAccept(thisMember -> System.out.println("This member is: " + thisMember.id()));
* }
* </pre>
*
* @return A completable future to be completed once the member has joined.
*/
CompletableFuture<LocalMember> join();
/**
* Joins the instance to the membership group with a user-provided member ID.
* <p>
* Joining the group results in a <em>new</em> member being created and joining the group. Each {@link DistributedGroup}
* instance may represent multiple members of a group. The returned {@link CompletableFuture} will be completed
* with the joined {@link LocalMember} object once the member has joined the group, but does not guarantee that
* all other instances of the group have seen the newly joined member.
* <p>
* When joining a group with a user-provided {@code memberId}, a persistent member is created. In the event that this
* node crashes, the member may rejoin the group on any node with the same {@code memberId} and receive pending messages.
* While the persistent member is disconnected from the cluster, it will not appear in the group {@link #members()}
* list but its state will not be removed from the cluster.
* <p>
* This method returns a {@link CompletableFuture} which can be used to block until the operation completes
* or to be notified in a separate thread once the operation completes. To block until the operation completes,
* use the {@link CompletableFuture#join()} method to block the calling thread:
* <pre>
* {@code
* group.join("foo").join();
* }
* </pre>
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
* thread, use one of the many completable future callbacks:
* <pre>
* {@code
* group.join("foo").thenAccept(thisMember -> System.out.println("This member is: " + thisMember.id()));
* }
* </pre>
*
* @param memberId The unique member ID to assign to the member.
* @return A completable future to be completed once the member has joined.
*/
CompletableFuture<LocalMember> join(String memberId);
/**
* Joins the instance to the membership group with a user-provided member ID.
* <p>
* Joining the group results in a <em>new</em> member being created and joining the group. Each {@link DistributedGroup}
* instance may represent multiple members of a group. The returned {@link CompletableFuture} will be completed
* with the joined {@link LocalMember} object once the member has joined the group, but does not guarantee that
* all other instances of the group have seen the newly joined member.
* <p>
* {@code metadata} provided when a persistent member joins a group can be viewed by all other instances of the
* same group. Metadata objects mut be serializable either via Java's {@link java.io.Serializable} or by registering a
* Catalyst {@link io.atomix.catalyst.serializer.TypeSerializer} on the group's {@link #serializer()}.
* <p>
* This method returns a {@link CompletableFuture} which can be used to block until the operation completes
* or to be notified in a separate thread once the operation completes. To block until the operation completes,
* use the {@link CompletableFuture#join()} method to block the calling thread:
* <pre>
* {@code
* group.join("foo").join();
* }
* </pre>
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
* thread, use one of the many completable future callbacks:
* <pre>
* {@code
* group.join("foo").thenAccept(thisMember -> System.out.println("This member is: " + thisMember.id()));
* }
* </pre>
*
* @param metadata Metadata to assign to the joined group member.
* @return A completable future to be completed once the member has joined.
*/
CompletableFuture<LocalMember> join(Object metadata);
/**
* Joins the instance to the membership group with a user-provided member ID.
* <p>
* Joining the group results in a <em>new</em> member being created and joining the group. Each {@link DistributedGroup}
* instance may represent multiple members of a group. The returned {@link CompletableFuture} will be completed
* with the joined {@link LocalMember} object once the member has joined the group, but does not guarantee that
* all other instances of the group have seen the newly joined member.
* <p>
* When joining a group with a user-provided {@code memberId}, a persistent member is created. In the event that this
* node crashes, the member may rejoin the group on any node with the same {@code memberId} and receive pending messages.
* While the persistent member is disconnected from the cluster, it will not appear in the group {@link #members()}
* list but its state will not be removed from the cluster.
* <p>
* {@code metadata} provided when a persistent member joins a group can be viewed by all other instances of the
* same group. Metadata objects mut be serializable either via Java's {@link java.io.Serializable} or by registering a
* Catalyst {@link io.atomix.catalyst.serializer.TypeSerializer} on the group's {@link #serializer()}.
* <p>
* This method returns a {@link CompletableFuture} which can be used to block until the operation completes
* or to be notified in a separate thread once the operation completes. To block until the operation completes,
* use the {@link CompletableFuture#join()} method to block the calling thread:
* <pre>
* {@code
* group.join("foo", new MyMetadata()).join();
* }
* </pre>
* Alternatively, to execute the operation asynchronous and be notified once the lock is acquired in a different
* thread, use one of the many completable future callbacks:
* <pre>
* {@code
* group.join("foo", new MyMetadata()).thenAccept(thisMember -> System.out.println("This member is: " + thisMember.id()));
* }
* </pre>
*
* @param memberId The unique member ID to assign to the member.
* @param metadata Metadata to assign to the joined group member.
* @return A completable future to be completed once the member has joined.
*/
CompletableFuture<LocalMember> join(String memberId, Object metadata);
/**
* Adds a listener for members joining the group.
* <p>
* The provided {@link Consumer} will be called each time a member joins the group. Note that
* the join consumer will be called before the joining member's {@link #join()} completes.
* <p>
* The returned {@link Listener} can be used to {@link Listener#close() unregister} the listener
* when its use if finished.
*
* @param listener The join listener.
* @return The listener context.
*/
Listener<GroupMember> onJoin(Consumer<GroupMember> listener);
/**
* Removes the member with the given member ID from the group.
*
* @param memberId The member ID of the member to remove from the group.
* @return A completable future to be completed once the member has been removed.
*/
CompletableFuture<Void> remove(String memberId);
/**
* Adds a listener for members leaving the group.
* <p>
* The provided {@link Consumer} will be called each time a member leaves the group. Members can
* leave the group either voluntarily or by crashing or otherwise becoming disconnected from the
* cluster for longer than their session timeout. Note that the leave consumer will be called before
* the leaving member's {@link LocalMember#leave()} completes.
* <p>
* The returned {@link Listener} can be used to {@link Listener#close() unregister} the listener
* when its use if finished.
*
* @param listener The leave listener.
* @return The listener context.
*/
Listener<GroupMember> onLeave(Consumer<GroupMember> listener);
}