@Override
 public void send(String routingKey, CommandMessage<?> commandMessage) throws Exception {
   String destination =
       consistentHash.getMember(routingKey, commandMessage.getPayloadType().getName());
   if (destination == null) {
     throw new CommandDispatchException(
         "No node known to accept " + commandMessage.getPayloadType().getName());
   }
   Address dest = getAddress(destination);
   channel.send(dest, new DispatchMessage(commandMessage, serializer, false));
 }
 private void waitForConnectorSync(int timeoutInSeconds) throws InterruptedException {
   int t = 0;
   while (ConsistentHash.emptyRing().equals(connector1.getConsistentHash())
       || !connector1.getConsistentHash().equals(connector2.getConsistentHash())) {
     // don't have a member for String yet, which means we must wait a little longer
     if (t++ > timeoutInSeconds * 10) {
       fail(
           "Connectors did not manage to synchronize consistent hash ring within "
               + timeoutInSeconds
               + " seconds...");
     }
     Thread.sleep(100);
   }
 }
 private void waitForConnectorSync() throws InterruptedException {
   int t = 0;
   while (ConsistentHash.emptyRing().equals(connector1.getConsistentHash())
       || !connector1.getConsistentHash().equals(connector2.getConsistentHash())) {
     // don't have a member for String yet, which means we must wait a little longer
     if (t++ > 1500) {
       assertEquals(
           "Connectors did not synchronize within 30 seconds.",
           connector1.getConsistentHash().toString(),
           connector2.getConsistentHash().toString());
     }
     Thread.sleep(20);
   }
 }
 @Override
 public <R> void send(
     String routingKey, CommandMessage<?> commandMessage, CommandCallback<R> callback)
     throws Exception {
   String destination =
       consistentHash.getMember(routingKey, commandMessage.getPayloadType().getName());
   if (destination == null) {
     throw new CommandDispatchException(
         "No node known to accept " + commandMessage.getPayloadType().getName());
   }
   Address dest = getAddress(destination);
   callbacks.put(
       commandMessage.getIdentifier(), new MemberAwareCommandCallback<R>(dest, callback));
   channel.send(dest, new DispatchMessage(commandMessage, serializer, true));
 }
/**
 * A CommandBusConnector that uses JGroups to discover and connect to other JGroupsConnectors in the
 * network. Depending on the configuration of the {@link JChannel channel} that was provided, this
 * implementation allows for a dynamic discovery and addition of new members. When members
 * disconnect, their portion of the processing is divided over the remaining members.
 *
 * <p>This connector uses a consistent hashing algorithm to route commands. This ensures that
 * commands with the same routing key will be sent to the same member, regardless of the sending
 * member of that message.
 *
 * <p>Members join the CommandBus using a load factor (see {@link #connect(int)}). This load factor
 * defines the number of sections on the consistent hash ring a node will receive. The more nodes on
 * the ring, the bigger the relative load a member receives. Using a higher number of hashes will
 * also result in a more evenly distribution of load over the different members.
 *
 * @author Allard Buijze
 * @since 2.0
 */
public class JGroupsConnector implements CommandBusConnector {

  private static final Logger logger = LoggerFactory.getLogger(JGroupsConnector.class);

  private final JChannel channel;
  private volatile ConsistentHash consistentHash = ConsistentHash.emptyRing();
  private final String clusterName;
  private final CommandBus localSegment;
  private final Serializer serializer;
  private final JoinCondition joinedCondition = new JoinCondition();
  private final ConcurrentMap<String, MemberAwareCommandCallback> callbacks =
      new ConcurrentHashMap<String, MemberAwareCommandCallback>();
  private final Set<String> supportedCommandTypes = new CopyOnWriteArraySet<String>();
  private volatile int currentLoadFactor;
  private final JGroupsConnector.MessageReceiver messageReceiver;

