All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jsync.eventbus.impl.DefaultEventBus Maven / Gradle / Ivy

There is a newer version: 1.10.13
Show newest version
/*
 * Copyright (c) 2011-2013 The original author or authors
 * ------------------------------------------------------
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution.
 *
 *     The Eclipse Public License is available at
 *     http://www.eclipse.org/legal/epl-v10.html
 *
 *     The Apache License v2.0 is available at
 *     http://www.opensource.org/licenses/apache2.0.php
 *
 * You may elect to redistribute this code under either of these licenses.
 */

package io.jsync.eventbus.impl;

import io.jsync.AsyncResult;
import io.jsync.AsyncResultHandler;
import io.jsync.Handler;
import io.jsync.VoidHandler;
import io.jsync.buffer.Buffer;
import io.jsync.eventbus.EventBus;
import io.jsync.eventbus.Message;
import io.jsync.eventbus.ReplyException;
import io.jsync.eventbus.ReplyFailure;
import io.jsync.impl.AsyncInternal;
import io.jsync.impl.Closeable;
import io.jsync.impl.DefaultContext;
import io.jsync.impl.DefaultFutureResult;
import io.jsync.impl.management.ManagementRegistry;
import io.jsync.json.JsonArray;
import io.jsync.json.JsonObject;
import io.jsync.logging.Logger;
import io.jsync.logging.impl.LoggerFactory;
import io.jsync.net.NetClient;
import io.jsync.net.NetServer;
import io.jsync.net.NetSocket;
import io.jsync.net.impl.ServerID;
import io.jsync.parsetools.RecordParser;
import io.jsync.spi.cluster.AsyncMultiMap;
import io.jsync.spi.cluster.ChoosableIterable;
import io.jsync.spi.cluster.ClusterManager;

import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author Tim Fox
 */
public class DefaultEventBus implements EventBus {

    private static final Logger log = LoggerFactory.getLogger(DefaultEventBus.class);

    private static final Buffer PONG = new Buffer(new byte[]{(byte) 1});
    private static final long PING_INTERVAL = 20000;
    private static final long PING_REPLY_INTERVAL = 20000;
    private final AsyncInternal async;
    private final ConcurrentMap connections = new ConcurrentHashMap<>();
    private final ConcurrentMap handlerMap = new ConcurrentHashMap<>();
    private final ClusterManager clusterMgr;
    private final AtomicLong replySequence = new AtomicLong(0);
    private ServerID serverID;
    private NetServer server;
    private AsyncMultiMap subs;
    private long defaultReplyTimeout = -1;

    public DefaultEventBus(AsyncInternal async) {
        // Just some dummy server ID
        this.async = async;
        this.serverID = new ServerID(-1, "localhost");
        this.server = null;
        this.subs = null;
        this.clusterMgr = null;
        ManagementRegistry.registerEventBus(serverID);
    }

    public DefaultEventBus(AsyncInternal async, int port, String hostname, ClusterManager clusterManager) {
        this(async, port, hostname, clusterManager, null);
    }

    public DefaultEventBus(AsyncInternal async, int port, String hostname, ClusterManager clusterManager,
                           Handler> listenHandler) {
        this.async = async;
        this.clusterMgr = clusterManager;
        this.subs = clusterMgr.getAsyncMultiMap("subs");
        this.server = setServer(port, hostname, listenHandler);
        ManagementRegistry.registerEventBus(serverID);
    }

