diff --git a/pom.xml b/pom.xml index 18756daa..78a6e61e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ io.revolver dropwizard-callback-resilience4j - 1.0.5 + 1.0.5-SNAPSHOT jar dropwizard-revolver diff --git a/src/main/java/io/dropwizard/revolver/RevolverBundle.java b/src/main/java/io/dropwizard/revolver/RevolverBundle.java index ac74e468..77b7cf86 100644 --- a/src/main/java/io/dropwizard/revolver/RevolverBundle.java +++ b/src/main/java/io/dropwizard/revolver/RevolverBundle.java @@ -31,11 +31,7 @@ import io.dropwizard.revolver.aeroapike.AerospikeConnectionManager; import io.dropwizard.revolver.callback.InlineCallbackHandler; import io.dropwizard.revolver.core.RevolverExecutionException; -import io.dropwizard.revolver.core.config.AerospikeMailBoxConfig; -import io.dropwizard.revolver.core.config.InMemoryMailBoxConfig; -import io.dropwizard.revolver.core.config.RevolverConfig; -import io.dropwizard.revolver.core.config.RevolverServiceConfig; -import io.dropwizard.revolver.core.config.ServiceDiscoveryConfig; +import io.dropwizard.revolver.core.config.*; import io.dropwizard.revolver.core.config.hystrix.HystrixUtil; import io.dropwizard.revolver.core.config.hystrix.ThreadPoolConfig; import io.dropwizard.revolver.core.model.RevolverExecutorType; @@ -66,33 +62,28 @@ import io.dropwizard.revolver.persistence.AeroSpikePersistenceProvider; import io.dropwizard.revolver.persistence.InMemoryPersistenceProvider; import io.dropwizard.revolver.persistence.PersistenceProvider; -import io.dropwizard.revolver.resource.RevolverApiManageResource; -import io.dropwizard.revolver.resource.RevolverConfigResource; -import io.dropwizard.revolver.resource.RevolverMailboxResource; -import io.dropwizard.revolver.resource.RevolverMailboxResourceV2; -import io.dropwizard.revolver.resource.RevolverMetadataResource; +import io.dropwizard.revolver.provider.BlacklistMethodData; +import io.dropwizard.revolver.provider.BlacklistProcessor; +import io.dropwizard.revolver.resource.*; import io.dropwizard.revolver.splitting.PathExpressionSplitConfig; import io.dropwizard.revolver.splitting.SplitConfig; import io.dropwizard.riemann.RiemannBundle; import io.dropwizard.riemann.RiemannConfig; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.apache.curator.framework.CuratorFramework; +import org.msgpack.jackson.dataformat.MessagePackFactory; + +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; -import lombok.extern.slf4j.Slf4j; -import lombok.val; -import org.apache.curator.framework.CuratorFramework; -import org.msgpack.jackson.dataformat.MessagePackFactory; /** * @author phaneesh @@ -167,6 +158,8 @@ public void run(T configuration, Environment environment) { public abstract ConfigSource getConfigSource(); + public abstract Set getBlacklistData(); + public void onConfigChange(String configData) { log.info("Config changed! Override to propagate config changes to other bundles"); } @@ -295,7 +288,7 @@ private static void registerHttpsCommand(RevolverServiceConfig config) { } private static void registerCommand(RevolverServiceConfig config, - RevolverHttpServiceConfig revolverHttpServiceConfig) { + RevolverHttpServiceConfig revolverHttpServiceConfig) { if (config instanceof RevolverHttpServiceConfig) { @@ -424,9 +417,9 @@ private void initializeRevolver(T configuration, Environment environment) { .resolverConfig(revolverConfig.getServiceResolverConfig()) .serviceDiscoveryConfig(serviceDiscoveryConfig).build() : RevolverServiceResolver.builder() - .resolverConfig(revolverConfig.getServiceResolverConfig()) - .objectMapper(environment.getObjectMapper()). - serviceDiscoveryConfig(serviceDiscoveryConfig).build(); + .resolverConfig(revolverConfig.getServiceResolverConfig()) + .objectMapper(environment.getObjectMapper()). + serviceDiscoveryConfig(serviceDiscoveryConfig).build(); } else { serviceNameResolver = RevolverServiceResolver.builder() .objectMapper(environment.getObjectMapper()) @@ -518,7 +511,7 @@ private void registerTypes(Bootstrap bootstrap) { private void registerResources(Environment environment, MetricRegistry metrics, - PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler) { + PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler) { environment.jersey().register(new RevolverMetadataResource(revolverConfig)); environment.jersey().register( new RevolverMailboxResource(persistenceProvider, environment.getObjectMapper(), @@ -535,6 +528,13 @@ private void registerResources(Environment environment, MetricRegistry metrics, } environment.jersey().register(new RevolverConfigResource(dynamicConfigHandler)); environment.jersey().register(new RevolverApiManageResource()); + environment.jersey().register(new RevolverCallbackResource(persistenceProvider, callbackHandler)); + environment.jersey().register( + new RevolverRequestResource(environment.getObjectMapper(), MSG_PACK_OBJECT_MAPPER, + persistenceProvider, callbackHandler, metrics, revolverConfig)); + + // Register blacklist processor + environment.jersey().register(new BlacklistProcessor(getBlacklistData())); } private void registerFilters(Environment environment) { diff --git a/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java b/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java new file mode 100644 index 00000000..0bdbfd82 --- /dev/null +++ b/src/main/java/io/dropwizard/revolver/provider/BlacklistMethodData.java @@ -0,0 +1,14 @@ +package io.dropwizard.revolver.provider; + +import lombok.*; + +@Getter +@Builder +@ToString +@AllArgsConstructor +@EqualsAndHashCode +public class BlacklistMethodData { + private final String httpMethod; + private final String relativePath; // path of the method excluding parent resource path + private final String resourceClassName; +} diff --git a/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java b/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java new file mode 100644 index 00000000..4f33102c --- /dev/null +++ b/src/main/java/io/dropwizard/revolver/provider/BlacklistProcessor.java @@ -0,0 +1,79 @@ +package io.dropwizard.revolver.provider; + +import lombok.extern.slf4j.Slf4j; +import org.glassfish.jersey.server.model.ModelProcessor; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.Resource.Builder; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.model.ResourceModel; + +import javax.ws.rs.core.Configuration; +import javax.ws.rs.ext.Provider; +import java.util.List; +import java.util.Set; + +@Provider +@Slf4j +public class BlacklistProcessor implements ModelProcessor { + + private final Set blacklistMethods; + + public BlacklistProcessor(final Set blacklistMethods) { + this.blacklistMethods = blacklistMethods; + } + + @Override + public ResourceModel processResourceModel(final ResourceModel resourceModel, final Configuration configuration) { + if (blacklistMethods == null || blacklistMethods.isEmpty()) { + log.info("No API end-point to blacklist"); + return resourceModel; + } + + ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false); + for (final Resource resource : resourceModel.getResources()) { + final Resource.Builder resourceBuilder; + + final List childResources = resource.getChildResources(); + + resourceBuilder = childResources.isEmpty() + ? Resource.builder(resource) + : Resource.builder(resource.getPath()); + + // Add child resources with non-blacklisted child resource methods only + childResources.forEach(childResource -> { + final Builder childResourceBuilder = Resource.builder(childResource.getPath()); + childResource.getResourceMethods().stream() + .filter(this::shouldAddMethod) + .forEach(childResourceBuilder::addMethod); + resourceBuilder.addChildResource(childResourceBuilder.build()); + }); + + newResourceModelBuilder.addResource(resourceBuilder.build()); + } + + return newResourceModelBuilder.build(); + } + + @Override + public ResourceModel processSubResource(final ResourceModel subResourceModel, final Configuration configuration) { + return subResourceModel; + } + + private boolean shouldAddMethod(final ResourceMethod method) { + // This is slightly sub-optimal, + // but given that blacklist count will be small, not creating a complex Map to match Jersey resource structure + return blacklistMethods.stream() + .noneMatch(blacklistData -> { + if (method.getInvocable().getHandler().getHandlerClass().getName().equals(blacklistData.getResourceClassName()) && + method.getHttpMethod().equalsIgnoreCase(blacklistData.getHttpMethod()) && + method.getParent().getPath().equals(blacklistData.getRelativePath())) { + log.info("Blacklisting method with path: {}, http method: {}, resource: {}", + blacklistData.getRelativePath(), + blacklistData.getHttpMethod(), + blacklistData.getResourceClassName()); + return true; + } + return false; + }); + } +} diff --git a/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java b/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java new file mode 100644 index 00000000..e1b238e0 --- /dev/null +++ b/src/main/java/io/dropwizard/revolver/resource/RevolverCallbackResource.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 Phaneesh Nagaraja . + * + * 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 io.dropwizard.revolver.resource; + +import com.codahale.metrics.annotation.Metered; +import com.google.common.base.Strings; +import io.dropwizard.msgpack.MsgPackMediaType; +import io.dropwizard.revolver.base.core.RevolverCallbackResponse; +import io.dropwizard.revolver.callback.InlineCallbackHandler; +import io.dropwizard.revolver.http.RevolverHttpCommand; +import io.dropwizard.revolver.persistence.PersistenceProvider; +import io.dropwizard.revolver.util.HeaderUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import javax.inject.Singleton; +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import lombok.val; + +/** + * @author phaneesh + */ +@Path("/revolver") +@Slf4j +@Singleton +@Api(value = "RequestCallback", description = "Revolver gateway api for callbacks on mailbox requests") +public class RevolverCallbackResource { + + private static final String RESPONSE_CODE_HEADER = "X-RESPONSE-CODE"; + + private final PersistenceProvider persistenceProvider; + + private final InlineCallbackHandler callbackHandler; + + public RevolverCallbackResource(PersistenceProvider persistenceProvider, + InlineCallbackHandler callbackHandler) { + this.persistenceProvider = persistenceProvider; + this.callbackHandler = callbackHandler; + } + + @Path("/v1/callback/{requestId}") + @POST + @Metered + @ApiOperation(value = "Callback for updating responses for a given mailbox request") + @Produces({MediaType.APPLICATION_JSON, MsgPackMediaType.APPLICATION_MSGPACK, + MediaType.APPLICATION_XML}) + @Consumes({MediaType.APPLICATION_JSON, MsgPackMediaType.APPLICATION_MSGPACK, + MediaType.APPLICATION_XML}) + public Response handleCallback(@PathParam("requestId") String requestId, + @HeaderParam(RESPONSE_CODE_HEADER) String responseCode, + @Context HttpHeaders headers, byte[] responseBody) { + long start = System.currentTimeMillis(); + try { + val callbackRequest = persistenceProvider.request(requestId); + log.debug("Callback request in handleCallback : " + callbackRequest); + if (callbackRequest == null) { + return Response.status(Response.Status.BAD_REQUEST).build(); + } + val response = RevolverCallbackResponse.builder().body(responseBody) + .headers(headers.getRequestHeaders()).statusCode( + responseCode != null ? Integer.parseInt(responseCode) + : Response.Status.OK.getStatusCode()).build(); + val mailboxTtl = HeaderUtil.getTTL(callbackRequest); + persistenceProvider.saveResponse(requestId, response, mailboxTtl); + if (callbackRequest.getMode() != null && ( + callbackRequest.getMode().equals(RevolverHttpCommand.CALL_MODE_CALLBACK) + || callbackRequest.getMode() + .equals(RevolverHttpCommand.CALL_MODE_CALLBACK_SYNC)) && !Strings + .isNullOrEmpty(callbackRequest.getCallbackUri())) { + callbackHandler.handle(requestId, response); + } + log.info( + "Callback processing for request id: {} with response size: {} bytes completed in {} ms", + requestId, responseBody.length, (System.currentTimeMillis() - start)); + return Response.accepted().build(); + } catch (Exception e) { + log.error("Callback error", e); + return Response.serverError().build(); + } + } +} diff --git a/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java b/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java new file mode 100644 index 00000000..1893b89b --- /dev/null +++ b/src/main/java/io/dropwizard/revolver/resource/RevolverRequestResource.java @@ -0,0 +1,614 @@ +/* + * Copyright 2016 Phaneesh Nagaraja . + * + * 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 io.dropwizard.revolver.resource; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.annotation.Metered; +import com.collections.CollectionUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import io.dropwizard.jersey.PATCH; +import io.dropwizard.msgpack.MsgPackMediaType; +import io.dropwizard.revolver.RevolverBundle; +import io.dropwizard.revolver.base.core.RevolverAckMessage; +import io.dropwizard.revolver.base.core.RevolverCallbackRequest; +import io.dropwizard.revolver.base.core.RevolverCallbackResponse; +import io.dropwizard.revolver.base.core.RevolverRequestState; +import io.dropwizard.revolver.callback.InlineCallbackHandler; +import io.dropwizard.revolver.core.config.ApiLatencyConfig; +import io.dropwizard.revolver.core.config.RevolverConfig; +import io.dropwizard.revolver.core.tracing.TraceInfo; +import io.dropwizard.revolver.http.RevolverHttpCommand; +import io.dropwizard.revolver.http.RevolversHttpHeaders; +import io.dropwizard.revolver.http.config.RevolverHttpApiConfig; +import io.dropwizard.revolver.http.model.ApiPathMap; +import io.dropwizard.revolver.http.model.RevolverHttpRequest; +import io.dropwizard.revolver.http.model.RevolverHttpResponse; +import io.dropwizard.revolver.optimizer.config.OptimizerConfig; +import io.dropwizard.revolver.optimizer.config.OptimizerTimeConfig; +import io.dropwizard.revolver.persistence.PersistenceProvider; +import io.dropwizard.revolver.splitting.HeaderExpressionSplitConfig; +import io.dropwizard.revolver.splitting.PathExpressionSplitConfig; +import io.dropwizard.revolver.splitting.RevolverHttpApiSplitConfig; +import io.dropwizard.revolver.splitting.SplitConfig; +import io.dropwizard.revolver.splitting.SplitStrategy; +import io.dropwizard.revolver.util.ResponseTransformationUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import java.io.IOException; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.inject.Singleton; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import lombok.extern.slf4j.Slf4j; +import lombok.val; + +/** + * @author phaneesh + */ +@Path("/apis") +@Slf4j +@Singleton +@Api(value = "Revolver Gateway", description = "Revolver api gateway endpoints") +public class RevolverRequestResource { + + private static final Map BAD_REQUEST_RESPONSE = Collections + .singletonMap("message", "Bad Request"); + private static final Map DUPLICATE_REQUEST_RESPONSE = Collections + .singletonMap("message", "Duplicate"); + private static Map SERVICE_UNAVAILABLE_RESPONSE = Collections + .singletonMap("message", "Service Unavailable"); + private final ObjectMapper jsonObjectMapper; + private final ObjectMapper msgPackObjectMapper; + private final PersistenceProvider persistenceProvider; + private final InlineCallbackHandler callbackHandler; + private final MetricRegistry metrics; + private final RevolverConfig revolverConfig; + + public RevolverRequestResource(ObjectMapper jsonObjectMapper, ObjectMapper msgPackObjectMapper, + PersistenceProvider persistenceProvider, InlineCallbackHandler callbackHandler, + MetricRegistry metrics, RevolverConfig revolverConfig) { + this.jsonObjectMapper = jsonObjectMapper; + this.msgPackObjectMapper = msgPackObjectMapper; + this.persistenceProvider = persistenceProvider; + this.callbackHandler = callbackHandler; + this.metrics = metrics; + this.revolverConfig = revolverConfig; + } + + @GET + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver GET api endpoint") + public Response get(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.GET, path, + headers, uriInfo, null); + pushMetrics(response, service, path); + return response; + } + + @HEAD + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver HEAD api endpoint") + public Response head(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.HEAD, path, + headers, uriInfo, null); + pushMetrics(response, service, path); + return response; + } + + @POST + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver POST api endpoint") + public Response post(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.POST, path, + headers, uriInfo, body); + pushMetrics(response, service, path); + return response; + } + + @PUT + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver PUT api endpoint") + public Response put(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.PUT, path, + headers, uriInfo, body); + pushMetrics(response, service, path); + return response; + } + + @DELETE + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver DELETE api endpoint") + public Response delete(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.DELETE, + path, headers, uriInfo, null); + pushMetrics(response, service, path); + return response; + } + + @PATCH + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver PATCH api endpoint") + public Response patch(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.PATCH, path, + headers, uriInfo, body); + pushMetrics(response, service, path); + return response; + } + + @OPTIONS + @Path(value = "/{service}/{path: .*}") + @Metered + @ApiOperation(value = "Revolver OPTIONS api endpoint") + public Response options(@PathParam("service") String service, @PathParam("path") String path, + @Context HttpHeaders headers, @Context UriInfo uriInfo, byte[] body) throws Exception { + Response response = processRequest(service, RevolverHttpApiConfig.RequestMethod.OPTIONS, + path, headers, uriInfo, body); + pushMetrics(response, service, path); + return response; + } + + + private Response processRequest(String service, RevolverHttpApiConfig.RequestMethod method, + String path, HttpHeaders headers, UriInfo uriInfo, byte[] body) throws Exception { + val apiMap = resolvePath(service, path, headers); + if (apiMap == null) { + return Response.status(Response.Status.BAD_REQUEST).entity(ResponseTransformationUtil + .transform(BAD_REQUEST_RESPONSE, + headers.getMediaType() != null ? headers.getMediaType().toString() + : MediaType.APPLICATION_JSON, jsonObjectMapper, + msgPackObjectMapper)).build(); + } + String serviceKey = service + "." + apiMap.getApi().getApi(); + if (RevolverBundle.apiStatus.containsKey(serviceKey) && !RevolverBundle.apiStatus + .get(serviceKey)) { + return Response.status(Response.Status.SERVICE_UNAVAILABLE) + .entity(ResponseTransformationUtil.transform(SERVICE_UNAVAILABLE_RESPONSE, + headers.getMediaType() != null ? headers.getMediaType().toString() + : MediaType.APPLICATION_JSON, jsonObjectMapper, + msgPackObjectMapper)).build(); + } + val callMode = getCallMode(apiMap, headers); + + if (Strings.isNullOrEmpty(callMode)) { + return executeInline(service, apiMap.getApi(), method, path, headers, uriInfo, body); + } + switch (callMode.toUpperCase()) { + case RevolverHttpCommand.CALL_MODE_POLLING: + return executeCommandAsync(service, apiMap.getApi(), method, path, headers, uriInfo, + body, apiMap.getApi().isAsync(), callMode); + case RevolverHttpCommand.CALL_MODE_CALLBACK: + if (Strings.isNullOrEmpty( + headers.getHeaderString(RevolversHttpHeaders.CALLBACK_URI_HEADER))) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ResponseTransformationUtil.transform(BAD_REQUEST_RESPONSE, + headers.getMediaType() != null ? headers.getMediaType() + .toString() : MediaType.APPLICATION_JSON, + jsonObjectMapper, msgPackObjectMapper)).build(); + } + return executeCommandAsync(service, apiMap.getApi(), method, path, headers, uriInfo, + body, apiMap.getApi().isAsync(), callMode); + case RevolverHttpCommand.CALL_MODE_CALLBACK_SYNC: + if (Strings.isNullOrEmpty( + headers.getHeaderString(RevolversHttpHeaders.CALLBACK_URI_HEADER))) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ResponseTransformationUtil.transform(BAD_REQUEST_RESPONSE, + headers.getMediaType() != null ? headers.getMediaType() + .toString() : MediaType.APPLICATION_JSON, + jsonObjectMapper, msgPackObjectMapper)).build(); + } + return executeCallbackSync(service, apiMap.getApi(), method, path, headers, uriInfo, + body); + } + return Response.status(Response.Status.BAD_REQUEST).entity(ResponseTransformationUtil + .transform(BAD_REQUEST_RESPONSE, + headers.getMediaType() != null ? headers.getMediaType().toString() + : MediaType.APPLICATION_JSON, jsonObjectMapper, + msgPackObjectMapper)).build(); + } + + private ApiPathMap resolvePath(String service, String path, HttpHeaders headers) { + val apiMap = RevolverBundle.matchPath(service, path); + if (apiMap == null) { + return null; + } + + String newPath = null; + + RevolverHttpApiConfig httpApiConfiguration = apiMap.getApi(); + RevolverHttpApiSplitConfig revolverHttpApiSplitConfig = httpApiConfiguration + .getSplitConfig(); + if (null != revolverHttpApiSplitConfig && revolverHttpApiSplitConfig.isEnabled() + && revolverHttpApiSplitConfig.getSplitStrategy() != null) { + SplitStrategy splitStrategy = revolverHttpApiSplitConfig.getSplitStrategy(); + switch (splitStrategy) { + case PATH: + newPath = getPathFromSplitConfig(httpApiConfiguration); + break; + case PATH_EXPRESSION: + newPath = getPathFromPathExpression(revolverHttpApiSplitConfig, path); + break; + case HEADER_EXPRESSION: + newPath = getPathFromHeaderExpression(revolverHttpApiSplitConfig, path, + headers); + break; + } + } + if (Strings.isNullOrEmpty(newPath)) { + return apiMap; + } + return RevolverBundle.matchPath(service, newPath); + } + + private String getPathFromSplitConfig(RevolverHttpApiConfig httpApiConfiguration) { + double random = Math.random(); + for (SplitConfig splitConfig : httpApiConfiguration.getSplitConfig().getSplits()) { + if (splitConfig.getFrom() <= random && splitConfig.getTo() > random) { + return splitConfig.getPath(); + } + } + return null; + } + + private String getPathFromPathExpression(RevolverHttpApiSplitConfig revolverHttpApiSplitConfig, + String path) { + + List pathExpressionSplitConfigs = revolverHttpApiSplitConfig + .getPathExpressionSplitConfigs(); + + for (PathExpressionSplitConfig pathExpressionSplitConfig : CollectionUtils + .nullSafeList(pathExpressionSplitConfigs)) { + if (matches(pathExpressionSplitConfig.getExpression(), path)) { + return pathExpressionSplitConfig.getPath(); + } + } + return null; + } + + private String getPathFromHeaderExpression( + RevolverHttpApiSplitConfig revolverHttpApiSplitConfig, String path, + HttpHeaders headers) { + + List headerExpressionSplitConfigs = revolverHttpApiSplitConfig + .getHeaderExpressionSplitConfigs(); + + for (HeaderExpressionSplitConfig headerExpressionSplitConfig : CollectionUtils + .nullSafeList(headerExpressionSplitConfigs)) { + if (!headers.getRequestHeaders().containsKey(headerExpressionSplitConfig.getHeader())) { + continue; + } + if (matches(headerExpressionSplitConfig.getExpression(), + headers.getHeaderString(headerExpressionSplitConfig.getHeader()))) { + return headerExpressionSplitConfig.getPath(); + } + } + return null; + } + + private boolean matches(String expression, String path) { + Pattern pattern = Pattern.compile(expression); + Matcher matcher = pattern.matcher(path); + return matcher.matches(); + } + + private String getCallMode(ApiPathMap apiMap, HttpHeaders headers) { + + val callMode = headers.getRequestHeaders().getFirst(RevolversHttpHeaders.CALL_MODE_HEADER); + OptimizerConfig optimizerConfig = revolverConfig.getOptimizerConfig(); + if (optimizerConfig == null || !optimizerConfig.isEnabled() + || optimizerConfig.getTimeConfig() == null || !Strings.isNullOrEmpty(callMode) + || !(headers.getRequestHeaders() + .containsKey(RevolversHttpHeaders.DYAMIC_MAILBOX))) { + return callMode; + } + ApiLatencyConfig apiLatencyConfig = apiMap.getApi().getApiLatencyConfig(); + if (apiLatencyConfig == null || apiLatencyConfig.isDowngradeDisable()) { + return callMode; + } + OptimizerTimeConfig timeoutConfig = revolverConfig.getOptimizerConfig().getTimeConfig(); + if (apiLatencyConfig.getLatency() > timeoutConfig.getAppLatencyThresholdValue()) { + return RevolverHttpCommand.CALL_MODE_POLLING; + } + return callMode; + } + + private Response executeInline(String service, RevolverHttpApiConfig api, + RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers, + UriInfo uriInfo, byte[] body) throws IOException, TimeoutException { + val sanatizedHeaders = new MultivaluedHashMap(); + headers.getRequestHeaders().forEach(sanatizedHeaders::put); + cleanHeaders(sanatizedHeaders, api); + val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi()); + RevolverHttpResponse revolverHttpResponse = execute(httpCommand, service, api, method, path, + headers, uriInfo, body, sanatizedHeaders); + return transform(headers, revolverHttpResponse, api.getApi(), path, method); + } + + private RevolverHttpResponse execute(RevolverHttpCommand httpCommand, String service, + RevolverHttpApiConfig api, RevolverHttpApiConfig.RequestMethod method, String path, + HttpHeaders headers, UriInfo uriInfo, byte[] body, + MultivaluedHashMap sanatizedHeaders) throws TimeoutException { + return httpCommand.execute(RevolverHttpRequest.builder().traceInfo(TraceInfo.builder() + .requestId(headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER)) + .transactionId(headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER)) + .timestamp(System.currentTimeMillis()).build()).api(api.getApi()).service(service) + .path(path).method(method).headers(sanatizedHeaders) + .queryParams(uriInfo.getQueryParameters()).body(body).build()); + } + + private Response transform(HttpHeaders headers, RevolverHttpResponse response, String api, + String path, RevolverHttpApiConfig.RequestMethod method) throws IOException { + val httpResponse = Response.status(response.getStatusCode()); + //Add all the headers except content type header + if (response.getHeaders() != null) { + response.getHeaders().keySet().stream() + .filter(h -> !h.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) + .filter(h -> !h.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) + .forEach(h -> httpResponse.header(h, response.getHeaders().getFirst(h))); + } + httpResponse.header("X-REQUESTED-PATH", path); + httpResponse.header("X-REQUESTED-METHOD", method); + httpResponse.header("X-REQUESTED-API", api); + String responseMediaType = response.getHeaders() != null && Strings + .isNullOrEmpty(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)) + ? MediaType.TEXT_HTML : response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); + String requestMediaType = headers != null && Strings + .isNullOrEmpty(headers.getHeaderString(HttpHeaders.ACCEPT)) ? null + : headers.getHeaderString(HttpHeaders.ACCEPT); + //If no accept was specified in request or accept was wildcard; just send it as the same content type as response + //Also send it as the content type as response content type if there requested content type is the same; + if (Strings.isNullOrEmpty(requestMediaType) + || requestMediaType.startsWith(MediaType.MEDIA_TYPE_WILDCARD) + || requestMediaType.equals(responseMediaType)) { + httpResponse.header(HttpHeaders.CONTENT_TYPE, responseMediaType); + httpResponse.entity(response.getBody()); + return httpResponse.build(); + } + Object responseData = null; + if (responseMediaType.startsWith(MediaType.APPLICATION_JSON)) { + JsonNode jsonNode = jsonObjectMapper.readTree(response.getBody()); + if (jsonNode.isArray()) { + responseData = jsonObjectMapper.convertValue(jsonNode, List.class); + } else { + responseData = jsonObjectMapper.convertValue(jsonNode, Map.class); + } + } else if (responseMediaType.startsWith(MsgPackMediaType.APPLICATION_MSGPACK)) { + JsonNode jsonNode = msgPackObjectMapper.readTree(response.getBody()); + if (jsonNode.isArray()) { + responseData = msgPackObjectMapper.convertValue(jsonNode, List.class); + } else { + responseData = msgPackObjectMapper.convertValue(jsonNode, Map.class); + } + } + if (responseData == null) { + httpResponse.entity(response.getBody()); + } else { + if (requestMediaType.startsWith(MediaType.APPLICATION_JSON)) { + httpResponse.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + httpResponse.entity(jsonObjectMapper.writeValueAsBytes(responseData)); + } else if (requestMediaType.startsWith(MsgPackMediaType.APPLICATION_MSGPACK)) { + httpResponse.header(HttpHeaders.CONTENT_TYPE, MsgPackMediaType.APPLICATION_MSGPACK); + httpResponse.entity(msgPackObjectMapper.writeValueAsBytes(responseData)); + } else { + httpResponse.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + httpResponse.entity(jsonObjectMapper.writeValueAsBytes(responseData)); + } + } + return httpResponse.build(); + } + + + private void cleanHeaders(MultivaluedMap headers, + RevolverHttpApiConfig apiConfig) { + headers.remove(HttpHeaders.HOST); + headers.remove(HttpHeaders.ACCEPT); + headers.remove(HttpHeaders.ACCEPT_ENCODING); + headers.putSingle(HttpHeaders.ACCEPT, apiConfig.getAcceptType()); + headers.putSingle(HttpHeaders.ACCEPT_ENCODING, apiConfig.getAcceptEncoding()); + } + + private Response executeCommandAsync(String service, RevolverHttpApiConfig api, + RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers, + UriInfo uriInfo, byte[] body, boolean isDownstreamAsync, String callMode) + throws Exception { + val sanatizedHeaders = new MultivaluedHashMap(); + headers.getRequestHeaders().forEach(sanatizedHeaders::put); + cleanHeaders(sanatizedHeaders, api); + val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi()); + val requestId = headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER); + val transactionId = headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER); + val mailBoxId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_ID_HEADER); + val mailBoxAuthId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_AUTH_ID_HEADER); + + val mailBoxTtl = + headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER) != null ? Integer + .parseInt(headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER)) + : -1; + //Short circuit if it is a duplicate request + if (persistenceProvider.exists(requestId)) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity(ResponseTransformationUtil + .transform(DUPLICATE_REQUEST_RESPONSE, + headers.getMediaType() == null ? MediaType.APPLICATION_JSON + : headers.getMediaType().toString(), jsonObjectMapper, + msgPackObjectMapper)).build(); + } + persistenceProvider.saveRequest(requestId, mailBoxId, mailBoxAuthId, + RevolverCallbackRequest.builder().api(api.getApi()).mode(headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALL_MODE_HEADER)).callbackUri( + headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALLBACK_URI_HEADER)) + .method(headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALLBACK_METHOD_HEADER)) + .service(service).path(path).headers(headers.getRequestHeaders()) + .queryParams(uriInfo.getQueryParameters()).body(body).build(), mailBoxTtl); + CompletableFuture response = httpCommand.executeAsync( + RevolverHttpRequest.builder().traceInfo( + TraceInfo.builder().requestId(requestId).transactionId(transactionId) + .timestamp(System.currentTimeMillis()).build()).api(api.getApi()) + .service(service).path(path).method(method).headers(sanatizedHeaders) + .queryParams(uriInfo.getQueryParameters()).body(body).build()); + //Async Downstream send accept on request path (Still circuit breaker will kick in. Keep circuit breaker aggressive) + if (isDownstreamAsync) { + val result = response.get(); + if (result.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) { + persistenceProvider + .setRequestState(requestId, RevolverRequestState.REQUESTED, mailBoxTtl); + } else { + persistenceProvider + .setRequestState(requestId, RevolverRequestState.RESPONDED, mailBoxTtl); + saveResponse(requestId, result, callMode, mailBoxTtl); + } + Response httpResponse = transform(headers, result, api.getApi(), path, method); + if (api.getApiLatencyConfig() != null) { + httpResponse.getHeaders().putSingle(RevolversHttpHeaders.RETRY_AFTER, + api.getApiLatencyConfig().getLatency()); + } + return httpResponse; + } else { + response.thenAcceptAsync(result -> { + try { + if (result.getStatusCode() == Response.Status.ACCEPTED.getStatusCode()) { + persistenceProvider + .setRequestState(requestId, RevolverRequestState.REQUESTED, + mailBoxTtl); + } else if (result.getStatusCode() == Response.Status.OK.getStatusCode()) { + persistenceProvider + .setRequestState(requestId, RevolverRequestState.RESPONDED, + mailBoxTtl); + saveResponse(requestId, result, callMode, mailBoxTtl); + } else { + persistenceProvider + .setRequestState(requestId, RevolverRequestState.ERROR, mailBoxTtl); + saveResponse(requestId, result, callMode, mailBoxTtl); + } + } catch (Exception e) { + log.error("Error setting request state for request id: {}", requestId, e); + } + }); + RevolverAckMessage revolverAckMessage = RevolverAckMessage.builder() + .requestId(requestId).acceptedAt(Instant.now().toEpochMilli()).build(); + return Response.accepted().entity(ResponseTransformationUtil + .transform(revolverAckMessage, + headers.getMediaType() == null ? MediaType.APPLICATION_JSON + : headers.getMediaType().toString(), jsonObjectMapper, + msgPackObjectMapper)).header(RevolversHttpHeaders.RETRY_AFTER, + api.getApiLatencyConfig() == null ? 0 : api.getApiLatencyConfig().getLatency()) + .build(); + } + } + + private Response executeCallbackSync(String service, RevolverHttpApiConfig api, + RevolverHttpApiConfig.RequestMethod method, String path, HttpHeaders headers, + UriInfo uriInfo, byte[] body) throws Exception { + val sanatizedHeaders = new MultivaluedHashMap(); + headers.getRequestHeaders().forEach(sanatizedHeaders::put); + cleanHeaders(sanatizedHeaders, api); + val httpCommand = RevolverBundle.getHttpCommand(service, api.getApi()); + val requestId = headers.getHeaderString(RevolversHttpHeaders.REQUEST_ID_HEADER); + val transactionId = headers.getHeaderString(RevolversHttpHeaders.TXN_ID_HEADER); + val mailBoxId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_ID_HEADER); + val mailBoxAuthId = headers.getHeaderString(RevolversHttpHeaders.MAILBOX_AUTH_ID_HEADER); + + val mailBoxTtl = + headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER) != null ? Integer + .parseInt(headers.getHeaderString(RevolversHttpHeaders.MAILBOX_TTL_HEADER)) + : -1; + //Short circuit if it is a duplicate request + if (persistenceProvider.exists(requestId)) { + return Response.status(Response.Status.NOT_ACCEPTABLE).entity(ResponseTransformationUtil + .transform(DUPLICATE_REQUEST_RESPONSE, + headers.getMediaType() == null ? MediaType.APPLICATION_JSON + : headers.getMediaType().toString(), jsonObjectMapper, + msgPackObjectMapper)).build(); + } + persistenceProvider.saveRequest(requestId, mailBoxId, mailBoxAuthId, + RevolverCallbackRequest.builder().api(api.getApi()).mode(headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALL_MODE_HEADER)).callbackUri( + headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALLBACK_URI_HEADER)) + .method(headers.getRequestHeaders() + .getFirst(RevolversHttpHeaders.CALLBACK_METHOD_HEADER)) + .service(service).path(path).headers(headers.getRequestHeaders()) + .queryParams(uriInfo.getQueryParameters()).body(body).build(), mailBoxTtl); + CompletableFuture response = httpCommand.executeAsync( + RevolverHttpRequest.builder().traceInfo( + TraceInfo.builder().requestId(requestId).transactionId(transactionId) + .timestamp(System.currentTimeMillis()).build()).api(api.getApi()) + .service(service).path(path).method(method).headers(sanatizedHeaders) + .queryParams(uriInfo.getQueryParameters()).body(body).build()); + persistenceProvider.setRequestState(requestId, RevolverRequestState.REQUESTED, mailBoxTtl); + val result = response.get(); + return transform(headers, result, api.getApi(), path, method); + } + + private void saveResponse(String requestId, RevolverHttpResponse result, String callMode, + int ttl) { + try { + val response = RevolverCallbackResponse.builder().body(result.getBody()) + .headers(result.getHeaders()).statusCode(result.getStatusCode()).build(); + persistenceProvider.saveResponse(requestId, response, ttl); + if (callMode != null && callMode.equals(RevolverHttpCommand.CALL_MODE_CALLBACK)) { + callbackHandler.handle(requestId, response); + } + } catch (Exception e) { + log.error("Error saving response!", e); + } + } + + private void pushMetrics(Response response, String service, String path) { + val apiMap = RevolverBundle.matchPath(service, path); + if (apiMap == null) { + return; + } + String api = apiMap.getApi().getApi(); + metrics.meter(String.format("%s.%s.%s", service, api, response.getStatus())); + } +} diff --git a/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java b/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java index 8486ab25..6f6bf16b 100644 --- a/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java +++ b/src/test/java/io/dropwizard/revolver/BaseRevolverTest.java @@ -58,6 +58,7 @@ import io.dropwizard.revolver.optimizer.config.OptimizerConfig; import io.dropwizard.revolver.optimizer.utils.OptimizerUtils; import io.dropwizard.revolver.persistence.InMemoryPersistenceProvider; +import io.dropwizard.revolver.provider.BlacklistMethodData; import io.dropwizard.revolver.retry.RevolverApiRetryConfig; import io.dropwizard.revolver.splitting.PathExpressionSplitConfig; import io.dropwizard.revolver.splitting.RevolverHttpApiSplitConfig; @@ -74,6 +75,8 @@ import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.List; +import java.util.Set; + import lombok.extern.slf4j.Slf4j; import lombok.val; import org.apache.curator.framework.CuratorFramework; @@ -386,6 +389,11 @@ public CuratorFramework getCurator() { public ConfigSource getConfigSource() { return null; } + + @Override + public Set getBlacklistData() { + return null; + } }; @Rule public WireMockRule wireMockRule = new WireMockRule(9999, 9933);