  /**
   * Initializes the Connector using given resources. The <code>channel</code> is used to connect
   * this connector to the other members. The <code>clusterName</code> is the name of the cluster
   * the channel will be connected to. For local dispatching of commands, the given <code>
   * localSegment</code> is used. When messages are remotely dispatched, the given <code>serializer
   * </code> is used to serialize and deserialize the messages.
   *
   * <p>Note that Connectors on different members need to have the same <code>channel</code>
   * configuration, <code>clusterName</code> and <code>serializer</code> configuration in order to
   * successfully set up a distributed cluster.
   *
   * @param channel The channel (configured, but not connected) used to discover and connect with
   *     the other members
   * @param clusterName The name of the cluster to connect to
   * @param localSegment The command bus on which messages with this member as destination are
   *     dispatched on
   * @param serializer The serialized used to serialize messages before sending them to other
   *     members.
   */
  public JGroupsConnector(
      JChannel channel, String clusterName, CommandBus localSegment, Serializer serializer) {
    this.channel = channel;
    this.clusterName = clusterName;
    this.localSegment = localSegment;
    this.serializer = serializer;
    this.messageReceiver = new MessageReceiver();
  }

  /**
   * Connects this member to the cluster using the given <code>loadFactor</code>. The <code>
   * loadFactor</code> defines the (approximate) relative load that this member will receive.
   *
   * <p>A good default value is 100, which will give this member 100 nodes on the distributed hash
   * ring. Giving all members (proportionally) lower values will result in a less evenly distributed
   * hash.
   *
   * @param loadFactor The load factor for this node.
   * @throws ConnectionFailedException when an error occurs while connecting
   */
  public synchronized void connect(int loadFactor) throws ConnectionFailedException {
    this.currentLoadFactor = loadFactor;
    Assert.isTrue(loadFactor >= 0, "Load Factor must be a positive integer value.");
    Assert.isTrue(
        channel.getReceiver() == null || channel.getReceiver() == messageReceiver,
        "The given channel already has a receiver configured. "
            + "Has the channel been reused with other Connectors?");
    try {
      channel.setReceiver(messageReceiver);
      if (channel.isConnected() && !clusterName.equals(channel.getClusterName())) {
        throw new AxonConfigurationException(
            "The Channel that has been configured with this JGroupsConnector "
                + "is already connected to another cluster.");
      } else if (channel.isConnected()) {
        // we need to fetch state now that we have attached our MessageReceiver
        channel.getState(null, 10000);
      } else {
        // we need to connect. This will automatically fetch state as well.
        channel.connect(clusterName, null, 10000);
      }
      updateMembership();
    } catch (Exception e) {
      joinedCondition.markJoined(false);
      channel.disconnect();
      throw new ConnectionFailedException("Failed to connect to JGroupsConnectorFactoryBean", e);
    }
  }

  private void updateMembership() throws MembershipUpdateFailedException {
    try {
      if (channel.isConnected()) {
        channel.send(
            new Message(
                    null,
                    new JoinMessage(currentLoadFactor, new HashSet<String>(supportedCommandTypes)))
                .setFlag(Message.Flag.RSVP));
      }
    } catch (Exception e) {
      throw new MembershipUpdateFailedException(
          "Failed to dispatch Join message to Distributed Command Bus Members", e);
    }
  }

  /**
   * this method blocks until this member has successfully joined the other members, until the
   * thread is interrupted, or when joining has failed.
   *
   * @return <code>true</code> if the member successfully joined, otherwise <code>false</code>.
   * @throws InterruptedException when the thread is interrupted while joining
   */
  public boolean awaitJoined() throws InterruptedException {
    joinedCondition.await();
    return joinedCondition.isJoined();
  }

  /**
   * this method blocks until this member has successfully joined the other members, until the
   * thread is interrupted, when the given number of milliseconds have passed, or when joining has
   * failed.
   *
   * @param timeout The amount of time to wait for the connection to complete
   * @param timeUnit The time unit of the timeout
   * @return <code>true</code> if the member successfully joined, otherwise <code>false</code>.
   * @throws InterruptedException when the thread is interrupted while joining
   */
  public boolean awaitJoined(long timeout, TimeUnit timeUnit) throws InterruptedException {
    joinedCondition.await(timeout, timeUnit);
    return joinedCondition.isJoined();
  }

