package com.myorg.ripostemicroservicetemplate.endpoints; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.exception.ApiException; import com.nike.riposte.server.http.RequestInfo; import com.nike.riposte.server.http.ResponseInfo; import com.nike.riposte.server.http.StandardEndpoint; import com.nike.riposte.util.Matcher; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSetFuture; import com.datastax.driver.core.Session; import com.datastax.driver.core.SimpleStatement; import com.datastax.driver.core.Statement; import net.javacrumbs.futureconverter.java8guava.FutureConverter; import org.cassandraunit.utils.EmbeddedCassandraServerHelper; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; import io.netty.channel.ChannelHandlerContext; import static com.myorg.ripostemicroservicetemplate.error.ProjectApiError.EXAMPLE_EMBEDDED_CASSANDRA_DISABLED; import static com.nike.riposte.util.AsyncNettyHelper.functionWithTracingAndMdc; /** * Endpoint that shows how to do Cassandra calls in an async way using the async driver utilities, without creating * extra threads to monitor futures/etc. This maximizes the async nonblocking functionality. * * <p>NOTE: Don't let the volume of code in here throw you - a large portion of this class is for embedded cassandra * which wouldn't be necessary for a non-example project. * * <p>TODO: EXAMPLE CLEANUP - Delete this class. * * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") public class ExampleCassandraAsyncEndpoint extends StandardEndpoint<Void, String> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); public static final String MATCHING_ENDPOINT_PATH = "/exampleCassandraAsync"; private static final Matcher MATCHER = Matcher.match(MATCHING_ENDPOINT_PATH); private static final Statement basicCassandraQuery = new SimpleStatement("SELECT release_version FROM system.local"); private final boolean disableCassandra; @Inject public ExampleCassandraAsyncEndpoint(@Named("disableCassandra") Boolean disableCassandra) { this.disableCassandra = disableCassandra; // Start up Cassandra as early as possible so it's ready when the first request comes in. try { // We have to specify the storagedir due to a cassandra-unit bug. // See https://github.com/jsevellec/cassandra-unit/issues/186 System.setProperty("cassandra.storagedir", "build/embeddedCassandra/storageDir"); EmbeddedCassandraUtils.startEmbeddedCassandra(disableCassandra); } catch (Exception ex) { // No need to prevent the entire app from starting up if there are cassandra problems logger.error("Error during embedded cassandra startup", ex); } } @Override public @NotNull CompletableFuture<ResponseInfo<String>> execute( @NotNull RequestInfo<Void> request, @NotNull Executor longRunningTaskExecutor, @NotNull ChannelHandlerContext ctx ) { Session session = EmbeddedCassandraUtils.cassandraSession(disableCassandra); if (session == null) { ApiError apiErrorToThrow = (disableCassandra) ? EXAMPLE_EMBEDDED_CASSANDRA_DISABLED : SampleCoreApiError.GENERIC_SERVICE_ERROR; throw ApiException.newBuilder() .withApiErrors(apiErrorToThrow) .withExceptionMessage("Unable to get cassandra session.") .build(); } ResultSetFuture cassandraResultFuture = session.executeAsync(basicCassandraQuery); // Convert the cassandra result future to a CompletableFuture, then add a listener that turns the result of the // Cassandra call into the ResponseInfo<String> we need to return. Note that we're not doing // thenApplyAsync() because the work done to translate the Cassandra result to our ResponseInfo object is // trivial and doesn't need it's own thread. If you had more complex logic that was time consuming (or more // blocking calls) you would want to do the extra work with CompletableFuture.*Async() calls. return FutureConverter .toCompletableFuture(cassandraResultFuture) .thenApply(functionWithTracingAndMdc(this::buildResponseFromCassandraQueryResult, ctx)); } private ResponseInfo<String> buildResponseFromCassandraQueryResult(ResultSet result) { logger.info("Building response for async cassandra request"); return ResponseInfo .newBuilder("Cassandra query succeeded. Cassandra version: " + result.one().getString("release_version")) .withDesiredContentWriterMimeType("text/text") .build(); } @Override public @NotNull Matcher requestMatcher() { return MATCHER; } /** * Contains some static utilities for starting an embedded Cassandra instance. Normally your Guice module would * configure whatever cassandra cluster/session you wanted (embedded or otherwise), and you'd {@code @Inject} the * custer/session into your endpoints as needed. But since this is just test/example code tied to * ExampleCassandraAsyncEndpoint, we want this code to get wiped away when ExampleCassandraAsyncEndpoint is * deleted. */ public static class EmbeddedCassandraUtils { private static final Logger logger = LoggerFactory.getLogger(EmbeddedCassandraUtils.class); private static final String embeddedClusterContactPointHost = "localhost"; private static final int embeddedClusterContactPointPort = 9042; private static final String embeddedClusterWorkDirectory = "build/embeddedCassandra"; private static final String cassandraYamlFile = "/embedded-cassandra.yaml"; private static Session cassandraSession = null; // All access to this class should happen through the static methods. private EmbeddedCassandraUtils() { } @SuppressWarnings("UnusedReturnValue") private static Session startEmbeddedCassandra(boolean disableCassandra) { if (disableCassandra) { logger.warn("Embedded cassandra is NOT starting up because your app configuration explicitly requests " + "that it be disabled."); return null; } if (cassandraSession == null) { File cassandraWorkDir = new File(embeddedClusterWorkDirectory); String cassandraWorkDirAbsolutePath = cassandraWorkDir.getAbsolutePath(); if (!cassandraWorkDir.exists()) { logger.info("Creating the embedded Cassandra folders...{}", cassandraWorkDirAbsolutePath); if (!cassandraWorkDir.mkdirs()) { throw new RuntimeException("Unable to create working directory " + cassandraWorkDirAbsolutePath); } } // Start embedded cassandra logger.info("Finished Creating the embedded Cassandra folders...{}", cassandraWorkDirAbsolutePath); logger.info("Starting embedded Cassandra"); try { EmbeddedCassandraServerHelper.startEmbeddedCassandra(cassandraYamlFile, embeddedClusterWorkDirectory); } catch (Exception e) { throw new RuntimeException(e); } Cluster cassandraCluster = Cluster.builder() .addContactPoint(embeddedClusterContactPointHost) .withPort(embeddedClusterContactPointPort) .build(); cassandraSession = cassandraCluster.connect(); } return cassandraSession; } public static Session cassandraSession(boolean disableCassandra) { if (cassandraSession == null) { startEmbeddedCassandra(disableCassandra); } return cassandraSession; } } }