/* * Copyright 2016 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.internal; import io.atomix.catalyst.concurrent.Futures; import io.atomix.catalyst.concurrent.Listener; import io.atomix.catalyst.concurrent.Listeners; import io.atomix.copycat.client.CopycatClient; import io.atomix.group.DistributedGroup; import io.atomix.group.GroupMember; import io.atomix.group.LocalMember; import io.atomix.group.election.Election; import io.atomix.group.election.internal.GroupElection; import io.atomix.group.messaging.MessageClient; import io.atomix.group.messaging.internal.GroupMessage; import io.atomix.group.messaging.internal.GroupMessageClient; import io.atomix.group.messaging.internal.MessageConsumerService; import io.atomix.group.messaging.internal.MessageProducerService; import io.atomix.resource.AbstractResource; import io.atomix.resource.ResourceType; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; /** * Base {@link DistributedGroup} implementation which manages a membership set for the group. * <p> * The membership group is the base {@link DistributedGroup} type which is created when a new group * is created via the Atomix API. * <pre> * {@code * DistributedGroup group = atomix.getGroup("foo").get(); * } * </pre> * * @author <a href="http://github.com/kuujo>Jordan Halterman</a> */ public class MembershipGroup extends AbstractResource<DistributedGroup> implements DistributedGroup { private final Listeners<GroupMember> joinListeners = new Listeners<>(); private final Listeners<GroupMember> leaveListeners = new Listeners<>(); private final GroupElection election = new GroupElection(this); private final GroupMessageClient messages; private final DistributedGroup.Options options; private final Map<String, AbstractGroupMember> members = new ConcurrentHashMap<>(); private final MessageProducerService producerService; private final MessageConsumerService consumerService; private final Map<String, GroupCommands.Join> localJoins = new ConcurrentHashMap<>(); public MembershipGroup(CopycatClient client, Properties options) { super(client, new ResourceType(DistributedGroup.class), options); this.producerService = new MessageProducerService(this.client); this.consumerService = new MessageConsumerService(this.client); this.messages = new GroupMessageClient(producerService); this.options = new DistributedGroup.Options(options); } @Override public DistributedGroup.Config config() { return new DistributedGroup.Config(config); } @Override public DistributedGroup.Options options() { return options; } @Override public Election election() { return election; } @Override public MessageClient messaging() { return messages; } @Override public GroupMember member(String memberId) { return members.get(memberId); } @Override @SuppressWarnings("unchecked") public Collection<GroupMember> members() { return (Collection) members.values(); } @Override public CompletableFuture<LocalMember> join() { return join(UUID.randomUUID().toString(), false, null); } @Override public CompletableFuture<LocalMember> join(String memberId) { return join(memberId, true, null); } @Override public CompletableFuture<LocalMember> join(Object metadata) { return join(UUID.randomUUID().toString(), false, metadata); } @Override public CompletableFuture<LocalMember> join(String memberId, Object metadata) { return join(memberId == null ? UUID.randomUUID().toString() : memberId, memberId != null, metadata); } /** * Joins the group. * * @param memberId The member ID with which to join the group. * @param persistent Indicates whether the member ID is persistent. * @return A completable future to be completed once the member has joined the group. */ private CompletableFuture<LocalMember> join(String memberId, boolean persistent, Object metadata) { // When joining a group, the join request is guaranteed to complete prior to the join // event being received. final GroupCommands.Join cmd = new GroupCommands.Join(memberId, persistent, metadata); return client.submit(cmd).thenApply(info -> { AbstractGroupMember member = members.get(info.memberId()); if (member == null || !(member instanceof LocalGroupMember)) { member = new LocalGroupMember(info, this, producerService, consumerService); localJoins.put(memberId, cmd); members.put(info.memberId(), member); } return (LocalGroupMember) member; }); } @Override public Listener<GroupMember> onJoin(Consumer<GroupMember> listener) { return joinListeners.add(listener); } @Override public CompletableFuture<Void> remove(String memberId) { return client.submit(new GroupCommands.Leave(memberId)).thenRun(() -> { localJoins.remove(memberId); GroupMember member = members.remove(memberId); if (member != null) { leaveListeners.accept(member); } }); } @Override public Listener<GroupMember> onLeave(Consumer<GroupMember> listener) { return leaveListeners.add(listener); } @Override public CompletableFuture<DistributedGroup> open() { return super.open().thenApply(result -> { client.onEvent("join", this::onJoinEvent); client.onEvent("leave", this::onLeaveEvent); client.onEvent("alive", this::onAliveEvent); client.onEvent("dead", this::onDeadEvent); client.onEvent("message", this::onMessageEvent); client.onEvent("ack", this::onAckEvent); client.onEvent("term", this::onTermEvent); client.onEvent("elect", this::onElectEvent); return result; }).thenCompose(v -> sync()) .thenApply(v -> this); } @Override protected CompletableFuture<Void> recover(Integer attempt) { Boolean recover = Boolean.parseBoolean(options.getProperty("recover", "true")); if (!recover) { return Futures.completedFuture(null); } // When recovering the membership group, we need to ensure that all local non-persistent members are // removed from the group prior to fetching the group membership from the cluster again, and prior to // adding recovered members that the membership list is updated to ensure the list is consistent // when join event handlers are called. Map<String, GroupCommands.Join> joins = new HashMap<>(localJoins); return sync() .thenCompose(v -> { List<CompletableFuture> futures = new ArrayList<>(joins.size()); for (GroupCommands.Join join : joins.values()) { if (!join.persist()) { futures.add(remove(join.member())); } } return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); }) .thenCompose(v -> sync()) .thenCompose(v -> { List<CompletableFuture> futures = new ArrayList<>(joins.size()); for (GroupCommands.Join join : joins.values()) { if (join.persist()) { futures.add(join(join.member(), join.persist(), join.metadata())); } else { futures.add(join(join.metadata())); } } return CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])); }); } /** * Synchronizes the membership group. */ private CompletableFuture<Void> sync() { return client.submit(new GroupCommands.Listen()).thenAccept(status -> { for (GroupMemberInfo info : status.members()) { AbstractGroupMember member = this.members.get(info.memberId()); if (member == null) { member = new RemoteGroupMember(info, this, producerService); this.members.put(member.id(), member); } } election.setTerm(status.term()); if (status.leader() != null) { GroupMember leader = this.members.get(status.leader()); if (leader != null) { election.setLeader(leader); } } }); } /** * Handles a join event received from the cluster. */ private void onJoinEvent(GroupMemberInfo info) { // If the join event was for a local member, the local member is guaranteed to have already // been created since responses will always be received before events. Therefore, if the member // is null we can create a remote member. If the member is a local member, only call join listeners. // Local member join listeners are called here to ensure they're called *after* the join future // completes as is guaranteed by the event framework. AbstractGroupMember member = members.get(info.memberId()); if (member == null) { member = new RemoteGroupMember(info, this, producerService); members.put(info.memberId(), member); joinListeners.accept(member); } else { member.onStatusChange(GroupMember.Status.ALIVE); if (member instanceof LocalGroupMember) { joinListeners.accept(member); } } } /** * Handles a leave event received from the cluster. */ private void onLeaveEvent(String memberId) { GroupMember member = members.remove(memberId); if (member != null) { // Trigger leave listeners. leaveListeners.accept(member); } } /** * Handles a member status change to ALIVE. */ private void onAliveEvent(String memberId) { AbstractGroupMember member = members.get(memberId); if (member != null) { member.onStatusChange(GroupMember.Status.ALIVE); } } /** * Handles a member status change to DEAD. */ private void onDeadEvent(String memberId) { AbstractGroupMember member = members.get(memberId); if (member != null) { member.onStatusChange(GroupMember.Status.DEAD); } } /** * Handles a message event received from the cluster. */ @SuppressWarnings("unchecked") private void onMessageEvent(GroupMessage message) { consumerService.onMessage(message); } /** * Handles an ack event received from the cluster. */ private void onAckEvent(GroupCommands.Ack ack) { producerService.onAck(ack); } /** * Handles a term change event. */ private void onTermEvent(long term) { election.onTerm(term); } /** * Handles an elect event. */ private void onElectEvent(String memberId) { AbstractGroupMember member = members.get(memberId); if (member != null) { election.onElection(member); } } }