/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.data.stream.service; import co.cask.cdap.api.metrics.MetricsCollectionService; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.guice.ConfigModule; import co.cask.cdap.common.guice.DiscoveryRuntimeModule; import co.cask.cdap.common.guice.LocationRuntimeModule; import co.cask.cdap.common.guice.ZKClientModule; import co.cask.cdap.common.io.Locations; import co.cask.cdap.common.metrics.NoOpMetricsCollectionService; import co.cask.cdap.common.namespace.AbstractNamespaceClient; import co.cask.cdap.common.namespace.InMemoryNamespaceClient; import co.cask.cdap.common.namespace.NamespacedLocationFactory; import co.cask.cdap.common.utils.Tasks; import co.cask.cdap.data.runtime.DataFabricModules; import co.cask.cdap.data.runtime.DataSetServiceModules; import co.cask.cdap.data.runtime.DataSetsModules; import co.cask.cdap.data.runtime.LocationStreamFileWriterFactory; import co.cask.cdap.data.stream.StreamAdminModules; import co.cask.cdap.data.stream.StreamFileWriterFactory; import co.cask.cdap.data.stream.service.heartbeat.HeartbeatPublisher; import co.cask.cdap.data.stream.service.heartbeat.StreamWriterHeartbeat; import co.cask.cdap.data.view.ViewAdminModules; import co.cask.cdap.data2.datafabric.dataset.service.DatasetService; import co.cask.cdap.data2.transaction.stream.FileStreamAdmin; import co.cask.cdap.data2.transaction.stream.StreamAdmin; import co.cask.cdap.data2.transaction.stream.StreamConsumerFactory; import co.cask.cdap.data2.transaction.stream.StreamConsumerStateStoreFactory; import co.cask.cdap.data2.transaction.stream.leveldb.LevelDBStreamConsumerStateStoreFactory; import co.cask.cdap.data2.transaction.stream.leveldb.LevelDBStreamFileConsumerFactory; import co.cask.cdap.explore.guice.ExploreClientModule; import co.cask.cdap.metrics.guice.MetricsClientRuntimeModule; import co.cask.cdap.notifications.feeds.guice.NotificationFeedServiceRuntimeModule; import co.cask.cdap.notifications.guice.NotificationServiceRuntimeModule; import co.cask.cdap.notifications.service.NotificationService; import co.cask.cdap.proto.Id; import co.cask.tephra.TransactionManager; import com.google.common.util.concurrent.AbstractIdleService; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Scopes; import com.google.inject.Singleton; import com.google.inject.util.Modules; import org.apache.hadoop.conf.Configuration; import org.apache.twill.internal.zookeeper.InMemoryZKServer; import org.apache.twill.zookeeper.ZKClientService; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nullable; /** * */ public class DFSStreamHeartbeatsTest { private static final byte[] TWO_BYTES = new byte[] { 'a', 'b' }; private static String hostname; private static int port; private static StreamHttpService streamHttpService; private static StreamService streamService; private static TransactionManager txManager; private static DatasetService datasetService; private static NotificationService notificationService; private static MockHeartbeatPublisher heartbeatPublisher; private static InMemoryZKServer zkServer; private static ZKClientService zkClient; private static StreamAdmin streamAdmin; private static NamespacedLocationFactory namespacedLocationFactory; @ClassRule public static final TemporaryFolder TEMP_FOLDER = new TemporaryFolder(); @BeforeClass public static void beforeClass() throws IOException { zkServer = InMemoryZKServer.builder().setDataDir(TEMP_FOLDER.newFolder()).build(); zkServer.startAndWait(); CConfiguration cConf = CConfiguration.create(); cConf.set(Constants.Zookeeper.QUORUM, zkServer.getConnectionStr()); cConf.setInt(Constants.Stream.CONTAINER_INSTANCE_ID, 0); cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMP_FOLDER.newFolder().getAbsolutePath()); Injector injector = Guice.createInjector( Modules.override( new ZKClientModule(), new DataFabricModules().getInMemoryModules(), new ConfigModule(cConf, new Configuration()), new DiscoveryRuntimeModule().getInMemoryModules(), new LocationRuntimeModule().getInMemoryModules(), new ExploreClientModule(), new DataSetServiceModules().getInMemoryModules(), new DataSetsModules().getStandaloneModules(), new NotificationFeedServiceRuntimeModule().getInMemoryModules(), new NotificationServiceRuntimeModule().getInMemoryModules(), new MetricsClientRuntimeModule().getInMemoryModules(), new ViewAdminModules().getInMemoryModules(), // We need the distributed modules here to get the distributed stream service, which is the only one // that performs heartbeats aggregation new StreamServiceRuntimeModule().getDistributedModules(), new StreamAdminModules().getInMemoryModules()).with(new AbstractModule() { @Override protected void configure() { bind(MetricsCollectionService.class).to(NoOpMetricsCollectionService.class); bind(AbstractNamespaceClient.class).to(InMemoryNamespaceClient.class); bind(StreamConsumerStateStoreFactory.class).to(LevelDBStreamConsumerStateStoreFactory.class) .in(Singleton.class); bind(StreamAdmin.class).to(FileStreamAdmin.class).in(Singleton.class); bind(StreamConsumerFactory.class).to(LevelDBStreamFileConsumerFactory.class).in(Singleton.class); bind(StreamFileWriterFactory.class).to(LocationStreamFileWriterFactory.class).in(Singleton.class); bind(StreamFileJanitorService.class).to(LocalStreamFileJanitorService.class).in(Scopes.SINGLETON); bind(StreamMetaStore.class).to(InMemoryStreamMetaStore.class).in(Scopes.SINGLETON); bind(HeartbeatPublisher.class).to(MockHeartbeatPublisher.class).in(Scopes.SINGLETON); } })); zkClient = injector.getInstance(ZKClientService.class); txManager = injector.getInstance(TransactionManager.class); datasetService = injector.getInstance(DatasetService.class); notificationService = injector.getInstance(NotificationService.class); streamHttpService = injector.getInstance(StreamHttpService.class); streamService = injector.getInstance(StreamService.class); heartbeatPublisher = (MockHeartbeatPublisher) injector.getInstance(HeartbeatPublisher.class); namespacedLocationFactory = injector.getInstance(NamespacedLocationFactory.class); streamAdmin = injector.getInstance(StreamAdmin.class); zkClient.startAndWait(); txManager.startAndWait(); datasetService.startAndWait(); notificationService.startAndWait(); streamHttpService.startAndWait(); streamService.startAndWait(); hostname = streamHttpService.getBindAddress().getHostName(); port = streamHttpService.getBindAddress().getPort(); Locations.mkdirsIfNotExists(namespacedLocationFactory.get(Id.Namespace.DEFAULT)); } @AfterClass public static void afterClass() throws IOException { Locations.deleteQuietly(namespacedLocationFactory.get(Id.Namespace.DEFAULT), true); notificationService.startAndWait(); datasetService.startAndWait(); txManager.startAndWait(); streamService.stopAndWait(); streamHttpService.stopAndWait(); zkClient.stopAndWait(); zkServer.stopAndWait(); } private HttpURLConnection openURL(String location, HttpMethod method) throws IOException { URL url = new URL(location); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); urlConn.setRequestMethod(method.getName()); return urlConn; } @Test public void streamPublishesHeartbeatTest() throws Exception { final int entries = 10; final String streamName = "test_stream"; final Id.Stream streamId = Id.Stream.from(Id.Namespace.DEFAULT, streamName); // Create a new stream. streamAdmin.create(streamId); // Enqueue 10 entries for (int i = 0; i < entries; ++i) { HttpURLConnection urlConn = openURL(String.format("http://%s:%d/v3/namespaces/default/streams/%s", hostname, port, streamName), HttpMethod.POST); urlConn.setDoOutput(true); urlConn.addRequestProperty("test_stream1.header1", Integer.toString(i)); urlConn.getOutputStream().write(TWO_BYTES); Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode()); urlConn.disconnect(); } Tasks.waitFor((long) entries * TWO_BYTES.length, new Callable<Long>() { @Override public Long call() throws Exception { return getStreamSize(streamId, heartbeatPublisher); } }, Constants.Stream.HEARTBEAT_INTERVAL * 5, TimeUnit.SECONDS, 100, TimeUnit.MILLISECONDS); } private long getStreamSize(Id.Stream streamId, MockHeartbeatPublisher heartbeatPublisher) { StreamWriterHeartbeat heartbeat = heartbeatPublisher.getHeartbeat(); if (heartbeat == null) { return 0L; } Long size = heartbeat.getStreamsSizes().get(streamId); return size == null ? 0L : size; } /** * Mock heartbeat publisher that allows to do assertions on the heartbeats being published. */ private static final class MockHeartbeatPublisher extends AbstractIdleService implements HeartbeatPublisher { private static final Logger LOG = LoggerFactory.getLogger(MockHeartbeatPublisher.class); private final AtomicReference<StreamWriterHeartbeat> heartbeat = new AtomicReference<>(); @Override protected void startUp() throws Exception { LOG.info("Starting Publisher."); } @Override protected void shutDown() throws Exception { LOG.info("Stopping Publisher."); } @Override public ListenableFuture<StreamWriterHeartbeat> sendHeartbeat(StreamWriterHeartbeat heartbeat) { LOG.info("Received heartbeat {} for Streams {}", heartbeat, heartbeat.getStreamsSizes().keySet()); this.heartbeat.set(heartbeat); return Futures.immediateFuture(heartbeat); } @Nullable public StreamWriterHeartbeat getHeartbeat() { return heartbeat.get(); } } }