    static  BaseMessage createMessage(boolean send, String address, U message) {
        BaseMessage bm;
        if (message instanceof String) {
            bm = new StringMessage(send, address, (String) message);
        } else if (message instanceof Buffer) {
            bm = new BufferMessage(send, address, (Buffer) message);
        } else if (message instanceof JsonObject) {
            bm = new JsonObjectMessage(send, address, (JsonObject) message);
        } else if (message instanceof JsonArray) {
            bm = new JsonArrayMessage(send, address, (JsonArray) message);
        } else if (message instanceof byte[]) {
            bm = new ByteArrayMessage(send, address, (byte[]) message);
        } else if (message instanceof Integer) {
            bm = new IntMessage(send, address, (Integer) message);
        } else if (message instanceof Long) {
            bm = new LongMessage(send, address, (Long) message);
        } else if (message instanceof Float) {
            bm = new FloatMessage(send, address, (Float) message);
        } else if (message instanceof Double) {
            bm = new DoubleMessage(send, address, (Double) message);
        } else if (message instanceof Boolean) {
            bm = new BooleanMessage(send, address, (Boolean) message);
        } else if (message instanceof Short) {
            bm = new ShortMessage(send, address, (Short) message);
        } else if (message instanceof Character) {
            bm = new CharacterMessage(send, address, (Character) message);
        } else if (message instanceof Byte) {
            bm = new ByteMessage(send, address, (Byte) message);
        } else if (message == null) {
            bm = new StringMessage(send, address, null);
        } else {
            throw new IllegalArgumentException("Cannot send object of class " + message.getClass() + " on the event bus: " + message);
        }
        return bm;
    }

