com.meltmedia.dropwizard.etcd.json.EtcdWatchServiceIT.java Source code

Java tutorial

Introduction

Here is the source code for com.meltmedia.dropwizard.etcd.json.EtcdWatchServiceIT.java

Source

/**
 * Copyright (C) 2015 meltmedia (christian.trimble@meltmedia.com)
 *
 * 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.meltmedia.dropwizard.etcd.json;

import static com.meltmedia.dropwizard.etcd.json.EtcdMatchers.atAnyIndex;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.meltmedia.dropwizard.etcd.json.EtcdEvent.Type;
import com.meltmedia.dropwizard.etcd.json.WatchService.Watch;
import com.meltmedia.dropwizard.etcd.junit.EtcdClientRule;

public class EtcdWatchServiceIT {
    public static final String BASE_PATH = "/talu-twitter-streams/it";
    public static final String EXTERNAL_NOISE_BASE_PATH = "/talu-twitter-streams/noise";
    @ClassRule
    public static EtcdClientRule clientRule = new EtcdClientRule("http://127.0.0.1:2379")
            .withMaxFrameSize(1024 * 1000);
    @Rule
    public EtcdWatchServiceRule serviceRule = new EtcdWatchServiceRule(clientRule::getClient, BASE_PATH);
    public static TypeReference<NodeData> NODE_DATA_TYPE = new TypeReference<NodeData>() {
    };
    public static TypeReference<NoiseDocument> NOISE_DATA_TYPE = new TypeReference<NoiseDocument>() {
    };

    public EtcdDirectoryDao<NodeData> jobsDao;
    public EtcdDirectoryDao<NoiseDocument> externalNoiseDao;
    public ObjectMapper mapper;

    @Before
    public void setUp() {
        mapper = new ObjectMapper();

        jobsDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient, BASE_PATH + "/jobs", mapper,
                NODE_DATA_TYPE);

        externalNoiseDao = new EtcdDirectoryDao<NoiseDocument>(clientRule::getClient, EXTERNAL_NOISE_BASE_PATH,
                mapper, NOISE_DATA_TYPE);

    }

    @SuppressWarnings("unchecked")
    @Test
    public void shouldJoinExistingWatch() {
        // add a directory watch.
        WatchService service = serviceRule.getService();

        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        service.registerDirectoryWatch("/jobs", new TypeReference<NodeData>() {
        }, handler);

        // add a document to the directory.
        jobsDao.put("id", new NodeData().withName("id"));

        // verify that we got an event.
        verify(handler, timeout(1000).times(1)).handle(any(EtcdEvent.class));
    }

    @Test
    public void shouldJoinExistingWatchWithLotsOfEvents() throws InterruptedException {
        int eventsCount = 1000;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        Thread events = startNodeDataThread(jobsDao, eventsCount);

        try {
            service.registerDirectoryWatch("/jobs", new TypeReference<NodeData>() {
            }, handler);

            verifySequentialNodeData(handler, eventsCount);
        } finally {
            events.join();
        }
    }

    @Test
    public void shouldWatchMultipleDirectories() throws InterruptedException {
        int dir1Count = 1000;
        int dir2Count = 500;
        int dir3Count = 750;

        // add a directory watch.
        WatchService service = serviceRule.getService();

        EtcdDirectoryDao<NodeData> dir1Dao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir1", mapper, NODE_DATA_TYPE);
        EtcdDirectoryDao<NodeData> dir2Dao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir2", mapper, NODE_DATA_TYPE);
        EtcdDirectoryDao<NodeData> dir3Dao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir3", mapper, NODE_DATA_TYPE);

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler1 = mock(EtcdEventHandler.class);
        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler2 = mock(EtcdEventHandler.class);

        Thread dir1Events = startNodeDataThread(dir1Dao, dir1Count);
        Thread dir2Events = startNodeDataThread(dir2Dao, dir2Count);
        Thread dir3Events = startNodeDataThread(dir3Dao, dir3Count);

        try {
            service.registerDirectoryWatch("/dir1", new TypeReference<NodeData>() {
            }, handler1);
            Thread.sleep(10);
            service.registerDirectoryWatch("/dir2", new TypeReference<NodeData>() {
            }, handler2);

            verifySequentialNodeData(handler1, dir1Count);
            verifySequentialNodeData(handler2, dir2Count);
        } finally {
            dir1Events.join();
            dir2Events.join();
            dir3Events.join();
        }
    }

    @Test
    public void shouldHandleNoiseInSimilarPaths() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);
        EtcdDirectoryDao<NoiseDocument> noiseDao = new EtcdDirectoryDao<NoiseDocument>(clientRule::getClient,
                BASE_PATH + "/directory", mapper, new TypeReference<NoiseDocument>() {
                });

        Thread events = startNodeDataThread(dirDao, eventsCount);
        Thread noise = startNoiseThread(noiseDao, eventsCount);

        try {
            service.registerDirectoryWatch("/dir", new TypeReference<NodeData>() {
            }, handler);

            verifySequentialNodeData(handler, eventsCount);
        } finally {
            events.join();
            noise.join();
        }
    }

    @Test
    public void shouldHandleNoiseInSubPaths() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);
        EtcdDirectoryDao<NoiseDocument> noiseDao = new EtcdDirectoryDao<NoiseDocument>(clientRule::getClient,
                BASE_PATH + "/dir/sub", mapper, new TypeReference<NoiseDocument>() {
                });

        Thread events = startNodeDataThread(dirDao, eventsCount);
        Thread noise = startNoiseThread(noiseDao, eventsCount);

        try {
            service.registerDirectoryWatch("/dir", new TypeReference<NodeData>() {
            }, handler);

            verifySequentialNodeData(handler, eventsCount);
        } finally {
            events.join();
            noise.join();
        }
    }

    @Test
    public void shouldIgnoreEventsInSubPaths() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);
        EtcdDirectoryDao<NodeData> subDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir/sub", mapper, NODE_DATA_TYPE);

        Thread events = startNodeDataThread(dirDao, eventsCount);
        Thread noise = startNodeDataThread(subDao, eventsCount);

        try {
            service.registerDirectoryWatch("/dir", new TypeReference<NodeData>() {
            }, handler);

            verifySequentialNodeData(handler, eventsCount);
        } finally {
            events.join();
            noise.join();
        }
    }

    @SuppressWarnings("unchecked")
    @Test
    public void shouldWatchSingleFile() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);
        Thread events = startNodeDataThread(dirDao, eventsCount);

        try {
            service.registerValueWatch("/dir", "10", new TypeReference<NodeData>() {
            }, handler);

            verify(handler, timeout(10000)).handle(atAnyIndex(EtcdEvent.<NodeData>builder().withKey("10")
                    .withType(EtcdEvent.Type.added).withValue(new NodeData().withName("10")).build()));

            verify(handler, times(1)).handle(any(EtcdEvent.class));

        } finally {
            events.join();
        }
    }

    @SuppressWarnings("unchecked")
    @Test
    public void shouldWatchSingleFileWithNoise() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);

        startNoiseThread(externalNoiseDao, 4000).join();

        Thread events = startNodeDataThread(dirDao, eventsCount);

        try {
            service.registerValueWatch("/dir", "10", new TypeReference<NodeData>() {
            }, handler);

            verify(handler, timeout(10000)).handle(atAnyIndex(EtcdEvent.<NodeData>builder().withKey("10")
                    .withType(EtcdEvent.Type.added).withValue(new NodeData().withName("10")).build()));

            verify(handler, times(1)).handle(any(EtcdEvent.class));

        } finally {
            events.join();
        }
    }

    @Test
    public void shouldWatchSingleFileWithNoiseAndTimeout() throws InterruptedException {
        int eventsCount = 100;
        // add a directory watch.
        WatchService service = serviceRule.getService();

        CountDownLatch latch = new CountDownLatch(eventsCount);
        EtcdEventHandler<NodeData> handler = (event) -> {
            latch.countDown();
        };

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);

        service.registerDirectoryWatch("/dir", NODE_DATA_TYPE, handler);

        Thread noiseThread = startNoiseThread(externalNoiseDao, 4000);
        Thread waitThread = startWaitThread(1, TimeUnit.SECONDS);

        noiseThread.join();
        waitThread.join();

        startNodeDataThread(dirDao, eventsCount).join();

        if (!latch.await(10, TimeUnit.SECONDS)) {
            throw new IllegalStateException("could not catch up with state.");
        }
    }

    @Test
    public void shouldPublishEventsForUpdate() {
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);

        dirDao.resetDirectory();

        Watch watch = service.registerDirectoryWatch("/dir", new TypeReference<NodeData>() {
        }, handler);

        long originalIndex = dirDao.put("id", new NodeData().withName("original"));

        verify(handler, timeout(1000)).handle(EtcdEvent.<NodeData>builder().withKey("id").withType(Type.added)
                .withValue(new NodeData().withName("original")).withIndex(originalIndex).build());

        long updateIndex = dirDao.update("id", n -> "original".equals(n.getName()), n -> n.withName("updated"));

        verify(handler, timeout(1000)).handle(EtcdEvent.<NodeData>builder().withKey("id").withType(Type.updated)
                .withValue(new NodeData().withName("updated")).withPrevValue(new NodeData().withName("original"))
                .withIndex(updateIndex).build());

        watch.stop();
    }

    @Test
    public void startWatchOnLargeDirectory() {
        WatchService service = serviceRule.getService();

        @SuppressWarnings("unchecked")
        EtcdEventHandler<NodeData> handler = mock(EtcdEventHandler.class);

        EtcdDirectoryDao<NodeData> dirDao = new EtcdDirectoryDao<NodeData>(clientRule::getClient,
                BASE_PATH + "/dir", mapper, NODE_DATA_TYPE);

        dirDao.resetDirectory();

        for (int i = 0; i < 1000; i++) {
            dirDao.put("key" + i, new NodeData().withName("name" + i));
        }

        Watch watch = service.registerDirectoryWatch("/dir", new TypeReference<NodeData>() {
        }, handler);

        assertThat(watch.inSync(), equalTo(true));
        assertThat(service.outOfSyncWatchers().isEmpty(), equalTo(true));

        watch.stop();
    }

    public static Thread startNodeDataThread(EtcdDirectoryDao<NodeData> dao, int count) {
        Thread events = new Thread(() -> {
            for (int i = 0; i < count; i++) {
                dao.put(String.valueOf(i), new NodeData().withName(String.valueOf(i)));
            }
        });
        events.start();
        return events;
    }

    public static Thread startNoiseThread(EtcdDirectoryDao<NoiseDocument> dao, int count) {
        Random random = new Random();
        Thread events = new Thread(() -> {
            for (int i = 0; i < count; i++) {
                switch (random.nextInt(4)) {
                case 0:
                case 1:
                case 2:
                    dao.put("noise_" + String.valueOf(i), new NoiseDocument().withNoise(String.valueOf(i)));
                    break;
                case 3:
                    dao.putDir("/noise_" + String.valueOf(i));
                    break;
                }
            }
        });
        events.start();
        return events;
    }

    public static Thread startWaitThread(long timeout, TimeUnit unit) {
        Thread waitThread = new Thread(() -> {
            try {
                unit.sleep(timeout);
            } catch (Exception e) {
                // oh well!
            }
        });
        waitThread.start();
        return waitThread;
    }

    public static void verifySequentialNodeData(EtcdEventHandler<NodeData> handler, int count) {
        for (int i = 0; i < count; i++) {
            verify(handler, timeout(10000)).handle(atAnyIndex(
                    EtcdEvent.<NodeData>builder().withKey(String.valueOf(i)).withType(EtcdEvent.Type.added)
                            .withValue(new NodeData().withName(String.valueOf(i))).build()));
        }
    }

    public static class NodeData {
        protected String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public NodeData withName(String name) {
            this.name = name;
            return this;
        }

        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }

        public boolean equals(Object o) {
            return EqualsBuilder.reflectionEquals(this, o);
        }
    }

    public static class NoiseDocument {
        protected String noise;

        public String getNoise() {
            return noise;
        }

        public void setNoise(String noise) {
            this.noise = noise;
        }

        public NoiseDocument withNoise(String noise) {
            this.noise = noise;
            return this;
        }

        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }

        public boolean equals(Object o) {
            return EqualsBuilder.reflectionEquals(this, o);
        }

    }
}