/* * Copyright © 2014-2016 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.explore.service; import co.cask.cdap.api.data.schema.Schema; 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.IOModule; import co.cask.cdap.common.guice.LocationRuntimeModule; import co.cask.cdap.common.namespace.guice.NamespaceClientRuntimeModule; 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.stream.StreamAdminModules; import co.cask.cdap.data.stream.StreamViewHttpHandler; import co.cask.cdap.data.stream.service.StreamFetchHandler; import co.cask.cdap.data.stream.service.StreamHandler; import co.cask.cdap.data.stream.service.StreamHttpService; import co.cask.cdap.data.stream.service.StreamMetaStore; import co.cask.cdap.data.stream.service.StreamService; import co.cask.cdap.data.stream.service.StreamServiceRuntimeModule; import co.cask.cdap.data.view.ViewAdminModules; import co.cask.cdap.data2.datafabric.dataset.service.DatasetService; import co.cask.cdap.data2.datafabric.dataset.service.executor.DatasetOpExecutor; import co.cask.cdap.data2.dataset2.DatasetFramework; import co.cask.cdap.data2.transaction.stream.StreamAdmin; import co.cask.cdap.explore.client.ExploreClient; import co.cask.cdap.explore.client.ExploreExecutionResult; import co.cask.cdap.explore.executor.ExploreExecutorService; import co.cask.cdap.explore.guice.ExploreClientModule; import co.cask.cdap.explore.guice.ExploreRuntimeModule; import co.cask.cdap.gateway.handlers.CommonHandlers; import co.cask.cdap.internal.io.SchemaTypeAdapter; import co.cask.cdap.metrics.guice.MetricsClientRuntimeModule; import co.cask.cdap.notifications.feeds.NotificationFeedManager; import co.cask.cdap.notifications.feeds.service.NoOpNotificationFeedManager; import co.cask.cdap.notifications.guice.NotificationServiceRuntimeModule; import co.cask.cdap.notifications.service.NotificationService; import co.cask.cdap.proto.ColumnDesc; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.NamespaceMeta; import co.cask.cdap.proto.QueryHandle; import co.cask.cdap.proto.QueryResult; import co.cask.cdap.proto.QueryStatus; import co.cask.cdap.proto.StreamProperties; import co.cask.cdap.store.NamespaceStore; import co.cask.cdap.store.guice.NamespaceStoreModule; import co.cask.http.HttpHandler; import co.cask.tephra.TransactionManager; import co.cask.tephra.TxConstants; import co.cask.tephra.persist.LocalFileTransactionStateStorage; import co.cask.tephra.persist.TransactionStateStorage; import co.cask.tephra.runtime.TransactionStateStorageProvider; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Scopes; import com.google.inject.Singleton; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import com.google.inject.util.Modules; import org.apache.hadoop.conf.Configuration; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.junit.AfterClass; import org.junit.Assert; import org.junit.rules.TemporaryFolder; import java.io.File; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.sql.SQLException; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.ws.rs.HttpMethod; /** * Base class for tests that need explore service to be running. */ public class BaseHiveExploreServiceTest { private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Schema.class, new SchemaTypeAdapter()) .create(); protected static final Id.Namespace NAMESPACE_ID = Id.Namespace.from("namespace"); protected static final Id.Namespace OTHER_NAMESPACE_ID = Id.Namespace.from("other"); protected static final String DEFAULT_DATABASE = "default"; protected static final String NAMESPACE_DATABASE = "cdap_namespace"; protected static final String OTHER_NAMESPACE_DATABASE = "cdap_other"; protected static final Id.DatasetModule KEY_STRUCT_VALUE = Id.DatasetModule.from(NAMESPACE_ID, "keyStructValue"); protected static final Id.DatasetInstance MY_TABLE = Id.DatasetInstance.from(NAMESPACE_ID, "my_table"); protected static final String MY_TABLE_NAME = getDatasetHiveName(MY_TABLE); protected static final Id.DatasetModule OTHER_KEY_STRUCT_VALUE = Id.DatasetModule.from(OTHER_NAMESPACE_ID, "keyStructValue"); protected static final Id.DatasetInstance OTHER_MY_TABLE = Id.DatasetInstance.from(OTHER_NAMESPACE_ID, "my_table"); protected static final String OTHER_MY_TABLE_NAME = getDatasetHiveName(OTHER_MY_TABLE); // Controls for test suite for whether to run BeforeClass/AfterClass // Make sure to reset it back to true after using it in a test class public static boolean runBefore = true; public static boolean runAfter = true; protected static TransactionManager transactionManager; protected static DatasetFramework datasetFramework; protected static DatasetOpExecutor dsOpService; protected static DatasetService datasetService; protected static ExploreExecutorService exploreExecutorService; protected static ExploreService exploreService; protected static NotificationService notificationService; protected static StreamHttpService streamHttpService; protected static StreamService streamService; protected static ExploreClient exploreClient; protected static ExploreTableManager exploreTableManager; private static StreamAdmin streamAdmin; private static StreamMetaStore streamMetaStore; private static NamespaceStore namespaceStore; protected static Injector injector; protected static void initialize(TemporaryFolder tmpFolder) throws Exception { initialize(CConfiguration.create(), tmpFolder); } protected static void initialize(CConfiguration cConf, TemporaryFolder tmpFolder) throws Exception { initialize(cConf, tmpFolder, false); } protected static void initialize(CConfiguration cConf, TemporaryFolder tmpFolder, boolean useStandalone) throws Exception { if (!runBefore) { return; } Configuration hConf = new Configuration(); List<Module> modules = useStandalone ? createStandaloneModules(cConf, hConf, tmpFolder) : createInMemoryModules(cConf, hConf, tmpFolder); injector = Guice.createInjector(modules); transactionManager = injector.getInstance(TransactionManager.class); transactionManager.startAndWait(); dsOpService = injector.getInstance(DatasetOpExecutor.class); dsOpService.startAndWait(); datasetService = injector.getInstance(DatasetService.class); datasetService.startAndWait(); exploreExecutorService = injector.getInstance(ExploreExecutorService.class); exploreExecutorService.startAndWait(); datasetFramework = injector.getInstance(DatasetFramework.class); exploreClient = injector.getInstance(ExploreClient.class); exploreService = injector.getInstance(ExploreService.class); Assert.assertTrue(exploreClient.isServiceAvailable()); notificationService = injector.getInstance(NotificationService.class); notificationService.startAndWait(); streamService = injector.getInstance(StreamService.class); streamService.startAndWait(); streamHttpService = injector.getInstance(StreamHttpService.class); streamHttpService.startAndWait(); exploreTableManager = injector.getInstance(ExploreTableManager.class); streamAdmin = injector.getInstance(StreamAdmin.class); streamMetaStore = injector.getInstance(StreamMetaStore.class); namespaceStore = injector.getInstance(NamespaceStore.class); // create namespaces namespaceStore.create(new NamespaceMeta.Builder().setName(Id.Namespace.DEFAULT).build()); namespaceStore.create(new NamespaceMeta.Builder().setName(NAMESPACE_ID).build()); namespaceStore.create(new NamespaceMeta.Builder().setName(OTHER_NAMESPACE_ID).build()); // This happens when you create a namespace via REST APIs. However, since we do not start AppFabricServer in // Explore tests, simulating that scenario by explicitly calling DatasetFramework APIs. datasetFramework.createNamespace(Id.Namespace.DEFAULT); datasetFramework.createNamespace(NAMESPACE_ID); datasetFramework.createNamespace(OTHER_NAMESPACE_ID); } @AfterClass public static void stopServices() throws Exception { if (!runAfter) { return; } // Delete namespaces namespaceStore.delete(Id.Namespace.DEFAULT); namespaceStore.delete(NAMESPACE_ID); namespaceStore.delete(OTHER_NAMESPACE_ID); datasetFramework.deleteNamespace(Id.Namespace.DEFAULT); datasetFramework.deleteNamespace(NAMESPACE_ID); datasetFramework.deleteNamespace(OTHER_NAMESPACE_ID); streamHttpService.stopAndWait(); streamService.stopAndWait(); notificationService.stopAndWait(); exploreClient.close(); exploreExecutorService.stopAndWait(); datasetService.stopAndWait(); dsOpService.stopAndWait(); transactionManager.stopAndWait(); } protected static String getDatasetHiveName(Id.DatasetInstance datasetID) { return "dataset_" + datasetID.getId().replaceAll("\\.", "_").replaceAll("-", "_"); } protected static ExploreClient getExploreClient() { return exploreClient; } protected static QueryStatus waitForCompletionStatus(QueryHandle handle, long sleepTime, TimeUnit timeUnit, int maxTries) throws ExploreException, HandleNotFoundException, InterruptedException, SQLException { QueryStatus status; int tries = 0; do { timeUnit.sleep(sleepTime); status = exploreService.getStatus(handle); if (++tries > maxTries) { break; } } while (!status.getStatus().isDone()); return status; } protected static void runCommand(Id.Namespace namespace, String command, boolean expectedHasResult, List<ColumnDesc> expectedColumnDescs, List<QueryResult> expectedResults) throws Exception { ListenableFuture<ExploreExecutionResult> future = exploreClient.submit(namespace, command); assertStatementResult(future, expectedHasResult, expectedColumnDescs, expectedResults); } protected static void assertStatementResult(ListenableFuture<ExploreExecutionResult> future, boolean expectedHasResult, List<ColumnDesc> expectedColumnDescs, List<QueryResult> expectedResults) throws Exception { ExploreExecutionResult results = future.get(); Assert.assertEquals(expectedHasResult, results.hasNext()); Assert.assertEquals(expectedColumnDescs, results.getResultSchema()); Assert.assertEquals(expectedResults, trimColumnValues(results)); results.close(); } protected static List<QueryResult> trimColumnValues(Iterator<QueryResult> results) { int i = 0; List<QueryResult> newResults = Lists.newArrayList(); // Max 100 results while (results.hasNext() && i < 100) { i++; QueryResult result = results.next(); List<Object> newCols = Lists.newArrayList(); for (Object obj : result.getColumns()) { if (obj instanceof String) { newCols.add(((String) obj).trim()); } else if (obj instanceof Double) { // NOTE: this means only use 4 decimals for double and float values in test cases newCols.add((double) Math.round((Double) obj * 10000) / 10000); } else { newCols.add(obj); } } newResults.add(new QueryResult(newCols)); } return newResults; } protected static void createStream(Id.Stream streamId) throws Exception { streamAdmin.create(streamId); streamMetaStore.addStream(streamId); } protected static void dropStream(Id.Stream streamId) throws Exception { streamAdmin.drop(streamId); streamMetaStore.removeStream(streamId); } protected static void setStreamProperties(String namespace, String streamName, StreamProperties properties) throws IOException { int port = streamHttpService.getBindAddress().getPort(); URL url = new URL(String.format("http://127.0.0.1:%d%s/namespaces/%s/streams/%s/properties", port, Constants.Gateway.API_VERSION_3, namespace, streamName)); HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); urlConn.setRequestMethod(HttpMethod.PUT); urlConn.setDoOutput(true); urlConn.getOutputStream().write(GSON.toJson(properties).getBytes(Charsets.UTF_8)); Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode()); urlConn.disconnect(); } protected static void sendStreamEvent(Id.Stream streamId, byte[] body) throws IOException { sendStreamEvent(streamId, Collections.<String, String>emptyMap(), body); } protected static void sendStreamEvent(Id.Stream streamId, Map<String, String> headers, byte[] body) throws IOException { HttpURLConnection urlConn = openStreamConnection(streamId); urlConn.setRequestMethod(HttpMethod.POST); urlConn.setDoOutput(true); for (Map.Entry<String, String> header : headers.entrySet()) { // headers must be prefixed by the stream name, otherwise they are filtered out by the StreamHandler. // the handler also strips the stream name from the key before writing it to the stream. urlConn.addRequestProperty(streamId.getId() + "." + header.getKey(), header.getValue()); } urlConn.getOutputStream().write(body); Assert.assertEquals(HttpResponseStatus.OK.getCode(), urlConn.getResponseCode()); urlConn.disconnect(); } private static HttpURLConnection openStreamConnection(Id.Stream streamId) throws IOException { int port = streamHttpService.getBindAddress().getPort(); URL url = new URL(String.format("http://127.0.0.1:%d%s/namespaces/%s/streams/%s", port, Constants.Gateway.API_VERSION_3, streamId.getNamespaceId(), streamId.getId())); return (HttpURLConnection) url.openConnection(); } private static List<Module> createInMemoryModules(CConfiguration configuration, Configuration hConf, TemporaryFolder tmpFolder) throws IOException { configuration.set(Constants.CFG_DATA_INMEMORY_PERSISTENCE, Constants.InMemoryPersistenceType.MEMORY.name()); configuration.set(Constants.Explore.LOCAL_DATA_DIR, tmpFolder.newFolder("hive").getAbsolutePath()); configuration.set(TxConstants.Manager.CFG_TX_SNAPSHOT_LOCAL_DIR, tmpFolder.newFolder("tx").getAbsolutePath()); configuration.setBoolean(TxConstants.Manager.CFG_DO_PERSIST, true); return ImmutableList.of( new ConfigModule(configuration, hConf), new IOModule(), new DiscoveryRuntimeModule().getInMemoryModules(), new LocationRuntimeModule().getInMemoryModules(), new DataSetsModules().getStandaloneModules(), new DataSetServiceModules().getInMemoryModules(), new MetricsClientRuntimeModule().getInMemoryModules(), new ExploreRuntimeModule().getInMemoryModules(), new ExploreClientModule(), new StreamServiceRuntimeModule().getInMemoryModules(), new ViewAdminModules().getInMemoryModules(), new StreamAdminModules().getInMemoryModules(), new NotificationServiceRuntimeModule().getInMemoryModules(), new NamespaceClientRuntimeModule().getInMemoryModules(), new NamespaceStoreModule().getInMemoryModules(), new AbstractModule() { @Override protected void configure() { bind(NotificationFeedManager.class).to(NoOpNotificationFeedManager.class); Multibinder<HttpHandler> handlerBinder = Multibinder.newSetBinder(binder(), HttpHandler.class, Names.named(Constants.Stream.STREAM_HANDLER)); handlerBinder.addBinding().to(StreamHandler.class); handlerBinder.addBinding().to(StreamFetchHandler.class); handlerBinder.addBinding().to(StreamViewHttpHandler.class); CommonHandlers.add(handlerBinder); bind(StreamHttpService.class).in(Scopes.SINGLETON); // Use LocalFileTransactionStateStorage, so that we can use transaction snapshots for assertions in test install(Modules.override(new DataFabricModules().getInMemoryModules()).with(new AbstractModule() { @Override protected void configure() { bind(TransactionStateStorage.class) .annotatedWith(Names.named("persist")) .to(LocalFileTransactionStateStorage.class).in(Scopes.SINGLETON); bind(TransactionStateStorage.class).toProvider(TransactionStateStorageProvider.class).in(Singleton.class); } })); } } ); } // these are needed if we actually want to query streams, as the stream input format looks at the filesystem // to figure out splits. private static List<Module> createStandaloneModules(CConfiguration cConf, Configuration hConf, TemporaryFolder tmpFolder) throws IOException { File localDataDir = tmpFolder.newFolder(); cConf.set(Constants.CFG_LOCAL_DATA_DIR, localDataDir.getAbsolutePath()); cConf.set(Constants.CFG_DATA_INMEMORY_PERSISTENCE, Constants.InMemoryPersistenceType.LEVELDB.name()); cConf.set(Constants.Explore.LOCAL_DATA_DIR, tmpFolder.newFolder("hive").getAbsolutePath()); hConf.set(Constants.CFG_LOCAL_DATA_DIR, localDataDir.getAbsolutePath()); hConf.set(Constants.AppFabric.OUTPUT_DIR, cConf.get(Constants.AppFabric.OUTPUT_DIR)); hConf.set("hadoop.tmp.dir", new File(localDataDir, cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsolutePath()); return ImmutableList.of( new ConfigModule(cConf, hConf), new IOModule(), new DiscoveryRuntimeModule().getStandaloneModules(), new LocationRuntimeModule().getStandaloneModules(), new DataFabricModules().getStandaloneModules(), new DataSetsModules().getStandaloneModules(), new DataSetServiceModules().getStandaloneModules(), new MetricsClientRuntimeModule().getStandaloneModules(), new ExploreRuntimeModule().getStandaloneModules(), new ExploreClientModule(), new StreamServiceRuntimeModule().getStandaloneModules(), new ViewAdminModules().getStandaloneModules(), new StreamAdminModules().getStandaloneModules(), new NotificationServiceRuntimeModule().getStandaloneModules(), new NamespaceClientRuntimeModule().getInMemoryModules(), new NamespaceStoreModule().getStandaloneModules(), new AbstractModule() { @Override protected void configure() { bind(NotificationFeedManager.class).to(NoOpNotificationFeedManager.class); Multibinder<HttpHandler> handlerBinder = Multibinder.newSetBinder(binder(), HttpHandler.class, Names.named(Constants.Stream.STREAM_HANDLER)); handlerBinder.addBinding().to(StreamHandler.class); handlerBinder.addBinding().to(StreamFetchHandler.class); CommonHandlers.add(handlerBinder); bind(StreamHttpService.class).in(Scopes.SINGLETON); } } ); } }