    @Override
    public EventBus send(String address, Object message, final Handler replyHandler) {
        // In order to fool the type system we need to wrap the handler - at run time the Message handler
        // will happily handle non String types as types are erased at run-time
        Handler> wrapped = replyHandler == null ? null : new Handler>() {
            @Override
            public void handle(Message msg) {
                replyHandler.handle(msg);
            }
        };
        sendOrPub(createMessage(true, address, message), wrapped);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Object message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Object message) {
        sendOrPub(createMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, JsonObject message, final Handler> replyHandler) {
        sendOrPub(new JsonObjectMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, JsonObject message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, JsonObject message) {
        sendOrPub(new JsonObjectMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, JsonArray message, final Handler> replyHandler) {
        sendOrPub(new JsonArrayMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, JsonArray message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, JsonArray message) {
        sendOrPub(new JsonArrayMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Buffer message, final Handler> replyHandler) {
        sendOrPub(new BufferMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Buffer message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Buffer message) {
        sendOrPub(new BufferMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, byte[] message, final Handler> replyHandler) {
        sendOrPub(new ByteArrayMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, byte[] message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, byte[] message) {
        sendOrPub(new ByteArrayMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, String message, final Handler> replyHandler) {
        sendOrPub(new StringMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, String message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, String message) {
        sendOrPub(new StringMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Integer message, final Handler> replyHandler) {
        sendOrPub(new IntMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Integer message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Integer message) {
        sendOrPub(new IntMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Long message, final Handler> replyHandler) {
        sendOrPub(new LongMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Long message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Long message) {
        sendOrPub(new LongMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Float message, final Handler> replyHandler) {
        sendOrPub(new FloatMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Float message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Float message) {
        sendOrPub(new FloatMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Double message, final Handler> replyHandler) {
        sendOrPub(new DoubleMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Double message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Double message) {
        sendOrPub(new DoubleMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Boolean message, final Handler> replyHandler) {
        sendOrPub(new BooleanMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Boolean message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Boolean message) {
        sendOrPub(new BooleanMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Short message, final Handler> replyHandler) {
        sendOrPub(new ShortMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Short message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Short message) {
        sendOrPub(new ShortMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Character message, final Handler> replyHandler) {
        sendOrPub(new CharacterMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Character message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Character message) {
        sendOrPub(new CharacterMessage(true, address, message), null);
        return this;
    }

    @Override
    public  EventBus send(String address, Byte message, final Handler> replyHandler) {
        sendOrPub(new ByteMessage(true, address, message), replyHandler);
        return this;
    }

    @Override
    public  EventBus sendWithTimeout(String address, Byte message, long timeout, final Handler>> replyHandler) {
        sendOrPubWithTimeout(createMessage(true, address, message), replyHandler, timeout);
        return this;
    }

    @Override
    public EventBus send(String address, Byte message) {
        sendOrPub(new ByteMessage(true, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Object message) {
        sendOrPub(createMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, JsonObject message) {
        sendOrPub(new JsonObjectMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, JsonArray message) {
        sendOrPub(new JsonArrayMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Buffer message) {
        sendOrPub(new BufferMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, byte[] message) {
        sendOrPub(new ByteArrayMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, String message) {
        sendOrPub(new StringMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Integer message) {
        sendOrPub(new IntMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Long message) {
        sendOrPub(new LongMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Float message) {
        sendOrPub(new FloatMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Double message) {
        sendOrPub(new DoubleMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Boolean message) {
        sendOrPub(new BooleanMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Short message) {
        sendOrPub(new ShortMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Character message) {
        sendOrPub(new CharacterMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus publish(String address, Byte message) {
        sendOrPub(new ByteMessage(false, address, message), null);
        return this;
    }

    @Override
    public EventBus registerHandler(String address, Handler handler,
                                    Handler> completionHandler) {
        registerHandler(address, handler, completionHandler, false, false, -1);
        return this;
    }

    @Override
    public EventBus registerHandler(String address, Handler handler) {
        registerHandler(address, handler, null);
        return this;
    }

    @Override
    public EventBus registerLocalHandler(String address, Handler handler) {
        registerHandler(address, handler, null, false, true, -1);
        return this;
    }

    @Override
    public EventBus unregisterHandler(String address, Handler handler,
                                      Handler> completionHandler) {
        checkStarted();
        Handlers handlers = handlerMap.get(address);
        if (handlers != null) {
            synchronized (handlers) {
                int size = handlers.list.size();
                // Requires a list traversal. This is tricky to optimise since we can't use a set since
                // we need fast ordered traversal for the round robin
                for (int i = 0; i < size; i++) {
                    HandlerHolder holder = handlers.list.get(i);
                    if (holder.handler == handler) {
                        if (holder.timeoutID != -1) {
                            async.cancelTimer(holder.timeoutID);
                        }
                        handlers.list.remove(i);
                        holder.removed = true;
                        if (handlers.list.isEmpty()) {
                            handlerMap.remove(address);
                            if (subs != null && !holder.localOnly) {
                                removeSub(address, serverID, completionHandler);
                            } else if (completionHandler != null) {
                                callCompletionHandler(completionHandler);
                            }
                        } else if (completionHandler != null) {
                            callCompletionHandler(completionHandler);
                        }
                        holder.context.removeCloseHook(new HandlerEntry(address, handler));
                        return this;
                    }
                }
            }
        }
        return this;
    }

    @Override
    public EventBus unregisterHandler(String address, Handler handler) {
        unregisterHandler(address, handler, null);
        return this;
    }

    @Override
    public void close(Handler> doneHandler) {
        // Explicitly unregister all handlers on close
        unregisterAllHandlers();
        if (clusterMgr != null) {
            clusterMgr.leave();
        }
        if (server != null) {
            server.close(doneHandler);
        }
    }

    private void unregisterAllHandlers() {
        // Unregister all handlers explicitly - don't rely on context hooks
        for (Map.Entry entry : handlerMap.entrySet()) {
            for (HandlerHolder holder : entry.getValue().list) {
                unregisterHandler(entry.getKey(), holder.handler);
            }
        }
    }

    @Override
    public EventBus setDefaultReplyTimeout(long timeoutMs) {
        this.defaultReplyTimeout = timeoutMs;
        return this;
    }

    @Override
    public long getDefaultReplyTimeout() {
        return defaultReplyTimeout;
    }

     void sendReply(ServerID dest, BaseMessage message, Handler> replyHandler) {
        sendOrPub(dest, message, replyHandler, -1);
    }

     void sendReplyWithTimeout(ServerID dest, BaseMessage message, long timeout, Handler>> replyHandler) {
        if (message.address == null) {
            sendNoHandlersFailure(replyHandler);
        } else {
            Handler> handler = convertHandler(replyHandler);
            sendOrPub(dest, message, handler, replyHandler, timeout);
        }
    }

    private NetServer setServer(int port, final String hostName, final Handler> listenHandler) {
        final NetServer server = async.createNetServer().connectHandler(new Handler() {
            public void handle(final NetSocket socket) {
                final RecordParser parser = RecordParser.newFixed(4, null);
                Handler handler = new Handler() {
                    int size = -1;

                    public void handle(Buffer buff) {
                        if (size == -1) {
                            size = buff.getInt(0);
                            parser.fixedSizeMode(size);
                        } else {
                            BaseMessage received = MessageFactory.read(buff);
                            if (received.type() == MessageFactory.TYPE_PING) {
                                // Send back a pong - a byte will do
                                socket.write(PONG);
                            } else {
                                receiveMessage(received, -1, null, null);
                            }
                            parser.fixedSizeMode(4);
                            size = -1;
                        }
                    }
                };
                parser.setOutput(handler);
                socket.dataHandler(parser);
            }

        });
        server.listen(port, hostName, new AsyncResultHandler() {
            @Override
            public void handle(AsyncResult asyncResult) {
                if (asyncResult.succeeded()) {
                    // Obtain system configured public host/port
                    int publicPort = Integer.getInteger("async.cluster.public.port", -1);
                    String publicHost = System.getProperty("async.cluster.public.host", null);

                    // If using a wilcard port (0) then we ask the server for the actual port:
                    int serverPort = (publicPort == -1) ? server.port() : publicPort;
                    String serverHost = (publicHost == null) ? hostName : publicHost;
                    DefaultEventBus.this.serverID = new ServerID(serverPort, serverHost);
                }
                if (listenHandler != null) {
                    if (asyncResult.succeeded()) {
                        listenHandler.handle(new DefaultFutureResult<>((Void) null));
                    } else {
                        listenHandler.handle(new DefaultFutureResult(asyncResult.cause()));
                    }
                } else if (asyncResult.failed()) {
                    log.error("Failed to listen", asyncResult.cause());
                }
            }
        });
        return server;
    }

    private  void sendToSubs(ChoosableIterable subs, BaseMessage message,
                                long timeoutID,
                                Handler>> asyncResultHandler,
                                Handler> replyHandler) {
        if (message.send) {
            // Choose one
            ServerID sid = subs.choose();
            if (!sid.equals(serverID)) {  //We don't send to this node
                sendRemote(sid, message);
            } else {
                receiveMessage(message, timeoutID, asyncResultHandler, replyHandler);
            }
        } else {
            // Publish
            for (ServerID sid : subs) {
                if (!sid.equals(serverID)) {  //We don't send to this node
                    sendRemote(sid, message);
                } else {
                    receiveMessage(message, timeoutID, null, replyHandler);
                }
            }
        }
    }

    private  void sendOrPubWithTimeout(BaseMessage message,
                                             Handler>> asyncResultHandler, long timeout) {
        if (asyncResultHandler == null) {
            throw new IllegalArgumentException(
                    "Cannot sendWithTimeout using a null reply handler on the event bus");
        }
        Handler> handler = convertHandler(asyncResultHandler);
        sendOrPub(null, message, handler, asyncResultHandler, timeout);
    }

    private  void sendOrPub(BaseMessage message, Handler> replyHandler) {
        sendOrPub(null, message, replyHandler, -1);
    }

    private  void sendOrPub(ServerID replyDest, BaseMessage message, Handler> replyHandler, long timeout) {
        sendOrPub(replyDest, message, replyHandler, null, timeout);
    }

    private String generateReplyAddress() {
        if (clusterMgr != null) {
            // The address is a cryptographically secure id that can't be guessed
            return UUID.randomUUID().toString();
        } else {
            // Just use a sequence - it's faster
            return Long.toString(replySequence.incrementAndGet());
        }
    }

    private  void sendOrPub(ServerID replyDest, final BaseMessage message, final Handler> replyHandler,
                                  final Handler>> asyncResultHandler, long timeout) {
        checkStarted();
        DefaultContext context = async.getOrCreateContext();
        if (timeout == -1) {
            timeout = defaultReplyTimeout;
        }
        try {
            message.sender = serverID;
            long timeoutID = -1;
            if (replyHandler != null) {
                message.replyAddress = generateReplyAddress();
                if (timeout != -1) {
                    // Add a timeout to remove the reply handler to prevent leaks in case a reply never comes
                    timeoutID = async.setTimer(timeout, new Handler() {
                        @Override
                        public void handle(Long timerID) {
                            log.warn("Message reply handler for message.address='" + message.address + "' timed out as no reply was received - it will be removed");
                            unregisterHandler(message.replyAddress, replyHandler);
                            if (asyncResultHandler != null) {
                                asyncResultHandler.handle(new DefaultFutureResult>(new ReplyException(ReplyFailure.TIMEOUT, "Timed out waiting for reply")));
                            }
                        }
                    });
                }
                registerHandler(message.replyAddress, replyHandler, null, true, true, timeoutID);
            }
            if (replyDest != null) {
                if (!replyDest.equals(this.serverID)) {
                    sendRemote(replyDest, message);
                } else {
                    receiveMessage(message, timeoutID, asyncResultHandler, replyHandler);
                }
            } else {
                if (subs != null) {
                    final long fTimeoutID = timeoutID;
                    subs.get(message.address, new AsyncResultHandler>() {
                        public void handle(AsyncResult> event) {
                            if (event.succeeded()) {
                                ChoosableIterable serverIDs = event.result();
                                if (serverIDs != null && !serverIDs.isEmpty()) {
                                    sendToSubs(serverIDs, message, fTimeoutID, asyncResultHandler, replyHandler);
                                } else {
                                    receiveMessage(message, fTimeoutID, asyncResultHandler, replyHandler);
                                }
                            } else {
                                log.error("Failed to send message", event.cause());
                            }
                        }
                    });
                } else {
                    // Not clustered
                    receiveMessage(message, timeoutID, asyncResultHandler, replyHandler);
                }
            }

        } finally {
            // Reset the context id - send can cause messages to be delivered in different contexts so the context id
            // of the current thread can change
            if (context != null) {
                async.setContext(context);
            }
        }
    }

    private  Handler> convertHandler(final Handler>> handler) {
        return new Handler>() {
            @Override
            public void handle(Message reply) {
                DefaultFutureResult> result;
                if (reply.body() instanceof ReplyException) {
                    // This is kind of clunky - but hey-ho
                    result = new DefaultFutureResult<>((ReplyException) reply.body());
                } else {
                    result = new DefaultFutureResult<>(reply);
                }
                if (handler != null) {
                    handler.handle(result);
                } else {
                    log.error("Reply sent by sender was not expecting one");
                }
            }
        };
    }

    private void registerHandler(String address, Handler handler,
                                 Handler> completionHandler,
                                 boolean replyHandler, boolean localOnly, long timeoutID) {
        checkStarted();
        if (address == null) {
            throw new NullPointerException("address");
        }
        DefaultContext context = async.getContext();
        boolean hasContext = context != null;
        if (!hasContext) {
            context = async.createEventLoopContext();
        }
        Handlers handlers = handlerMap.get(address);
        if (handlers == null) {
            handlers = new Handlers();
            Handlers prevHandlers = handlerMap.putIfAbsent(address, handlers);
            if (prevHandlers != null) {
                handlers = prevHandlers;
            }
            if (completionHandler == null) {
                completionHandler = new Handler>() {
                    public void handle(AsyncResult event) {
                        if (event.failed()) {
                            log.error("Failed to remove entry", event.cause());
                        }
                    }
                };
            }
            handlers.list.add(new HandlerHolder(handler, replyHandler, localOnly, context, timeoutID));
            if (subs != null && !replyHandler && !localOnly) {
                // Propagate the information
                subs.add(address, serverID, completionHandler);
            } else {
                callCompletionHandler(completionHandler);
            }
        } else {
            handlers.list.add(new HandlerHolder(handler, replyHandler, localOnly, context, timeoutID));
            if (completionHandler != null) {
                callCompletionHandler(completionHandler);
            }
        }
        if (hasContext) {
            HandlerEntry entry = new HandlerEntry(address, handler);
            context.addCloseHook(entry);
        }
    }

    private void callCompletionHandler(Handler> completionHandler) {
        completionHandler.handle(new DefaultFutureResult<>((Void) null));
    }

    public void cleanSubsForServerID(ServerID theServerID) {
        if (subs != null) {
            subs.removeAllForValue(theServerID, new Handler>() {
                public void handle(AsyncResult event) {
                }
            });
        }
    }

    public ServerID serverID() {
        return serverID;
    }

    private void cleanupConnection(ServerID theServerID,
                                   ConnectionHolder holder,
                                   boolean failed) {
        if (holder.timeoutID != -1) {
            async.cancelTimer(holder.timeoutID);
        }
        if (holder.pingTimeoutID != -1) {
            async.cancelTimer(holder.pingTimeoutID);
        }
        try {
            holder.socket.close();
        } catch (Exception ignore) {
        }

        // The holder can be null or different if the target server is restarted with same serverid
        // before the cleanup for the previous one has been processed
        if (connections.remove(theServerID, holder)) {
            log.debug("Cluster connection closed: " + theServerID + " holder " + holder);
        }
    }

    private void sendRemote(final ServerID theServerID, final BaseMessage message) {

        // We need to deal with the fact that connecting can take some time and is async, and we cannot
        // block to wait for it. So we add any sends to a pending list if not connected yet.
        // Once we connect we send them.
        // This can also be invoked concurrently from different threads, so it gets a little
        // tricky
        ConnectionHolder holder = connections.get(theServerID);
        if (holder == null) {
            NetClient client = async.createNetClient();
            // When process is creating a lot of connections this can take some time
            // so increase the timeout
            client.setConnectTimeout(60 * 1000);
            holder = new ConnectionHolder(client);
            ConnectionHolder prevHolder = connections.putIfAbsent(theServerID, holder);
            if (prevHolder != null) {
                // Another one sneaked in
                holder = prevHolder;
            } else {
                holder.connect(client, theServerID);
            }
        }
        holder.writeMessage(message);
    }

    private void schedulePing(final ConnectionHolder holder) {
        holder.pingTimeoutID = async.setTimer(PING_INTERVAL, new Handler() {
            public void handle(Long ignore) {
                // If we don't get a pong back in time we close the connection
                holder.timeoutID = async.setTimer(PING_REPLY_INTERVAL, new Handler() {
                    public void handle(Long timerID) {
                        // Didn't get pong in time - consider connection dead
                        log.warn("No pong from server " + serverID + " - will consider it dead, timerID: " + timerID + " holder " + holder);
                        cleanupConnection(holder.theServerID, holder, true);
                    }
                });
                new PingMessage(serverID).write(holder.socket);
            }
        });
    }

    private void removeSub(String subName, ServerID theServerID, final Handler> completionHandler) {
        subs.remove(subName, theServerID, completionHandler);
    }

    // Called when a message is incoming
    private  void receiveMessage(BaseMessage msg, long timeoutID, Handler>> asyncResultHandler,
                                    Handler> replyHandler) {
        msg.bus = this;
        final Handlers handlers = handlerMap.get(msg.address);
        if (handlers != null) {
            if (msg.send) {
                //Choose one
                HandlerHolder holder = handlers.choose();
                if (holder != null) {
                    doReceive(msg, holder);
                }
            } else {
                // Publish
                for (HandlerHolder holder : handlers.list) {
                    doReceive(msg, holder);
                }
            }
        } else {
            // no handlers
            if (asyncResultHandler != null) {
                sendNoHandlersFailure(asyncResultHandler);
                if (timeoutID != -1) {
                    async.cancelTimer(timeoutID);
                }
                if (replyHandler != null) {
                    unregisterHandler(msg.replyAddress, replyHandler);
                }
            }
        }
    }

    private  void sendNoHandlersFailure(final Handler>> handler) {
        async.runOnContext(new Handler() {
            @Override
            public void handle(Void v) {
                handler.handle(new DefaultFutureResult>(new ReplyException(ReplyFailure.NO_HANDLERS)));
            }
        });
    }


    private  void doReceive(final BaseMessage msg, final HandlerHolder holder) {
        // Each handler gets a fresh copy
        final Message copied = msg.copy();

        holder.context.execute(new Runnable() {
            public void run() {
                // Need to check handler is still there - the handler might have been removed after the message were sent but
                // before it was received
                try {
                    if (!holder.removed) {
                        holder.handler.handle(copied);
                    }
                } finally {
                    if (holder.replyHandler) {
                        unregisterHandler(msg.address, holder.handler);
                    }
                }
            }
        });
    }

    private void checkStarted() {
        if (serverID == null) {
            throw new IllegalStateException("Event Bus is not started");
        }
    }

    private static class HandlerHolder {
        final DefaultContext context;
        final Handler> handler;
        final boolean replyHandler;
        final boolean localOnly;
        final long timeoutID;
        boolean removed;

        HandlerHolder(Handler> handler, boolean replyHandler, boolean localOnly, DefaultContext context, long timeoutID) {
            this.context = context;
            this.handler = handler;
            this.replyHandler = replyHandler;
            this.localOnly = localOnly;
            this.timeoutID = timeoutID;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            HandlerHolder that = (HandlerHolder) o;
            return handler.equals(that.handler);
        }

        @Override
        public int hashCode() {
            return handler.hashCode();
        }

    }

    private static class Handlers {

        final List list = new CopyOnWriteArrayList<>();
        final AtomicInteger pos = new AtomicInteger(0);

        HandlerHolder choose() {
            while (true) {
                int size = list.size();
                if (size == 0) {
                    return null;
                }
                int p = pos.getAndIncrement();
                if (p >= size - 1) {
                    pos.set(0);
                }
                try {
                    return list.get(p);
                } catch (IndexOutOfBoundsException e) {
                    // Can happen
                    pos.set(0);
                }
            }
        }
    }

    private class ConnectionHolder {
        final NetClient client;
        final Queue pending = new ConcurrentLinkedQueue<>();
        volatile NetSocket socket;
        volatile boolean connected;
        long timeoutID = -1;
        long pingTimeoutID = -1;
        ServerID theServerID;

        private ConnectionHolder(NetClient client) {
            this.client = client;
        }

        void writeMessage(BaseMessage message) {
            if (connected) {
                message.write(socket);
            } else {
                synchronized (this) {
                    if (connected) {
                        message.write(socket);
                    } else {
                        pending.add(message);
                    }
                }
            }
        }

        synchronized void connected(final ServerID theServerID, NetSocket socket) {
            this.socket = socket;
            this.theServerID = theServerID;
            connected = true;
            socket.exceptionHandler(new Handler() {
                public void handle(Throwable t) {
                    cleanupConnection(theServerID, ConnectionHolder.this, true);
                }
            });
            socket.closeHandler(new VoidHandler() {
                public void handle() {
                    cleanupConnection(theServerID, ConnectionHolder.this, false);
                }
            });
            socket.dataHandler(new Handler() {
                public void handle(Buffer data) {
                    // Got a pong back
                    async.cancelTimer(timeoutID);
                    schedulePing(ConnectionHolder.this);
                }
            });
            // Start a pinger
            schedulePing(ConnectionHolder.this);
            for (BaseMessage message : pending) {
                message.write(socket);
            }
            pending.clear();
        }

        void connect(NetClient client, final ServerID theServerID) {
            client.connect(theServerID.port, theServerID.host, new AsyncResultHandler() {
                public void handle(AsyncResult res) {
                    if (res.succeeded()) {
                        connected(theServerID, res.result());
                    } else {
                        cleanupConnection(theServerID, ConnectionHolder.this, true);
                    }
                }
            });
        }
    }

    private class HandlerEntry implements Closeable {
        final String address;
        final Handler handler;

        private HandlerEntry(String address, Handler handler) {
            this.address = address;
            this.handler = handler;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (getClass() != o.getClass()) return false;
            HandlerEntry entry = (HandlerEntry) o;
            if (!address.equals(entry.address)) return false;
            if (!handler.equals(entry.handler)) return false;
            return true;
        }

        @Override
        public int hashCode() {
            int result = address != null ? address.hashCode() : 0;
            result = 31 * result + (handler != null ? handler.hashCode() : 0);
            return result;
        }

        // Called by context on undeploy
        public void close(Handler> doneHandler) {
            unregisterHandler(this.address, this.handler);
            doneHandler.handle(new DefaultFutureResult<>((Void) null));
        }
    }
}