Java tutorial
/** * 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); } } }