Package com.mapr.franz.catcher

Source Code of com.mapr.franz.catcher.Client$HostPort

/*
* Copyright MapR Technologies, 2013
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mapr.franz.catcher;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.ConcurrentHashMultiset;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.protobuf.ByteString;
import com.google.protobuf.ServiceException;
import com.googlecode.protobuf.pro.duplex.PeerInfo;
import com.mapr.franz.catcher.wire.Catcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
* Handles sending log messages to the array of catcher servers.  In doing so, we need
* to be as robust as possible and recover from previous errors.
* <p/>
* The basic idea is that when we originally connect, we give a list of as many servers
* as we can in the form of hostname, port.  These servers are connected to and they
* are queried to find out about any other servers that might be in the cloud.  Servers
* might have multiple routes, but we resolve this because each server returns a unique
* identifier.
* <p/>
* When it comes time to log, we look in a topic => server cache.  If we find a preferred
* server there, we use it.  Otherwise, we pick some server at random.
* <p/>
* When we send a log message, the response may include a redirect to a different server.
* If so, we make sure that we have connected to all forms of that server's address and
* cache that for later.  Then we retry the send to the new server.  We may also cache that
* message for later retry.
* <p/>
* If a log request results in an error, we try to get rid of the connection that caused
* us this grief.  This might ultimately cause us to forget a host or even a cache entry,
* but we will attempt to re-open these connections later, usually due to a referral back
* to that host.  During server cloud reorganizations, we may not re-open the same server.
*/
public class Client {
    private static final int MAX_SERVER_RETRIES_BEFORE_BLACKLISTING = 4;
    private static final int MAX_REDIRECTS_BEFORE_LIVING_WITH_INDIRECTS = 100;

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

    private final ConnectionFactory connector;

    private final long myUniqueId;
    private static final long BIG_PRIME = 213887370601841L;

    // cache of topic => server preferences
    // updates to this race against accesses with little effect.  Mostly, entries
    // will be added and never removed.  Under failure modes, entries may be deleted
    // as well, but the use of an entry that is about to be deleted is not a problem
    // since all that can happen is that an attempt might be made to delete it again.
    private Map<String, Long> topicMap = Maps.newHashMap();

    // mapping from server id to all known connections for that server
    // updates to this race accesses, but this is safe since these should almost
    // always be added at program start and never changed.  Under failure modes,
    // connections may be deleted, but using a connection that is due for deletion
    // is not a big deal.
    private Multimap<Long, CatcherConnection> hostConnections = Multimaps.synchronizedMultimap(HashMultimap.<Long, CatcherConnection>create());

    // history of all connections
    // this is retained so that we can be sure to call close on everything that we
    // ever open.  It also lets us have a back-stop of servers if the topic map points us
    // at a dead server.
    private Set<CatcherConnection> allConnections = Collections.newSetFromMap(new ConcurrentHashMap<CatcherConnection, Boolean>());

    // history of all host,port => server mappings.  Useful for reverse engineering in failure modes
    private Map<HostPort, Long> knownServers = Maps.newConcurrentMap();

    // TODO should periodically send a Hello to update the list of servers

    // TODO should reset this periodically so we try servers again in case network is repaired
    // list of server connections that have been persistently bad which we now ignore
    private Multiset<HostPort> serverBlackList = ConcurrentHashMultiset.create();

    // TODO should decrement this periodically so that we start trying to handle redirects again
    private volatile int redirectCount = 0;

    // TODO should periodically attempt to reconnect to any of these that have gotten lost
    // collects all of the servers we have tried to connect with
    private final Set<HostPort> allKnownServers = Collections.newSetFromMap(new ConcurrentHashMap<HostPort, Boolean>());

    // how many messages has this Client sent (used to generate message UUID)
    private AtomicLong messageCount = new AtomicLong(0);

    public Client(Iterable<PeerInfo> servers) throws IOException, ServiceException {
        this(new ConnectionFactory(), servers);
    }

