package com.thinkbiganalytics.cluster; /*- * #%L * kylo-cluster-manager-core * %% * Copyright (C) 2017 ThinkBig Analytics * %% * 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. * #L% */ import org.apache.commons.lang3.StringUtils; import org.jgroups.Address; import org.jgroups.Channel; import org.jgroups.ChannelListener; import org.jgroups.JChannel; import org.jgroups.Message; import org.jgroups.ReceiverAdapter; import org.jgroups.View; import org.jgroups.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import java.io.FileNotFoundException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import javax.annotation.PostConstruct; /** * */ public class JGroupsClusterService extends ReceiverAdapter implements ClusterService { private static final Logger log = LoggerFactory.getLogger(JGroupsClusterService.class); List<Address> members; JChannel channel; @Value("${kylo.cluster.jgroupsConfigFile:#{null}}") private String jgroupsConfigFile; private static final String CLUSTER_NAME = "internal-kylo-cluster"; private List<ClusterServiceListener> listeners = new ArrayList<>(); private List<ClusterServiceMessageReceiver> messageReceivers = new ArrayList<>(); public void subscribe(ClusterServiceListener listener){ listeners.add(listener); } public void subscribe(ClusterServiceMessageReceiver messageReceiver){ messageReceivers.add(messageReceiver); } /** * Start a channel on the cluster * @throws Exception */ @Override public void start() throws Exception{ if(channel != null){ log.info("Kylo cluster has already been started"); } else if(StringUtils.isNotBlank(jgroupsConfigFile) ) { try { channel = new JChannel(jgroupsConfigFile); String name = Util.generateLocalName(); channel.setName("Kylo - " + name); channel.setReceiver(this); channel.addChannelListener(new Listener()); channel.connect(CLUSTER_NAME); }catch(FileNotFoundException e){ log.error("Unable to find the jgroups cluster configuration file {}. Kylo is not clustered ",jgroupsConfigFile); } } } public void stop() throws Exception { if(channel != null){ log.info("Stopping {} ",getAddressAsString()); channel.disconnect(); } } @Override public boolean isClustered() { return this.channel != null; } @Override public String getAddressAsString(){ if(isClustered()) { return channel.getAddressAsString(); } else { return "localhost"; } } public Address getAddress(){ if(isClustered()) { return channel.getAddress(); } return null; } /** * Update the members reference and determine what node should be primary if needed * @param view the view that just joined the cluster */ public void updateMembers(View view){ //snapshot prev members final List<Address> previousMembers = members != null ? members : new ArrayList<>(); members =view.getMembers(); log.info("Cluster membership changed: There are now {} members in the cluster. {} ",members.size(),members); List<String> previous = previousMembers.stream().map(m -> m.toString()).collect(Collectors.toList()); listeners.stream().forEach(listener -> listener.onClusterMembershipChanged(previous,getMembersAsString())); } /** * Called when a group joins the cluster * @param view */ public void viewAccepted(View view) { updateMembers(view); } /** * Called when a node (including this node) sends a message * @param msg a message */ public void receive(Message msg) { log.info("Receiving {} : {} ",msg.getSrc(),msg.getObject()); messageReceivers.stream().forEach(messageReceiver -> { ClusterMessage clusterMessage = (ClusterMessage) msg.getObject(); messageReceiver.onMessageReceived(msg.getSrc().toString(),clusterMessage); }); } protected class Listener implements ChannelListener { protected Listener() { } public void channelClosed(Channel channel) { log.info("*** Channel closed {},{}",channel.getName(),channel.getView().getMembers()); listeners.stream().forEach(listener -> listener.onClosed(membersAsString(channel.getView().getMembers()))); } public void channelConnected(Channel channel) { log.info("*** Channel connected {},{}",channel.getName(),channel.getView().getMembers()); listeners.stream().forEach(listener -> listener.onConnected(membersAsString(channel.getView().getMembers()))); } public void channelDisconnected(Channel channel) { listeners.stream().forEach(listener -> listener.onDisconnected(membersAsString(channel.getView().getMembers()))); } } public void clusterEnabled(){ if(channel == null){ throw new UnsupportedOperationException(" This is not a clustered Kylo"); } } /** * All messages are converted to a ClusterMessage * @param message a message to send */ @Override public void sendMessage(String type, Serializable message){ clusterEnabled(); try { log.info("Sending {} from {} ",message,this.channel.getAddressAsString()); ClusterMessage msg = new StandardClusterMessage(type,message); channel.send(null, msg); } catch (Exception e){ e.printStackTrace(); //throw send exception } } @Override public void sendMessageToOthers(String type,Serializable message){ clusterEnabled(); try { for(Address address : getOtherMembers()){ log.info("Sending message to {} from {} ",address,this.channel.getAddressAsString()); ClusterMessage msg = new StandardClusterMessage(type,message); channel.send(address, msg); } } catch (Exception e){ e.printStackTrace(); //throw send exception } } public List<Address> getMembers() { return members != null ? members : Collections.emptyList(); } public List<Address> getOtherMembers(){ return getMembers().stream().filter(a -> !a.toString().equalsIgnoreCase(this.channel.getAddressAsString())).collect(Collectors.toList()); } @Override public List<String> getMembersAsString() { return membersAsString(members); } private List<String> membersAsString(List<Address> members){ return members != null ? members.stream().map(a -> a.toString()).collect(Collectors.toList()) : Collections.emptyList(); } @Override public List<String> getOtherMembersAsString(){ return getMembersAsString().stream().filter(a -> !a.equalsIgnoreCase(this.channel.getAddressAsString())).collect(Collectors.toList()); } }