  @Override
  public <R> void send(
      String routingKey, CommandMessage<?> commandMessage, CommandCallback<R> callback)
      throws Exception {
    String destination =
        consistentHash.getMember(routingKey, commandMessage.getPayloadType().getName());
    if (destination == null) {
      throw new CommandDispatchException(
          "No node known to accept " + commandMessage.getPayloadType().getName());
    }
    Address dest = getAddress(destination);
    callbacks.put(
        commandMessage.getIdentifier(), new MemberAwareCommandCallback<R>(dest, callback));
    channel.send(dest, new DispatchMessage(commandMessage, serializer, true));
  }

  @Override
  public void send(String routingKey, CommandMessage<?> commandMessage) throws Exception {
    String destination =
        consistentHash.getMember(routingKey, commandMessage.getPayloadType().getName());
    if (destination == null) {
      throw new CommandDispatchException(
          "No node known to accept " + commandMessage.getPayloadType().getName());
    }
    Address dest = getAddress(destination);
    channel.send(dest, new DispatchMessage(commandMessage, serializer, false));
  }

  @Override
  public synchronized <C> void subscribe(Class<C> commandType, CommandHandler<? super C> handler) {
    localSegment.subscribe(commandType, handler);
    if (supportedCommandTypes.add(commandType.getName())) {
      updateMembership();
    }
  }

  @Override
  public synchronized <C> boolean unsubscribe(
      Class<C> commandType, CommandHandler<? super C> handler) {
    if (localSegment.unsubscribe(commandType, handler)) {
      if (supportedCommandTypes.remove(commandType.getName())) {
        updateMembership();
      }
      return true;
    }
    return false;
  }

  private Address getAddress(String nodeName) {
    for (Address member : channel.getView()) {
      if (channel.getName(member).equals(nodeName)) {
        return member;
      }
    }
    throw new IllegalArgumentException(
        "Given node doesn't seem to be a member of the DistributedCommandBus");
  }

  /**
   * Returns the consistent hash on which current assignment of commands to nodes is being executed.
   *
   * @return the consistent hash on which current assignment of commands to nodes is being executed
   */
  public ConsistentHash getConsistentHash() {
    return consistentHash;
  }

  private class MessageReceiver extends ReceiverAdapter {

    @Override
    public void getState(OutputStream ostream) throws Exception {
      Util.objectToStream(consistentHash, new DataOutputStream(ostream));
    }

    @Override
    public void setState(InputStream istream) throws Exception {
      consistentHash = (ConsistentHash) Util.objectFromStream(new DataInputStream(istream));
    }

    @Override
    public void viewAccepted(View view) {
      ConsistentHash newHash = consistentHash.withExclusively(getMemberNames(view));
      if (!consistentHash.equals(newHash)) {
        int messagesLost = 0;
        // check whether the members with outstanding callbacks are all alive
        for (Map.Entry<String, MemberAwareCommandCallback> entry : callbacks.entrySet()) {
          if (!entry.getValue().isMemberLive(view)) {
            MemberAwareCommandCallback callback = callbacks.remove(entry.getKey());
            if (callback != null) {
              messagesLost++;
              callback.onFailure(
                  new RemoteCommandHandlingException(
                      "The connection with the destination was lost before the result was reported."));
            }
          }
        }
        consistentHash = newHash;
        logger.info("Membership has changed. Rebuilt consistent hash ring.");
        logger.debug("New distributed hash: {}", consistentHash.toString());
        if (messagesLost > 0 && logger.isWarnEnabled()) {
          logger.warn(
              "A member was disconnected while waiting for a reply. {} messages are lost without reply.",
              messagesLost);
        }
      }
    }

    @Override
    public void suspect(Address mbr) {
      if (logger.isWarnEnabled()) {
        logger.warn("Suspect member: {}.", channel.getName(mbr));
      }
    }

