package org.infinispan.remoting.transport.jgroups; import static org.infinispan.remoting.transport.jgroups.CommandAwareRpcDispatcher.constructRequestOptions; import static org.infinispan.remoting.transport.jgroups.CommandAwareRpcDispatcher.isRsvpCommand; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import javax.management.MBeanServer; import javax.management.ObjectName; import org.infinispan.IllegalLifecycleStateException; import org.infinispan.commands.ReplicableCommand; import org.infinispan.commons.CacheConfigurationException; import org.infinispan.commons.CacheException; import org.infinispan.commons.marshall.StreamingMarshaller; import org.infinispan.commons.util.CollectionFactory; import org.infinispan.commons.util.FileLookup; import org.infinispan.commons.util.FileLookupFactory; import org.infinispan.commons.util.TypedProperties; import org.infinispan.commons.util.Util; import org.infinispan.configuration.global.TransportConfiguration; import org.infinispan.configuration.global.TransportConfigurationBuilder; import org.infinispan.configuration.parsing.XmlConfigHelper; import org.infinispan.factories.KnownComponentNames; import org.infinispan.factories.annotations.ComponentName; import org.infinispan.factories.annotations.Inject; import org.infinispan.jmx.JmxUtil; import org.infinispan.notifications.cachemanagerlistener.CacheManagerNotifier; import org.infinispan.remoting.RpcException; import org.infinispan.remoting.inboundhandler.DeliverOrder; import org.infinispan.remoting.inboundhandler.InboundInvocationHandler; import org.infinispan.remoting.responses.CacheNotFoundResponse; import org.infinispan.remoting.responses.ExceptionResponse; import org.infinispan.remoting.responses.Response; import org.infinispan.remoting.rpc.ResponseFilter; import org.infinispan.remoting.rpc.ResponseMode; import org.infinispan.remoting.transport.AbstractTransport; import org.infinispan.remoting.transport.Address; import org.infinispan.remoting.transport.BackupResponse; import org.infinispan.util.TimeService; import org.infinispan.util.concurrent.CompletableFutures; import org.infinispan.util.concurrent.TimeoutException; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import org.infinispan.xsite.XSiteBackup; import org.infinispan.xsite.XSiteReplicateCommand; import org.jgroups.AnycastAddress; import org.jgroups.Event; import org.jgroups.JChannel; import org.jgroups.MembershipListener; import org.jgroups.MergeView; import org.jgroups.UpHandler; import org.jgroups.View; import org.jgroups.blocks.RequestOptions; import org.jgroups.blocks.RspFilter; import org.jgroups.jmx.JmxConfigurator; import org.jgroups.protocols.relay.RELAY2; import org.jgroups.protocols.relay.RouteStatusListener; import org.jgroups.protocols.relay.SiteMaster; import org.jgroups.protocols.tom.TOA; import org.jgroups.util.Buffer; import org.jgroups.util.ExtendedUUID; import org.jgroups.util.Rsp; /** * An encapsulation of a JGroups transport. JGroups transports can be configured using a variety of * methods, usually by passing in one of the following properties: * <ul> * <li><tt>configurationString</tt> - a JGroups configuration String</li> * <li><tt>configurationXml</tt> - JGroups configuration XML as a String</li> * <li><tt>configurationFile</tt> - String pointing to a JGroups XML configuration file</li> * <li><tt>channelLookup</tt> - Fully qualified class name of a * {@link JGroupsChannelLookup} instance</li> * </ul> * These are normally passed in as Properties in * {@link TransportConfigurationBuilder#withProperties(Properties)} or * in the Infinispan XML configuration file. * * @author Manik Surtani * @author Galder ZamarreƱo * @since 4.0 */ public class JGroupsTransport extends AbstractTransport implements MembershipListener { public static final String CONFIGURATION_STRING = "configurationString"; public static final String CONFIGURATION_XML = "configurationXml"; public static final String CONFIGURATION_FILE = "configurationFile"; public static final String CHANNEL_LOOKUP = "channelLookup"; protected static final String DEFAULT_JGROUPS_CONFIGURATION_FILE = "default-configs/default-jgroups-udp.xml"; private static final Log log = LogFactory.getLog(JGroupsTransport.class); private static final boolean trace = log.isTraceEnabled(); protected boolean connectChannel = true, disconnectChannel = true, closeChannel = true; protected CommandAwareRpcDispatcher dispatcher; protected TypedProperties props; protected StreamingMarshaller marshaller; protected CacheManagerNotifier notifier; protected TimeService timeService; protected InboundInvocationHandler globalHandler; protected ScheduledExecutorService timeoutExecutor; protected Executor remoteExecutor; private boolean globalStatsEnabled; private MBeanServer mbeanServer; private String domain; protected JChannel channel; protected Address address; protected Address physicalAddress; // these members are not valid until we have received the first view on a second thread // and channelConnectedLatch is signaled protected volatile int viewId = -1; protected volatile List<Address> members = null; protected volatile Address coordinator = null; protected volatile boolean isCoordinator = false; private final Lock viewUpdateLock = new ReentrantLock(); private final Condition viewUpdateCondition = viewUpdateLock.newCondition(); private CompletableFuture<Void> nextViewFuture = new CompletableFuture<>(); private final ThreadPoolProbeHandler handler; /** * This form is used when the transport is created by an external source and passed in to the * GlobalConfiguration. * * @param channel * created and running channel to use */ public JGroupsTransport(JChannel channel) { this.channel = channel; if (channel == null) throw new IllegalArgumentException("Cannot deal with a null channel!"); if (channel.isConnected()) throw new IllegalArgumentException("Channel passed in cannot already be connected!"); handler = new ThreadPoolProbeHandler(); } public JGroupsTransport() { handler = new ThreadPoolProbeHandler(); } @Override public Log getLog() { return log; } // ------------------------------------------------------------------------------------------------------------------ // Lifecycle and setup stuff // ------------------------------------------------------------------------------------------------------------------ /** * Initializes the transport with global cache configuration and transport-specific properties. * * @param marshaller marshaller to use for marshalling and unmarshalling * @param notifier notifier to use */ @Inject public void initialize(StreamingMarshaller marshaller, CacheManagerNotifier notifier, TimeService timeService, InboundInvocationHandler globalHandler, @ComponentName(KnownComponentNames.TIMEOUT_SCHEDULE_EXECUTOR) ScheduledExecutorService timeoutExecutor, @ComponentName(KnownComponentNames.REMOTE_COMMAND_EXECUTOR) ExecutorService remoteExecutor) { this.marshaller = marshaller; this.notifier = notifier; this.timeService = timeService; this.globalHandler = globalHandler; this.timeoutExecutor = timeoutExecutor; this.remoteExecutor = remoteExecutor; this.handler.updateThreadPool(remoteExecutor); } @Override public void start() { props = TypedProperties.toTypedProperties(configuration.transport().properties()); if (log.isInfoEnabled()) log.startingJGroupsChannel(configuration.transport().clusterName()); initChannelAndRPCDispatcher(); addXSiteViewListener(); startJGroupsChannelIfNeeded(); waitForInitialNodes(); channel.getProtocolStack().getTransport().registerProbeHandler(handler); } protected void startJGroupsChannelIfNeeded() { String clusterName = configuration.transport().clusterName(); if (connectChannel) { try { channel.connect(clusterName); } catch (Exception e) { throw new CacheException("Unable to start JGroups Channel", e); } try { // Normally this would be done by CacheManagerJmxRegistration but // the channel is not started when the cache manager starts but // when first cache starts, so it's safer to do it here. globalStatsEnabled = configuration.globalJmxStatistics().enabled(); if (globalStatsEnabled) { String groupName = String.format("type=channel,cluster=%s", ObjectName.quote(clusterName)); mbeanServer = JmxUtil.lookupMBeanServer(configuration); domain = JmxUtil.buildJmxDomain(configuration, mbeanServer, groupName); JmxConfigurator.registerChannel(channel, mbeanServer, domain, clusterName, true); } } catch (Exception e) { throw new CacheException("Channel connected, but unable to register MBeans", e); } } address = fromJGroupsAddress(channel.getAddress()); if (!connectChannel) { // the channel was already started externally, we need to initialize our member list viewAccepted(channel.getView()); } if (log.isInfoEnabled()) log.localAndPhysicalAddress(clusterName, getAddress(), getPhysicalAddresses()); } @Override public int getViewId() { if (channel == null) throw new CacheException("The cache has been stopped and invocations are not allowed!"); return viewId; } @Override public CompletableFuture<Void> withView(int expectedViewId) { if (viewId >= expectedViewId) return CompletableFutures.completedNull(); if (trace) { log.tracef("Waiting for transaction data for view %d, current view is %d", expectedViewId, viewId); } viewUpdateLock.lock(); try { if (viewId >= expectedViewId) { return CompletableFutures.completedNull(); } else if (viewId < 0) { throw new IllegalLifecycleStateException(); } else { return nextViewFuture.thenCompose(nil -> withView(expectedViewId)); } } finally { viewUpdateLock.unlock(); } } @Override public void waitForView(int viewId) throws InterruptedException { if (channel == null) return; log.tracef("Waiting on view %d being accepted", viewId); long remainingNanos = Long.MAX_VALUE; viewUpdateLock.lock(); try { while (channel != null && getViewId() < viewId && remainingNanos > 0) { remainingNanos = viewUpdateCondition.awaitNanos(remainingNanos); } } finally { viewUpdateLock.unlock(); } } @Override public void stop() { if (channel != null) { channel.getProtocolStack().getTransport().unregisterProbeHandler(handler); } String clusterName = configuration.transport().clusterName(); try { if (disconnectChannel && channel != null && channel.isConnected()) { log.disconnectJGroups(clusterName); // Unregistering before disconnecting/closing because // after that the cluster name is null if (globalStatsEnabled) { JmxConfigurator.unregisterChannel(channel, mbeanServer, domain, channel.getClusterName()); } channel.disconnect(); } if (closeChannel && channel != null && channel.isOpen()) { channel.close(); } } catch (Exception toLog) { log.problemClosingChannel(toLog, clusterName); } if (dispatcher != null) { log.stoppingRpcDispatcher(clusterName); dispatcher.close(); if (channel != null) { // Remove reference to up_handler UpHandler handler = channel.getUpHandler(); log.debugf("Removing existing UpHandler %s", handler); channel.setUpHandler(null); } } channel = null; viewId = -1; members = Collections.emptyList(); coordinator = null; isCoordinator = false; dispatcher = null; CompletableFuture<Void> oldFuture = null; viewUpdateLock.lock(); try { // Create a completable future for the new view oldFuture = nextViewFuture; nextViewFuture = new CompletableFuture<>(); // Wake up any threads blocked in waitForView() viewUpdateCondition.signalAll(); } finally { viewUpdateLock.unlock(); // And finally, complete the future for the old view if (oldFuture != null) { oldFuture.complete(null); } } } protected void initChannel() { final TransportConfiguration transportCfg = configuration.transport(); if (channel == null) { buildChannel(); if (connectChannel) { // Cannot change the name if the channelLookup already connected the channel String transportNodeName = transportCfg.nodeName(); if (transportNodeName != null && transportNodeName.length() > 0) { long range = Short.MAX_VALUE * 2; long randomInRange = (long) ((Math.random() * range) % range) + 1; transportNodeName = transportNodeName + "-" + randomInRange; channel.setName(transportNodeName); } } } // Channel.LOCAL *must* be set to false so we don't see our own messages - otherwise // invalidations targeted at remote instances will be received by self. // NOTE: total order needs to deliver own messages. the invokeRemotely method has a total order boolean // that when it is false, it discard our own messages, maintaining the property needed channel.setDiscardOwnMessages(false); // if we have a TopologyAwareConsistentHash, we need to set our own address generator in JGroups if (transportCfg.hasTopologyInfo()) { // We can do this only if the channel hasn't been started already if (connectChannel) { channel.addAddressGenerator(() -> JGroupsTopologyAwareAddress .randomUUID(channel.getName(), transportCfg.siteId(), transportCfg.rackId(), transportCfg.machineId())); } else { org.jgroups.Address jgroupsAddress = channel.getAddress(); if (jgroupsAddress instanceof ExtendedUUID) { JGroupsTopologyAwareAddress address = new JGroupsTopologyAwareAddress((ExtendedUUID) jgroupsAddress); if (!address.matches(transportCfg.siteId(), transportCfg.rackId(), transportCfg.machineId())) { throw new CacheException("Topology information does not match the one set by the provided JGroups channel"); } } else { throw new CacheException("JGroups address does not contain topology coordinates"); } } } } private void initChannelAndRPCDispatcher() throws CacheException { initChannel(); initRPCDispatcher(); } protected void initRPCDispatcher() { dispatcher = new CommandAwareRpcDispatcher(channel, this, globalHandler, timeoutExecutor, timeService, remoteExecutor, marshaller); dispatcher.start(); } // This is per CM, so the CL in use should be the CM CL private void buildChannel() { FileLookup fileLookup = FileLookupFactory.newInstance(); // in order of preference - we first look for an external JGroups file, then a set of XML // properties, and // finally the legacy JGroups String properties. String cfg; if (props != null) { if (props.containsKey(CHANNEL_LOOKUP)) { String channelLookupClassName = props.getProperty(CHANNEL_LOOKUP); try { JGroupsChannelLookup lookup = Util.getInstance(channelLookupClassName, configuration.classLoader()); channel = lookup.getJGroupsChannel(props); connectChannel = lookup.shouldConnect(); disconnectChannel = lookup.shouldDisconnect(); closeChannel = lookup.shouldClose(); } catch (ClassCastException e) { log.wrongTypeForJGroupsChannelLookup(channelLookupClassName, e); throw new CacheException(e); } catch (Exception e) { log.errorInstantiatingJGroupsChannelLookup(channelLookupClassName, e); throw new CacheException(e); } } if (channel == null && props.containsKey(CONFIGURATION_FILE)) { cfg = props.getProperty(CONFIGURATION_FILE); Collection<URL> confs = Collections.emptyList(); try { confs = fileLookup.lookupFileLocations(cfg, configuration.classLoader()); } catch (IOException io) { //ignore, we check confs later for various states } if (confs.isEmpty()) { throw log.jgroupsConfigurationNotFound(cfg); } else if (confs.size() > 1) { log.ambiguousConfigurationFiles(Util.toStr(confs)); } try { channel = new JChannel(confs.iterator().next()); } catch (Exception e) { throw log.errorCreatingChannelFromConfigFile(cfg, e); } } if (channel == null && props.containsKey(CONFIGURATION_XML)) { cfg = props.getProperty(CONFIGURATION_XML); try { channel = new JChannel(XmlConfigHelper.stringToElement(cfg)); } catch (Exception e) { throw log.errorCreatingChannelFromXML(cfg, e); } } if (channel == null && props.containsKey(CONFIGURATION_STRING)) { cfg = props.getProperty(CONFIGURATION_STRING); try { channel = new JChannel(new ByteArrayInputStream(cfg.getBytes())); } catch (Exception e) { throw log.errorCreatingChannelFromConfigString(cfg, e); } } } if (channel == null) { log.unableToUseJGroupsPropertiesProvided(props); try { channel = new JChannel(fileLookup.lookupFileLocation(DEFAULT_JGROUPS_CONFIGURATION_FILE, configuration.classLoader())); } catch (Exception e) { throw log.errorCreatingChannelFromConfigFile(DEFAULT_JGROUPS_CONFIGURATION_FILE, e); } } } // ------------------------------------------------------------------------------------------------------------------ // querying cluster status // ------------------------------------------------------------------------------------------------------------------ @Override public boolean isCoordinator() { return isCoordinator; } @Override public Address getCoordinator() { return coordinator; } private void waitForInitialNodes() { int initialClusterSize = configuration.transport().initialClusterSize(); if (initialClusterSize <= 1) return; long timeout = configuration.transport().initialClusterTimeout(); long remainingNanos = TimeUnit.MILLISECONDS.toNanos(timeout); viewUpdateLock.lock(); try { while (channel != null && channel.getView().getMembers().size() < initialClusterSize && remainingNanos > 0) { log.debugf("Waiting for %d nodes, current view has %d", initialClusterSize, channel.getView().getMembers().size()); remainingNanos = viewUpdateCondition.awaitNanos(remainingNanos); } } catch (InterruptedException e) { log.interruptedWaitingForCoordinator(e); Thread.currentThread().interrupt(); } finally { viewUpdateLock.unlock(); } if (remainingNanos <= 0) { throw log.timeoutWaitingForInitialNodes(initialClusterSize, channel.getView().getMembers()); } log.debugf("Initial cluster size of %d nodes reached", initialClusterSize); } @Override public List<Address> getMembers() { return members != null ? members : Collections.emptyList(); } @Override public boolean isMulticastCapable() { return channel.getProtocolStack().getTransport().supportsMulticasting(); } @Override public Address getAddress() { return address; } @Override public List<Address> getPhysicalAddresses() { if (physicalAddress == null && channel != null) { org.jgroups.Address addr = (org.jgroups.Address) channel.down(new Event(Event.GET_PHYSICAL_ADDRESS, channel.getAddress())); if (addr == null) { return Collections.emptyList(); } physicalAddress = new JGroupsAddress(addr); } return Collections.singletonList(physicalAddress); } // ------------------------------------------------------------------------------------------------------------------ // outbound RPC // ------------------------------------------------------------------------------------------------------------------ @Override public Map<Address, Response> invokeRemotely(Collection<Address> recipients, ReplicableCommand rpcCommand, ResponseMode mode, long timeout, ResponseFilter responseFilter, DeliverOrder deliverOrder, boolean anycast) throws Exception { CompletableFuture<Map<Address, Response>> future = invokeRemotelyAsync(recipients, rpcCommand, mode, timeout, responseFilter, deliverOrder, anycast); try { //no need to set a timeout for the future. The rpc invocation is guaranteed to complete within the timeout milliseconds return CompletableFutures.await(future); } catch (ExecutionException e) { throw Util.rewrapAsCacheException(e.getCause()); } } @Override public CompletableFuture<Map<Address, Response>> invokeRemotelyAsync(Collection<Address> recipients, ReplicableCommand rpcCommand, ResponseMode mode, long timeout, ResponseFilter responseFilter, DeliverOrder deliverOrder, boolean anycast) throws Exception { if (recipients != null && recipients.isEmpty()) { // don't send if recipients list is empty log.trace("Destination list is empty: no need to send message"); return CompletableFuture.completedFuture(Collections.emptyMap()); } boolean totalOrder = deliverOrder == DeliverOrder.TOTAL; if (trace) log.tracef("dests=%s, command=%s, mode=%s, timeout=%s", recipients, rpcCommand, mode, timeout); Address self = getAddress(); boolean ignoreLeavers = mode == ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS || mode == ResponseMode.WAIT_FOR_VALID_RESPONSE; List<Address> members = getMembers(); if (mode.isSynchronous() && recipients != null && !members.containsAll(recipients)) { if (!ignoreLeavers) { // SYNCHRONOUS Address suspect = recipients.stream().filter(a -> !members.contains(a)).findFirst().orElse(null); CompletableFuture<Map<Address, Response>> future = new CompletableFuture<>(); future.completeExceptionally(new SuspectException( "One or more nodes have left the cluster while replicating command " + rpcCommand, suspect)); return future; } } List<org.jgroups.Address> jgAddressList = toJGroupsAddressListExcludingSelf(recipients, totalOrder); if (jgAddressList != null && jgAddressList.isEmpty()) { return CompletableFutures.completedEmptyMap(); } List<Address> localMembers = this.members; int membersSize = localMembers.size(); boolean broadcast = membersSize > 2 && (jgAddressList == null || recipients.size() == membersSize); CompletableFuture<Responses> rspListFuture = null; SingleResponseFuture singleResponseFuture = null; org.jgroups.Address singleJGAddress = null; if (broadcast) { rspListFuture = dispatcher.invokeRemoteCommands(null, rpcCommand, toJGroupsMode(mode), timeout, toJGroupsFilter(responseFilter), deliverOrder); } else if (totalOrder) { rspListFuture = dispatcher .invokeRemoteCommands(jgAddressList, rpcCommand, toJGroupsMode(mode), timeout, toJGroupsFilter(responseFilter), deliverOrder); } else { boolean skipRpc; boolean singleRecipient; if (jgAddressList == null) { skipRpc = membersSize < 2; singleRecipient = !ignoreLeavers && membersSize == 2; if (singleRecipient) { if (localMembers.get(0).equals(self)) { singleJGAddress = toJGroupsAddress(localMembers.get(1)); } else { singleJGAddress = toJGroupsAddress(localMembers.get(0)); } } } else { skipRpc = false; singleRecipient = !ignoreLeavers && jgAddressList.size() == 1; if (singleRecipient) { singleJGAddress = jgAddressList.get(0); } } if (skipRpc) { return CompletableFutures.completedEmptyMap(); } if (singleRecipient) { singleResponseFuture = dispatcher .invokeRemoteCommand(singleJGAddress, rpcCommand, toJGroupsMode(mode), timeout, deliverOrder); } else { rspListFuture = dispatcher .invokeRemoteCommands(jgAddressList, rpcCommand, toJGroupsMode(mode), timeout, toJGroupsFilter(responseFilter), deliverOrder); } } if (mode.isAsynchronous()) { return CompletableFutures.completedEmptyMap(); } if (singleResponseFuture != null) { // Unicast request org.jgroups.Address finalSingleJGAddress = singleJGAddress; return singleResponseFuture.thenApply(rsp -> { if (trace) log.tracef("Responses: %s", rsp); Address sender = fromJGroupsAddress(finalSingleJGAddress); Response response = checkRsp(rsp, sender, ignoreTimeout(responseFilter), false); return Collections.singletonMap(sender, response); }); } else if (rspListFuture != null) { // Broadcast/anycast request return rspListFuture.thenApply(rsps -> { if (trace) log.tracef("Responses: %s", rsps); Map<Address, Response> responseMap = new HashMap<>(CollectionFactory.computeCapacity(rsps.size())); boolean hasResponses = false; boolean hasValidResponses = false; if (rsps.isTimedOut()) { throw addSuppressedExceptions(new TimeoutException("Replication timeout"), rsps); } for (Map.Entry<org.jgroups.Address, Rsp<Response>> e : rsps) { Rsp<Response> rsp = e.getValue(); if (rsp == null) { // This happens with WAIT_FOR_VALID_RESPONSE continue; } hasResponses |= rsp.wasReceived(); Address sender = fromJGroupsAddress(e.getKey()); Response response = checkRsp(rsp, sender, ignoreTimeout(responseFilter), ignoreLeavers); if (response != null) { hasValidResponses = true; responseMap.put(sender, response); } } if (!hasValidResponses) { // PartitionHandlingInterceptor relies on receiving a RpcException if there are only invalid responses // But we still need to throw a TimeoutException if there are no responses at all. if (hasResponses) { throw new RpcException(String.format("Received invalid responses from all of %s", recipients)); } else { throw new TimeoutException("Timed out waiting for valid responses!"); } } return responseMap; }); } else { throw new IllegalStateException("Should have one remote invocation future"); } } private TimeoutException addSuppressedExceptions(TimeoutException timeoutException, Responses rsps) { for (Map.Entry<org.jgroups.Address, Rsp<Response>> e : rsps) { Rsp<Response> rsp = e.getValue(); Throwable exception; if (rsp == null) { // no need to add suppression } else if (rsp.wasSuspected()) { timeoutException.addSuppressed(new RpcException(e.getKey() + " was suspected")); } else if (rsp.wasUnreachable()) { timeoutException.addSuppressed(new RpcException(e.getKey() + " was unreachable")); } else if ((exception = rsp.getException()) != null) { timeoutException.addSuppressed(exception); } else if (rsp.getValue() instanceof ExceptionResponse) { timeoutException.addSuppressed(((ExceptionResponse) rsp.getValue()).getException()); } else { timeoutException.addSuppressed(new RpcException("Not accepted: " + rsp.getValue())); } } return timeoutException; } @Override public void sendTo(Address destination, ReplicableCommand rpcCommand, DeliverOrder deliverOrder) throws Exception { if (trace) { log.tracef("sendTo: destination=%s, command=%s, order=%s", destination, rpcCommand, deliverOrder); } if (destination.equals(address)) { //removed requireNonNull. this will throw a NPE in that case if (trace) { log.trace("sendTo: not sending to self."); } return; } dispatcher.sendMessage( toJGroupsAddress(destination), dispatcher.marshallCall(rpcCommand), asyncRequestOptions(isRsvpCommand(rpcCommand), deliverOrder)); } @Override public void sendToMany(Collection<Address> destinations, ReplicableCommand rpcCommand, DeliverOrder deliverOrder) throws Exception { if (destinations == null) { sendToAll(rpcCommand, deliverOrder); return; } switch (destinations.size()) { case 0: return; case 1: sendTo(destinations.iterator().next(), rpcCommand, deliverOrder); return; } if (trace) { log.tracef("sendTo: destinations=%s, command=%s, order=%s", destinations, rpcCommand, deliverOrder); } final List<org.jgroups.Address> jgrpAddrList = toJGroupsAddressListExcludingSelf(destinations, deliverOrder == DeliverOrder.TOTAL); final Buffer buffer = dispatcher.marshallCall(rpcCommand); final RequestOptions options = asyncRequestOptions(isRsvpCommand(rpcCommand), deliverOrder); if (deliverOrder == DeliverOrder.TOTAL) { AnycastAddress anycastAddress = new AnycastAddress(jgrpAddrList); dispatcher.sendMessage(anycastAddress, buffer, options); } else if (jgrpAddrList.size() == 1) { dispatcher.sendMessage(jgrpAddrList.get(0), buffer, options); } else { dispatcher.castMessage(jgrpAddrList, buffer, options.anycasting(true).useAnycastAddresses(false)); } } private static RequestOptions asyncRequestOptions(boolean rsvp, DeliverOrder deliverOrder) { return constructRequestOptions(org.jgroups.blocks.ResponseMode.GET_NONE, rsvp, deliverOrder, 0, true); } private void sendToAll(ReplicableCommand rpcCommand, DeliverOrder deliverOrder) throws Exception { if (trace) { log.tracef("sendToAll: command=%s, order=%s", rpcCommand, deliverOrder); } final Buffer buffer = dispatcher.marshallCall(rpcCommand); final RequestOptions options = asyncRequestOptions(isRsvpCommand(rpcCommand), deliverOrder); if (deliverOrder == DeliverOrder.TOTAL) { dispatcher.sendMessage(new AnycastAddress(), buffer, options); } else { dispatcher.castMessage(null, buffer, options.anycasting(false)); } } private boolean ignoreTimeout(ResponseFilter responseFilter) { return responseFilter != null && !responseFilter.needMoreResponses(); } @Override public Map<Address, Response> invokeRemotely(Map<Address, ReplicableCommand> rpcCommands, ResponseMode mode, long timeout, boolean usePriorityQueue, ResponseFilter responseFilter, boolean totalOrder, boolean anycast) throws Exception { DeliverOrder deliverOrder = DeliverOrder.PER_SENDER; if (totalOrder) { deliverOrder = DeliverOrder.TOTAL; } else if (usePriorityQueue) { deliverOrder = DeliverOrder.NONE; } return invokeRemotely(rpcCommands, mode, timeout, responseFilter, deliverOrder, anycast); } @Override public Map<Address, Response> invokeRemotely(Map<Address, ReplicableCommand> rpcCommands, ResponseMode mode, long timeout, ResponseFilter responseFilter, DeliverOrder deliverOrder, boolean anycast) throws Exception { if (rpcCommands == null || rpcCommands.isEmpty()) { // don't send if recipients list is empty log.trace("Destination list is empty: no need to send message"); return Collections.emptyMap(); } if (trace) log.tracef("commands=%s, mode=%s, timeout=%s", rpcCommands, mode, timeout); boolean ignoreLeavers = mode == ResponseMode.SYNCHRONOUS_IGNORE_LEAVERS || mode == ResponseMode.WAIT_FOR_VALID_RESPONSE; CompletableFuture<Rsp<Response>>[] futures = new SingleResponseFuture[rpcCommands.size()]; int i = 0; for (Map.Entry<Address, ReplicableCommand> entry : rpcCommands.entrySet()) { org.jgroups.Address recipient = toJGroupsAddress(entry.getKey()); ReplicableCommand command = entry.getValue(); SingleResponseFuture future = dispatcher.invokeRemoteCommand(recipient, command, toJGroupsMode(mode), timeout, deliverOrder); futures[i] = future; i++; } if (mode.isAsynchronous()) return Collections.emptyMap(); CompletableFuture<Void> bigFuture = CompletableFuture.allOf(futures); CompletableFutures.await(bigFuture); Map<Address, Response> retval = new HashMap<>(CollectionFactory.computeCapacity(futures.length)); boolean hasResponses = false; i = 0; // We are not modifying the rpcCommands map, so the iteration order must stay the same even in a HashMap for (Map.Entry<Address, ReplicableCommand> addressReplicableCommandEntry : rpcCommands.entrySet()) { Address sender = addressReplicableCommandEntry.getKey(); Rsp<Response> rsp = futures[i].get(); Response response = checkRsp(rsp, sender, ignoreTimeout(responseFilter), ignoreLeavers); if (response != null) { retval.put(sender, response); hasResponses = true; } i++; } if (!hasResponses) { // It is possible for their to be no valid response if we ignored leavers and // all of our targets left // If all the targets were suspected we didn't have a timeout throw new TimeoutException("Timed out waiting for valid responses!"); } return retval; } @Override public BackupResponse backupRemotely(Collection<XSiteBackup> backups, XSiteReplicateCommand rpcCommand) throws Exception { if (trace) { log.tracef("About to send to backups %s, command %s", backups, rpcCommand); } Buffer buf = dispatcher.marshallCall(rpcCommand); Map<XSiteBackup, Future<Object>> syncBackupCalls = new HashMap<>(backups.size()); for (XSiteBackup xsb : backups) { SiteMaster recipient = new SiteMaster(xsb.getSiteName()); if (xsb.isSync()) { RequestOptions sync = constructRequestOptions(org.jgroups.blocks.ResponseMode.GET_ALL, false, DeliverOrder.NONE, xsb.getTimeout(), false); syncBackupCalls.put(xsb, dispatcher.sendMessageWithFuture(recipient, buf.getBuf(), buf.getOffset(), buf.getLength(), sync)); } else { RequestOptions async = constructRequestOptions(org.jgroups.blocks.ResponseMode.GET_NONE, false, DeliverOrder.PER_SENDER, xsb.getTimeout(), false); dispatcher.sendMessage(recipient, buf.getBuf(), buf.getOffset(), buf.getLength(), async); } } return new JGroupsBackupResponse(syncBackupCalls, timeService); } private static org.jgroups.blocks.ResponseMode toJGroupsMode(ResponseMode mode) { switch (mode) { case ASYNCHRONOUS: return org.jgroups.blocks.ResponseMode.GET_NONE; case WAIT_FOR_VALID_RESPONSE: return org.jgroups.blocks.ResponseMode.GET_FIRST; case SYNCHRONOUS: case SYNCHRONOUS_IGNORE_LEAVERS: return org.jgroups.blocks.ResponseMode.GET_ALL; } throw new CacheException("Unknown response mode " + mode); } private RspFilter toJGroupsFilter(ResponseFilter responseFilter) { return responseFilter == null ? null : new JGroupsResponseFilterAdapter(responseFilter); } protected Response checkRsp(Rsp<Response> rsp, Address sender, boolean ignoreTimeout, boolean ignoreLeavers) { Response response; if (rsp.wasReceived()) { if (rsp.hasException()) { log.tracef(rsp.getException(), "Unexpected exception from %s", sender); throw log.remoteException(sender, rsp.getException()); } else { response = checkResponse(rsp.getValue(), sender, ignoreLeavers); } } else if (rsp.wasSuspected()) { response = checkResponse(CacheNotFoundResponse.INSTANCE, sender, ignoreLeavers); } else { if (!ignoreTimeout) { throw new TimeoutException("Replication timeout for " + sender); } response = null; } return response; } private void addXSiteViewListener() { RELAY2 relay2 = channel.getProtocolStack().findProtocol(RELAY2.class); if (relay2 != null && relay2.getRouteStatusListener() == null) { relay2.setRouteStatusListener(new DefaultRouteStatusListener()); } } @Override public Set<String> getSitesView() { RELAY2 relay = channel.getProtocolStack().findProtocol(RELAY2.class); RouteStatusListener listener = relay != null ? relay.getRouteStatusListener() : null; return (listener instanceof Supplier) ? ((Supplier<Set<String>>) listener).get() : null; } // ------------------------------------------------------------------------------------------------------------------ // Implementations of JGroups interfaces // ------------------------------------------------------------------------------------------------------------------ private interface Notify { void emitNotification(List<Address> oldMembers, View newView); } private class NotifyViewChange implements Notify { @Override public void emitNotification(List<Address> oldMembers, View newView) { notifier.notifyViewChange(members, oldMembers, getAddress(), (int) newView.getViewId().getId()); } } private class NotifyMerge implements Notify { @Override public void emitNotification(List<Address> oldMembers, View newView) { MergeView mv = (MergeView) newView; final Address address = getAddress(); final int viewId = (int) newView.getViewId().getId(); notifier.notifyMerge(members, oldMembers, address, viewId, getSubgroups(mv.getSubgroups())); } private List<List<Address>> getSubgroups(List<View> subviews) { List<List<Address>> l = new ArrayList<>(subviews.size()); for (View v : subviews) l.add(fromJGroupsAddressList(v.getMembers())); return l; } } @Override public void viewAccepted(View newView) { log.debugf("New view accepted: %s", newView); List<org.jgroups.Address> newMembers = newView.getMembers(); if (newMembers == null || newMembers.isEmpty()) { log.debugf("Received null or empty member list from JGroups channel: " + newView); return; } List<Address> oldMembers = members; // Update every view-related field while holding the lock so that waitForView only returns // after everything was updated. CompletableFuture<Void> oldFuture = null; viewUpdateLock.lock(); try { viewId = (int) newView.getViewId().getId(); // we need a defensive copy anyway members = fromJGroupsAddressList(newMembers); // Delta view debug log for large cluster if (log.isDebugEnabled() && oldMembers != null) { List<Address> joined = new ArrayList<>(members); joined.removeAll(oldMembers); List<Address> left = new ArrayList<>(oldMembers); left.removeAll(members); log.debugf("Joined: %s, Left: %s", joined, left); } // The first view is installed before returning from JChannel.connect // So we need to set the local address here if (address == null) { address = fromJGroupsAddress(channel.getAddress()); } // Now that we have a view, figure out if we are the isCoordinator coordinator = fromJGroupsAddress(newView.getCreator()); isCoordinator = coordinator != null && coordinator.equals(address); // Create a completable future for the new view oldFuture = nextViewFuture; nextViewFuture = new CompletableFuture<>(); // Wake up any threads that are waiting to know about who the isCoordinator is // do it before the notifications, so if a listener throws an exception we can still start viewUpdateCondition.signalAll(); } finally { viewUpdateLock.unlock(); // And finally, complete the future for the old view if (oldFuture != null) { oldFuture.complete(null); } } // now notify listeners - *after* updating the isCoordinator. - JBCACHE-662 boolean hasNotifier = notifier != null; if (hasNotifier) { String clusterName = configuration.transport().clusterName(); Notify n; if (newView instanceof MergeView) { log.receivedMergedView(clusterName, newView); n = new NotifyMerge(); } else { log.receivedClusterView(clusterName, newView); n = new NotifyViewChange(); } n.emitNotification(oldMembers, newView); } JGroupsAddressCache.pruneAddressCache(); } @Override public void suspect(org.jgroups.Address suspected_mbr) { // no-op } @Override public void block() { // no-op since ISPN-83 has been resolved } @Override public void unblock() { // no-op since ISPN-83 has been resolved } // ------------------------------------------------------------------------------------------------------------------ // Helpers to convert between Address types // ------------------------------------------------------------------------------------------------------------------ protected static org.jgroups.Address toJGroupsAddress(Address a) { return ((JGroupsAddress) a).address; } static Address fromJGroupsAddress(final org.jgroups.Address addr) { return JGroupsAddressCache.fromJGroupsAddress(addr); } private List<org.jgroups.Address> toJGroupsAddressListExcludingSelf(Collection<Address> list, boolean totalOrder) { if (list == null) return null; if (list.isEmpty()) return Collections.emptyList(); List<org.jgroups.Address> retval = new ArrayList<>(list.size()); boolean ignoreSelf = !totalOrder; //in total order, we need to send the message to ourselves! Address self = getAddress(); for (Address a : list) { if (!ignoreSelf || !a.equals(self)) { retval.add(toJGroupsAddress(a)); } else { ignoreSelf = false; // short circuit address equality for future iterations } } return retval; } private static List<Address> fromJGroupsAddressList(List<org.jgroups.Address> list) { if (list == null || list.isEmpty()) return Collections.emptyList(); List<Address> retval = new ArrayList<>(list.size()); for (org.jgroups.Address a : list) retval.add(fromJGroupsAddress(a)); return Collections.unmodifiableList(retval); } // mainly for unit testing public CommandAwareRpcDispatcher getCommandAwareRpcDispatcher() { return dispatcher; } public JChannel getChannel() { return channel; } @Override public final void checkTotalOrderSupported() { //For replicated and distributed tx caches, we use TOA as total order protocol. if (channel.getProtocolStack().findProtocol(TOA.class) == null) { throw new CacheConfigurationException("In order to support total order based transaction, the TOA protocol " + "must be present in the JGroups's config."); } } class DefaultRouteStatusListener implements RouteStatusListener, Supplier<Set<String>> { private final Set<String> view = new ConcurrentSkipListSet<>(); @Override public void sitesUp(String... sites) { this.view.addAll(Arrays.asList(sites)); log.receivedXSiteClusterView(this.view); } @Override public void sitesDown(String... sites) { this.view.removeAll(Arrays.asList(sites)); log.receivedXSiteClusterView(this.view); } @Override public Set<String> get() { return Collections.unmodifiableSet(this.view); } } }