    public Client(ConnectionFactory connector, Iterable<PeerInfo> servers) throws IOException, ServiceException {
        this.connector = connector;

        // SecureRandom can cause delays if over-used.  Pulling 8 bytes shouldn't be a big deal.
        this.myUniqueId = new SecureRandom().nextLong();
        connectAll(Iterables.transform(servers, new Function<PeerInfo, HostPort>() {
            @Override
            public HostPort apply(PeerInfo input) {
                return new HostPort(input);
            }
        }));

        // every so often, we bring back a black-listed server
        ScheduledExecutorService background = Executors.newSingleThreadScheduledExecutor();
        background.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                if (serverBlackList.size() > 0) {
                    serverBlackList.elementSet().remove(serverBlackList.iterator().next());
                }
            }
        }, 10, 2, TimeUnit.SECONDS);

//        Queue<MessageRetry> retry = Queues.newConcurrentLinkedQueue();
//        background.scheduleWithFixedDelay(new Runnable() {
//            @Override
//            public void run() {
//                MessageRetry message = retry.poll();
//                if (message != null) {
//                    sendMessage(message.topic, message.content);
//                }
//            }
//        }, 1, 1, TimeUnit.SECONDS);

    }

    private static class MessageRetry {
        String topic;
        String content;
        long firstSendTime = System.nanoTime() / 1000000;
        int retryCount = 0;

        private MessageRetry(String topic, String content) {
            this.content = content;
            this.topic = topic;
        }
    }

    public void sendMessage(String topic, String message) throws IOException, ServiceException {
        sendMessage(topic, message.getBytes(Charsets.UTF_8));
    }

    public void sendMessage(String topic, byte[] message) throws IOException, ServiceException {
        sendMessage(topic, ByteBuffer.wrap(message));
    }

    public void sendMessage(String topic, ByteBuffer message) throws IOException {
        long messageId = (myUniqueId ^ (messageCount.getAndIncrement() * BIG_PRIME)) >>> 1;
        logger.info("Starting {} to topic {}", messageId, topic);
        // first find a good server to talk to
        final Long preferredServer = topicMap.get(topic);
        Collection<CatcherConnection> servers;
        if (preferredServer == null) {
            // unknown topic, pick server at random
            // TODO possibly make this more efficient if it turns out to be common
            List<CatcherConnection> s = Lists.newArrayList(hostConnections.values());
            if (s.size() == 0) {
                logger.error("Found no live catcher connections... will try to reopen");
                connectAll(allKnownServers);
            }
            // balance load when we don't know where the topic goes
            Collections.shuffle(s);
            servers = s;
        } else {

            // we have a preference... now we need to connect to same
            servers = hostConnections.get(preferredServer);
            logger.info("Topic {} to {}", topic, servers);

            // shouldn't happen, but it could depending on other threads
            if (servers == null) {
                servers = Lists.newArrayList(hostConnections.values());
                if (servers.size() == 0) {
                    logger.error("Found no live catcher connections... will try to reopen");
                    connectAll(allKnownServers);
                }
            }
        }
        if (servers.size() == 0 && allConnections.isEmpty()) {
            logger.error("No live catchers even after trying to re-open everything");
            throw new IOException("No catcher servers to connect to");
        }

        // now try to send the message keeping track of errors
        List<CatcherConnection> pendingConnectionRemovals = Lists.newArrayList();

        // note that concatenation here is done lazily.  We add everything to the list to be as persistent as possible.
        Catcher.LogMessage request = Catcher.LogMessage.newBuilder()
                .setTopic(topic)
                        // multiplying by a big prime acts to spread out the bit changes between consecutive messages
                .setClientId(messageId)
                .setPayload(ByteString.copyFrom(message))
                .build();

        // try the first line candidates
        boolean done = false;
        for (CatcherConnection s : servers) {
            done = sendInternal(s, topic, messageId, request, pendingConnectionRemovals);
            if (done) {
                break;
            }
        }
        // TODO if this was a redirect, we should send it there (or do it in sendInternal)

        // if that didn't do the job, try all other servers in random order
        if (!done) {
            // TODO we shouldn't do this in a client redirects world.  SHould just let the retry have it.
            List<CatcherConnection> tmp = Lists.newArrayList(allConnections);
            Collections.shuffle(tmp);
            for (CatcherConnection s : tmp) {
                done = sendInternal(s, topic, messageId, request, pendingConnectionRemovals);
                if (done) {
                    break;
                }
            }
        }

        // remove all connections that cause an error
        if (pendingConnectionRemovals.size() > 0) {
            allConnections.removeAll(pendingConnectionRemovals);
            for (CatcherConnection connection : pendingConnectionRemovals) {
                PeerInfo pi = connection.getServer();
                if (pi != null) {
                    HostPort hostPort = new HostPort(pi);
                    Long serverId = knownServers.get(hostPort);
                    knownServers.remove(hostPort);

                    if (serverId != null) {
                        // forget any topic mappings for this host
                        List<String> toRemove = Lists.newArrayList();
                        for (String t : topicMap.keySet()) {
                            if (topicMap.get(t).equals(serverId)) {
                                toRemove.add(t);
                            }
                        }
                        for (String t : toRemove) {
                            topicMap.remove(t);
                        }
                        hostConnections.remove(serverId, connection);
                    }
                }
                connection.close();
            }
        }
    }

    private boolean sendInternal(CatcherConnection s, String topic, long messageId, Catcher.LogMessage request, List<CatcherConnection> pendingConnectionRemovals) throws IOException {
        Catcher.LogMessageResponse r;
        try {
            r = s.getService().log(s.getController(), request);

            // server responded at least
            if (r.getSuccessful()) {
                logger.info("Success {} to {}", messageId, s.getServer());
                // repeated TopicMapping redirects = 3;
                if (r.hasRedirect()) {
                    // don't natter on about this forever.  It could be we can only see a few hosts
                    if (redirectCount < MAX_REDIRECTS_BEFORE_LIVING_WITH_INDIRECTS) {
                        Catcher.TopicMapping redirect = r.getRedirect();

                        long redirectId = redirect.getServer().getServerId();
                        logger.info("redirect {} to {}", messageId, redirectId);

                        // connect to all possible address of this redirected host
                        List<HostPort> newHosts = Lists.newArrayList();
                        for (Catcher.Host h : redirect.getServer().getHostList()) {
                            newHosts.add(new HostPort(h.getHostName(), h.getPort()));
                        }
                        connectAll(newHosts);

                        // we should now know about this host
                        // if so, cache the topic mapping
                        if (hostConnections.containsKey(redirectId)) {
                            topicMap.put(redirect.getTopic(), redirectId);
                        } else {
                            // if not, things are a bit odd.  Probably due to black-listing or connection errors.
                            logger.warn("Can't find server {} in connection map after redirect on topic {}", redirectId, topic);
                        }
                        redirectCount++;
                    }
                    // TODO send message to redirect server
                } else {
                    Long id = topicMap.get(topic);
                    if (id == null || id != r.getServerId()) {
                        topicMap.put(topic, r.getServerId());
                    }
                    redirectCount = 0;
                }

                // no more retries
                return true;
            } else {
                // server side failure... don't retry this
                IOException e = new IOException(r.getBackTrace());
                logger.warn(String.format("Server side failure %d to %s", messageId, s.getServer()), e);
                throw e;
            }
        } catch (ServiceException e) {
            // request failed for whatever reason.  Retry and remember the problem child
            logger.warn("Failure {} to {}", messageId, s.getServer());
            pendingConnectionRemovals.add(s);
        }
        return false;
    }

    public void close() {
        for (CatcherConnection connection : allConnections) {
            connection.close();
        }
        allConnections.clear();
        topicMap.clear();
        hostConnections.clear();
        knownServers.clear();
        serverBlackList.clear();
    }

    // TODO maybe should do this again against knownServers.keyset() every 30 seconds or so
    private void connectAll(Iterable<HostPort> servers) throws IOException {
        // all of the hosts we have attempted to contact
        Set<HostPort> attempted = Sets.newHashSet();

        // the ones we are going to try in each iteration
        Iterables.addAll(this.allKnownServers, servers);
        Set<HostPort> newServers = Sets.newHashSet(servers);
        while (newServers.size() > 0) {
            // the novel servers that we hear about during this iteration
            Set<HostPort> discovered = Sets.newHashSet();
            Catcher.Hello request = Catcher.Hello.newBuilder()
                    .setClientId(1)
                    .setApplication("test-client")
                    .build();
            for (HostPort server : newServers) {
                if (!attempted.contains(server) && !knownServers.keySet().contains(server)) {
                    try {
                        if (serverBlackList.count(server) < MAX_SERVER_RETRIES_BEFORE_BLACKLISTING) {
                            logger.info("Connecting to {}", server);
                            CatcherConnection s = connector.create(server.asPortInfo());
                            if (s != null) {
                                Catcher.HelloResponse r = s.getService().hello(s.getController(), request);
                                attempted.add(server);

                                hostConnections.put(r.getServerId(), s);
                                allConnections.add(s);
                                knownServers.put(server, r.getServerId());

                                int n = r.getHostCount();
                                for (int i = 0; i < n; i++) {
                                    Catcher.Host host = r.getHost(i);
                                    HostPort pi = new HostPort(host.getHostName(), host.getPort());
                                    logger.info("Discovered host at {}:{}", host.getHostName(), host.getPort());
                                    if (!attempted.contains(pi)) {
                                        logger.info("Unknown host, adding to discovered list.");
                                        discovered.add(pi);
                                    }
                                    attempted.add(server);
                                }
                                for (Catcher.Server otherServer : r.getClusterList()) {
                                    for (Catcher.Host host : otherServer.getHostList()) {
                                        HostPort pi = new HostPort(host.getHostName(), host.getPort());
                                        logger.info("Discovered host at {}:{} via other server", host.getHostName(), host.getPort());
                                        if (!attempted.contains(pi)) {
                                            logger.info("Unknown host, adding to discovered list.");
                                            discovered.add(pi);
                                        }
                                        attempted.add(server);
                                    }
                                }
                            } else {
                                serverBlackList.add(server);
                                logger.warn("Blacklisting {}", server);
                            }
                        }
                    } catch (ServiceException e) {
                        serverBlackList.add(server);
                        logger.warn("Hello failed", e);
                    }
                }
            }
            // this has to be true ... nice to check during testing, though
            assert Sets.intersection(discovered, attempted).size() == 0;
            newServers.clear();
            newServers.addAll(discovered);
        }
    }

    public static class HostPort {
        private String host;
        private int port;

        public HostPort(String host, int port) {
            this.host = host;
            this.port = port;
        }

        public HostPort(PeerInfo server) {
            this(server.getHostName(), server.getPort());
        }

        public String getHost() {
            return host;
        }

        public int getPort() {
            return port;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof HostPort)) return false;

            HostPort hostPort = (HostPort) o;
            return port == hostPort.port && host.equals(hostPort.host);
        }

        @Override
        public int hashCode() {
            int result = host.hashCode();
            result = 31 * result + port;
            return result;
        }

        public PeerInfo asPortInfo() {
            return new PeerInfo(host, port);
        }

        @Override
        public String toString() {
            return "HostPort{" +
                    "host='" + host + '\'' +
                    ", port=" + port +
                    '}';
        }
    }

    public static void main(String[] args) throws ServiceException, IOException {
        Client c = new Client(ImmutableList.of(new PeerInfo("localhost", 8080)));

        c.sendMessage("this", "hello world");

        c.close();
    }

}
TOP

Related Classes of com.mapr.franz.catcher.Client$HostPort

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.