    @Override
    public void receive(Message msg) {
      Object message = msg.getObject();
      if (message instanceof JoinMessage) {
        processJoinMessage(msg, (JoinMessage) message);
      } else if (message instanceof DispatchMessage) {
        processDispatchMessage(msg, (DispatchMessage) message);
      } else if (message instanceof ReplyMessage) {
        processReplyMessage((ReplyMessage) message);
      }
    }

    private void processDispatchMessage(final Message msg, final DispatchMessage message) {
      final CommandMessage commandMessage = message.getCommandMessage(serializer);
      if (message.isExpectReply()) {
        localSegment.dispatch(commandMessage, new ReplyingCallback(msg, commandMessage));
      } else {
        localSegment.dispatch(commandMessage);
      }
    }

    private void processJoinMessage(Message msg, JoinMessage joinMessage) {
      String channelName = channel.getName(msg.getSrc());

      consistentHash =
          consistentHash.withAdditionalNode(
              channelName, joinMessage.getLoadFactor(), joinMessage.getCommandTypes());
      if (logger.isInfoEnabled()) {
        logger.info("{} joined with load factor: {}", msg.getSrc(), joinMessage.getLoadFactor());
      }

      if (msg.getSrc().equals(channel.getAddress())) {
        joinedCondition.markJoined(true);
        logger.info("Local segment successfully joined the distributed command bus");
      }
    }

    @SuppressWarnings("unchecked")
    private void processReplyMessage(ReplyMessage replyMessage) {
      MemberAwareCommandCallback callback = callbacks.remove(replyMessage.getCommandIdentifier());
      if (callback != null) {
        if (replyMessage.isSuccess()) {
          callback.onSuccess(replyMessage.getReturnValue(serializer));
        } else {
          callback.onFailure(replyMessage.getError(serializer));
        }
      }
    }

    private class ReplyingCallback implements CommandCallback<Object> {

      private final Message msg;
      private final CommandMessage commandMessage;

      public ReplyingCallback(Message msg, CommandMessage commandMessage) {
        this.msg = msg;
        this.commandMessage = commandMessage;
      }

      @Override
      public void onSuccess(Object result) {
        try {
          channel.send(
              msg.getSrc(),
              new ReplyMessage(commandMessage.getIdentifier(), result, null, serializer));
        } catch (Exception e) {
          logger.error(
              "Unable to send reply to command [type: {}, id: {}]. ",
              new Object[] {
                commandMessage.getPayloadType().getSimpleName(), commandMessage.getIdentifier(), e
              });
        }
      }

      @Override
      public void onFailure(Throwable cause) {
        try {
          channel.send(
              msg.getSrc(),
              new ReplyMessage(commandMessage.getIdentifier(), null, cause, serializer));
        } catch (Exception e) {
          logger.error("Unable to send reply:", e);
        }
      }
    }
  }

  private List<String> getMemberNames(View view) {
    List<String> memberNames = new ArrayList<String>(view.size());
    for (Address member : view.getMembers()) {
      memberNames.add(channel.getName(member));
    }
    return memberNames;
  }

  private static final class JoinCondition {

    private final CountDownLatch joinCountDown = new CountDownLatch(1);
    private volatile boolean success;

    public void await() throws InterruptedException {
      joinCountDown.await();
    }

    public void await(long timeout, TimeUnit timeUnit) throws InterruptedException {
      joinCountDown.await(timeout, timeUnit);
    }

    private void markJoined(boolean joinSucceeded) {
      this.success = joinSucceeded;
      joinCountDown.countDown();
    }

    public boolean isJoined() {
      return success;
    }
  }

  private static class MemberAwareCommandCallback<R> implements CommandCallback<R> {

    private final Address dest;
    private final CommandCallback<R> callback;

    public MemberAwareCommandCallback(Address dest, CommandCallback<R> callback) {
      this.dest = dest;
      this.callback = callback;
    }

    public boolean isMemberLive(View currentView) {
      return currentView.containsMember(dest);
    }

    @Override
    public void onSuccess(R result) {
      callback.onSuccess(result);
    }

    @Override
    public void onFailure(Throwable cause) {
      callback.onFailure(cause);
    }
  }
}