Package com.mapr.franz.catcher

Source Code of com.mapr.franz.catcher.ClientTest$FarmedServer

/*
* Copyright MapR Technologies, $year
*
* 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.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.RpcController;
import com.google.protobuf.ServiceException;
import com.googlecode.protobuf.pro.duplex.PeerInfo;
import com.mapr.franz.catcher.wire.Catcher;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.junit.Assert.*;

public class ClientTest {
    // check for failure if we can't get to any server
    @Test
    public void noServer() throws ServiceException, IOException {
        Client c = new Client(new ConnectionFactory() {
            @Override
            public CatcherConnection create(PeerInfo server) {
                return null;
            }
        }, Lists.newArrayList(new PeerInfo("foo", 0)));
        try {
            c.sendMessage("topic", "message");
            fail("Should have failed with IOException");
        } catch (IOException e) {
            assertTrue(e.getMessage().contains("No catcher servers"));
        }
    }

    // check that we try a second time and that we get a good result the second time
    @Test
    public void oneServerRetry() throws ServiceException, IOException {
        final ServerFarm farm = new ServerFarm();
//        new MockUp<CatcherConnection>() {
//            CatcherConnection it;
//
//            @Mock(maxInvocations = 10)
//            public void $init(PeerInfo host) throws IOException {
//                it.setServer(host);
//            }
//
//            @Mock
//            public Catcher.CatcherService.BlockingInterface getService() {
//                return new FarmedServer(new Client.HostPort(new PeerInfo("foo", 123)), new SecureRandom().nextLong(), farm);
//            }
//
//            @Mock
//            public String toString() {
//                return "MockConnection(" + it.getServer() + ")";
//            }
//        };

        Client c = new Client(new ConnectionFactory() {
            int retry = 0;

            @Override
            public CatcherConnection create(PeerInfo server) throws IOException {
                if (retry++ == 0) {
                    return null;
                } else {
                    return new FakeConnection(server, farm);
                }
            }
        }, Lists.newArrayList(new PeerInfo("foo", 0)));

        c.sendMessage("3", "message");
        assertEquals(1, farm.getMessages().size());
        assertEquals("message", farm.getMessages().get(0));
    }

    // verifies redirects are remembered by the client
    // also verifies that server failures are dealt with only a few redirects
    // also verifies that overall transaction counts are evenly distributed
    @Test
    public void redirects() throws ServiceException, IOException {
        final ServerFarm farm = new ServerFarm();
        final Map<CatcherConnection, FarmedServer> servermap = Maps.newHashMap();
        final Map<CatcherConnection, PeerInfo> hostmap = Maps.newHashMap();

//        new MockUp<CatcherConnection>() {
//            CatcherConnection it;
//
//            @Mock(maxInvocations = 10)
//            public void $init(PeerInfo host) throws IOException {
//                servermap.put(it, farm.newServer(new Client.HostPort(host)));
//                hostmap.put(it, host);
//                it.setServer(host);
//            }
//
//            @Mock
//            public Catcher.CatcherService.BlockingInterface getService() {
//                return servermap.get(it);
//            }
//
//            @Mock
//            public String toString() {
//                return "MockConnection(" + hostmap.get(it) + ")";
//            }
//        };

        List<PeerInfo> hosts = Lists.newArrayList();
        for (int i = 0; i < 10; i++) {
            hosts.add(new PeerInfo(Integer.toString(i), 100));
        }

        Client c = new Client(new ConnectionFactory() {
            @Override
            public CatcherConnection create(final PeerInfo server) throws IOException {
                return new FakeConnection(server, farm);
            }
        }, hosts);

        for (int i = 0; i < 600; i++) {
            int topic = i % 30;
            c.sendMessage(Integer.toString(topic), "message " + i);
        }

        assertEquals(600, countMessages(farm.servers));
        assertEquals(600, farm.messageCount);
        int firstPassRedirects = farm.redirectCount;
        assertTrue("Should have few redirects, got " + firstPassRedirects, firstPassRedirects <= 30);
        assertEquals(10, farm.helloCount);

        // assert rough balance of traffic
        for (FarmedServer server : farm.servers) {
            assertEquals(600 / 10, server.getMessageCount(), 15);
        }

        // kill 4 servers.  That leaves 6 which still divides 30 topics evenly.
        List<FarmedServer> dead = Lists.newArrayList();
        dead.addAll(farm.servers.subList(6, 10));
        farm.servers = farm.servers.subList(0, 6);
        int deadCount = countMessages(dead);
        for (FarmedServer server : dead) {
            server.emulateServerFailure();
        }

        for (int i = 0; i < 600; i++) {
            int topic = i % 30;
            c.sendMessage(Integer.toString(topic), "message " + i);
        }

        assertEquals(2 * 600, farm.messageCount);
        assertEquals(deadCount, countMessages(dead));
        assertEquals(2 * 600, countMessages(Iterables.concat(farm.servers, dead)));

        // and again, should have rough balance.
        for (FarmedServer server : farm.servers) {
            assertEquals(600 / 10 + 600 / 6, server.getMessageCount(), 24);
        }

        assertEquals(2 * 600, farm.messageCount);
        assertTrue("Should have few redirects", farm.redirectCount - firstPassRedirects <= 30);
        assertEquals(10, farm.helloCount);

        c.close();
    }

    // TODO add test to see how new hosts discovered during hello are handled.

    private int countMessages(Iterable<FarmedServer> servers) {
        int count = 0;
        for (FarmedServer server : servers) {
            count += server.getMessageCount();
        }
        return count;
    }

    private static class FakeConnection implements CatcherConnection {

        private final FarmedServer service;
        private final PeerInfo server;

        private FakeConnection(PeerInfo server, ServerFarm farm) throws IOException {
            service = farm.newServer(new Client.HostPort(server));
            this.server = server;
        }

        public Catcher.CatcherService.BlockingInterface getService() {
            return service;
        }

        @Override
        public RpcController getController() {
            return null;
        }

        @Override
        public PeerInfo getServer() {
            return server;
        }

        @Override
        public void close() {
            // ignore
        }

        @Override
        public void setServer(PeerInfo host) {
            throw new RuntimeException("Shouldn't call this");
        }

        public String toString() {
            return "MockConnection(" + server + ")";
        }
    }


    /**
     * Keeps track of a bunch of FarmedServer's and the associated transaction counts.
     */
    private static class ServerFarm implements Iterable<FarmedServer> {
        private Logger logger = LoggerFactory.getLogger(String.class);

        private List<FarmedServer> servers = Lists.newArrayList();
        private int redirectCount = 0;
        private int messageCount = 0;
        private int helloCount = 0;
        final List<String> messages = Lists.newArrayList();
        private Set<Client.HostPort> deadHosts = Sets.newHashSet();

        public FarmedServer newServer(Client.HostPort hostPort) throws IOException {
            if (!deadHosts.contains(hostPort)) {
                FarmedServer r = new FarmedServer(hostPort, servers.size(), this);
                servers.add(r);
                return r;
            } else {
                throw new IOException("Connection refused :-)");
            }
        }

        public int size() {
            return servers.size();
        }

        public void notifyHello(long id) {
            logger.debug("Hello {}", id);
            helloCount++;
        }

        public void notifyClose(long id) {
            logger.debug("Closing {}", id);
        }

        public void notifyMessage(long id, String topic, String message) {
            logger.debug("Message received at {} on topic {}", id, topic);
            messages.add(message);
            messageCount++;
        }

        public void notifyRedirect(long from, long to) {
            logger.debug("Redirect from {} to {}", from, to);
            redirectCount++;
        }

        @Override
        public Iterator<FarmedServer> iterator() {
            return servers.iterator();
        }

        public List<String> getMessages() {
            return messages;
        }

        public void recordDeadHost(Client.HostPort hostPort) {
            deadHosts.add(hostPort);
        }
    }

    /**
     * Emulates minimal server function and allows emulation of failures.
     */
    private static class FarmedServer implements Catcher.CatcherService.BlockingInterface {
        protected int helloCount = 0;
        protected int redirectCount = 0;
        protected int messageCount = 0;
        private Client.HostPort hostPort;
        private final long id;
        private final ServerFarm farm;
        private boolean isDead = false;

        private FarmedServer(Client.HostPort hostPort, long id, ServerFarm farm) {
            this.hostPort = hostPort;
            this.id = id;
            this.farm = farm;
        }

        @Override
        public Catcher.HelloResponse hello(RpcController controller, Catcher.Hello request) throws ServiceException {
            checkForFailure();
            helloCount++;
            if (farm != null) {
                farm.notifyHello(id);
            }

            Catcher.HelloResponse.Builder r = Catcher.HelloResponse
                    .newBuilder().setServerId(id);

            for (FarmedServer server : farm) {
                Catcher.Server.Builder s = r.addClusterBuilder();
                s.setServerId(server.getId());
                s.addHostBuilder().setHostName(server.getHost()).setPort(server.getPort()).build();
                s.build();
            }
            return r.build();
        }

        @Override
        public Catcher.LogMessageResponse log(RpcController controller, Catcher.LogMessage request) throws ServiceException {
            Preconditions.checkArgument(request.hasPayload());
            checkForFailure();


            String topic = request.getTopic();
            farm.notifyMessage(id, topic, request.getPayload().toStringUtf8());

            int topicNumber = Integer.parseInt(topic);
            messageCount++;
            Catcher.LogMessageResponse.Builder r = Catcher.LogMessageResponse.newBuilder()
                    .setServerId(id)
                    .setSuccessful(true);

            if (farm.size() != 0) {
                FarmedServer redirectServer = farm.servers.get(topicNumber % farm.size());
                long redirectTo = redirectServer.getId();
                if (redirectTo != getId()) {
                    redirectCount++;
                    getFarm().notifyRedirect(getId(), redirectTo);
                    Catcher.TopicMapping.Builder redirect = r.getRedirectBuilder();
                    redirect.setTopic(Integer.toString(topicNumber));
                    Catcher.Server.Builder server = redirect.getServerBuilder().setServerId(redirectTo);
                    server.addHostBuilder()
                            .setHostName(redirectServer.getHost())
                            .setPort(redirectServer.getPort())
                            .build();
                    server.build();
                }
            }
            return r.build();
        }

        @Override
        public Catcher.CloseResponse close(RpcController controller, Catcher.Close request) throws ServiceException {
            checkForFailure();
            farm.notifyClose(id);
            return Catcher.CloseResponse.newBuilder().setServerId(id).build();
        }

        public ServerFarm getFarm() {
            return farm;
        }

        public long getId() {
            return id;
        }

        public void checkForFailure() throws ServiceException {
            if (isDead) {
                throw new ServiceException("Simulated server shutdown");
            }
        }

        public void emulateServerFailure() {
            farm.recordDeadHost(hostPort);
            isDead = true;
        }

        public String getHost() {
            return hostPort.getHost();
        }

        public int getPort() {
            return hostPort.getPort();
        }

        public int getMessageCount() {
            return messageCount;
        }
    }

    // TODO two server topic redirects work correctly (two servers, 100 requests on 10 topics, each topic has at most 1 request to wrong server)
    // TODO three server connect, cluster shrinks, send remaining topics to remaining nodes, minimal redirects
    // TODO three server connect, cluster shrinks, then expands, minimal redirects
}
TOP

Related Classes of com.mapr.franz.catcher.ClientTest$FarmedServer

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.