Skip to content

Commit

Permalink
Add injection point for handling exceptions
Browse files Browse the repository at this point in the history
-- Before we hard-coded how we handled exceptions in the Servlets.
For example, by default we return a 400 - Bad Request if we get an
Exception. However, this is very inflexible. For example, suppose
customers have some customizations (like their own SearchProviders) that
may generate errors the DataServlet doesn't know to look for, but aren't
the fault of the user. Then, Fili currently returns a 400, even though
it should probably be some sort of 5xx.

So we add an injection point to all servlets that allows customers to
provide their own logic for handling DataServlet errors.
  • Loading branch information
Andrew Cholewa committed Jul 13, 2018
1 parent 39678fd commit 279a162
Show file tree
Hide file tree
Showing 22 changed files with 758 additions and 241 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@
import com.yahoo.bard.webservice.druid.util.FieldConverterSupplier;
import com.yahoo.bard.webservice.druid.util.FieldConverters;
import com.yahoo.bard.webservice.druid.util.SketchFieldConverter;
import com.yahoo.bard.webservice.exception.DataExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliDataExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliDimensionExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliJobsExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliMetricExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliSlicesExceptionHandler;
import com.yahoo.bard.webservice.exception.FiliTablesExceptionHandler;
import com.yahoo.bard.webservice.exception.MetadataExceptionHandler;
import com.yahoo.bard.webservice.metadata.DataSourceMetadataLoadTask;
import com.yahoo.bard.webservice.metadata.DataSourceMetadataService;
import com.yahoo.bard.webservice.metadata.QuerySigningService;
Expand All @@ -84,30 +92,30 @@
import com.yahoo.bard.webservice.util.DefaultingDictionary;
import com.yahoo.bard.webservice.util.SimplifiedIntervalList;
import com.yahoo.bard.webservice.web.CsvResponseWriter;
import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;
import com.yahoo.bard.webservice.web.DefaultResponseFormatResolver;
import com.yahoo.bard.webservice.web.DimensionApiRequestMapper;
import com.yahoo.bard.webservice.web.apirequest.DimensionsApiRequest;
import com.yahoo.bard.webservice.web.FiliResponseWriter;
import com.yahoo.bard.webservice.web.FiliResponseWriterSelector;
import com.yahoo.bard.webservice.web.FilteredSketchMetricsHelper;
import com.yahoo.bard.webservice.web.apirequest.JobsApiRequest;
import com.yahoo.bard.webservice.web.JsonApiResponseWriter;
import com.yahoo.bard.webservice.web.JsonResponseWriter;
import com.yahoo.bard.webservice.web.apirequest.MetricsApiRequest;
import com.yahoo.bard.webservice.web.MetricsFilterSetBuilder;
import com.yahoo.bard.webservice.web.NoOpRequestMapper;
import com.yahoo.bard.webservice.web.RateLimiter;
import com.yahoo.bard.webservice.web.RequestMapper;
import com.yahoo.bard.webservice.web.ResponseFormatResolver;
import com.yahoo.bard.webservice.web.ResponseWriter;
import com.yahoo.bard.webservice.web.apirequest.SlicesApiRequest;
import com.yahoo.bard.webservice.web.apirequest.TablesApiRequest;
import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;
import com.yahoo.bard.webservice.web.apirequest.DataApiRequestFactory;
import com.yahoo.bard.webservice.web.apirequest.DefaultDataApiRequestFactory;
import com.yahoo.bard.webservice.web.apirequest.DefaultHavingApiGenerator;
import com.yahoo.bard.webservice.web.apirequest.DimensionsApiRequest;
import com.yahoo.bard.webservice.web.apirequest.HavingGenerator;
import com.yahoo.bard.webservice.web.apirequest.JobsApiRequest;
import com.yahoo.bard.webservice.web.apirequest.MetricsApiRequest;
import com.yahoo.bard.webservice.web.apirequest.PerRequestDictionaryHavingGenerator;
import com.yahoo.bard.webservice.web.apirequest.SlicesApiRequest;
import com.yahoo.bard.webservice.web.apirequest.TablesApiRequest;
import com.yahoo.bard.webservice.web.handlers.workflow.DruidWorkflow;
import com.yahoo.bard.webservice.web.handlers.workflow.RequestWorkflowProvider;
import com.yahoo.bard.webservice.web.ratelimit.DefaultRateLimiter;
Expand Down Expand Up @@ -345,6 +353,10 @@ protected void configure() {

bind(buildRateLimiter()).to(RateLimiter.class);

bind(getDataExceptionHandler()).to(DataExceptionHandler.class);

bindExceptionHandlers(this);

if (DRUID_DIMENSIONS_LOADER.isOn()) {
DimensionValueLoadTask dimensionLoader = buildDruidDimensionsLoader(
druidWebService,
Expand All @@ -356,14 +368,68 @@ protected void configure() {
if (SYSTEM_CONFIG.getBooleanProperty(DEPRECATED_PERMISSIVE_AVAILABILITY_FLAG, false)) {
LOG.warn(
"Permissive column availability feature flag is no longer supported, please use " +
"PermissivePhysicalTable to enable permissive column availability.");
"PermissivePhysicalTable to enable permissive column availability."
);
}
// Call post-binding hook to allow for additional binding
afterBinding(this);
}

};
}

/**
* Binds all the exception handlers to the specified binder.
*
* @param binder The binder to bind the exception handlers to
*/
protected void bindExceptionHandlers(AbstractBinder binder) {
binder.bind(getDimensionExceptionHandler())
.named(DimensionsApiRequest.EXCEPTION_HANDLER_NAMESPACE)
.to(MetadataExceptionHandler.class);

binder.bind(getMetricsExceptionHandler())
.named(MetricsApiRequest.EXCEPTION_HANDLER_NAMESPACE)
.to(MetadataExceptionHandler.class);

binder.bind(getTablesExceptionHandler())
.named(TablesApiRequest.EXCEPTION_HANDLER_NAMESPACE)
.to(MetadataExceptionHandler.class);

binder.bind(getSlicesExceptionHandler())
.named(SlicesApiRequest.EXCEPTION_HANDLER_NAMESPACE)
.to(MetadataExceptionHandler.class);

binder.bind(getJobsExceptionHandler())
.named(JobsApiRequest.EXCEPTION_HANDLER_NAMESPACE)
.to(MetadataExceptionHandler.class);

}

protected Class<? extends MetadataExceptionHandler> getDimensionExceptionHandler() {
return FiliDimensionExceptionHandler.class;
}

protected Class<? extends MetadataExceptionHandler> getMetricsExceptionHandler() {
return FiliMetricExceptionHandler.class;
}

protected Class<? extends MetadataExceptionHandler> getSlicesExceptionHandler() {
return FiliSlicesExceptionHandler.class;
}

protected Class<? extends MetadataExceptionHandler> getTablesExceptionHandler() {
return FiliTablesExceptionHandler.class;
}

protected Class<? extends MetadataExceptionHandler> getJobsExceptionHandler() {
return FiliJobsExceptionHandler.class;
}

protected Class<? extends DataExceptionHandler> getDataExceptionHandler() {
return FiliDataExceptionHandler.class;
}

/**
* Bind ApiRequest instances to resource scope names.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2018 Oath Inc.
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms.
package com.yahoo.bard.webservice.exception;

import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;

import com.fasterxml.jackson.databind.ObjectWriter;

import java.util.Optional;

import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.UriInfo;

/**
* Allows a customer to inject custom code for handling Exceptions in Fili's DataServlet.
*
* For example, customers may want to return 500's for some custom exceptions, or return
* an empty set when dimension rows are not found.
*/
public interface DataExceptionHandler {

/**
* Handles any exception generated during response processing.
*
* @param e The generated throwable (i.e. exception or error)
* @param asyncResponse A response that can be used to respond asynchronously
* (probably with some sort of appropriate error code)
* @param apiRequest A bean containing parsed information about the request, note that
* this won't exist if an exception was thrown while constructing the request
* @param containerRequestContext HTTP request context
* @param uriInfo URiInfo of the request
* @param writer A tool for serializing JSON
*/
void handleException(
Throwable e,
AsyncResponse asyncResponse,
Optional<DataApiRequest> apiRequest,
ContainerRequestContext containerRequestContext,
UriInfo uriInfo,
ObjectWriter writer
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2018 Oath Inc.
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms.
package com.yahoo.bard.webservice.exception;

import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;

import com.fasterxml.jackson.databind.ObjectWriter;

import java.util.Optional;

import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.UriInfo;

/**
* Allows a customer to inject custom code for handling Exceptions in Fili.
*
* For example, customers may want to return 500's for some custom exceptions, or return
* an empty set when dimension rows are not found.
*/
public interface ExceptionHandler {

/**
* Handles any exception generated during response processing.
*
* @param e The generated throwable (i.e. exception or error)
* @param asyncResponse A response that can be used to respond asynchronously
* (probably with some sort of appropriate error code)
* @param apiRequest A bean containing parsed information about the request, note that
* this won't exist if an exception was thrown while constructing the request
* @param containerRequestContext HTTP request context
* @param uriInfo URiInfo of the request
* @param writer A tool for serializing JSON
*/
void handleException(
Throwable e,
AsyncResponse asyncResponse,
Optional<DataApiRequest> apiRequest,
ContainerRequestContext containerRequestContext,
UriInfo uriInfo,
ObjectWriter writer
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright 2018 Oath Inc.
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms.
package com.yahoo.bard.webservice.exception;

import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
import static javax.ws.rs.core.Response.Status.GATEWAY_TIMEOUT;
import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;

import com.yahoo.bard.webservice.data.dimension.TimeoutException;
import com.yahoo.bard.webservice.table.resolver.NoMatchFoundException;
import com.yahoo.bard.webservice.web.RequestValidationException;
import com.yahoo.bard.webservice.web.apirequest.DataApiRequest;
import com.yahoo.bard.webservice.web.handlers.RequestHandlerUtils;

import com.fasterxml.jackson.databind.ObjectWriter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;

import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.UriInfo;

/**
* The default implementation of {@link DataExceptionHandler}.
*
* This class handles the following cases:
*
* <ol>
* <li> RequestValidationException - builds an error response and returns the HTTP status stored in the
* exception
* <li> NoMatchFoundException - Returns a 500 (Internal Server Error)
* <li> TimeoutException - Returns a 504 (Gateway Timeout)
* <li> Error | Exception - A 400 with an response payload containing the throwable's message
* </ol>
*/
public class FiliDataExceptionHandler implements DataExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(FiliDataExceptionHandler.class);

@Override
public void handleException(
Throwable e,
AsyncResponse asyncResponse,
Optional<DataApiRequest> apiRequest,
ContainerRequestContext containerRequestContext,
UriInfo uriInfo,
ObjectWriter writer
) {
if (e instanceof RequestValidationException) {
LOG.debug(e.getMessage(), e);
RequestValidationException rve = (RequestValidationException) e;
asyncResponse.resume(RequestHandlerUtils.makeErrorResponse(rve.getStatus(), rve, writer));
} else if (e instanceof NoMatchFoundException) {
LOG.info("Exception processing request", e);
asyncResponse.resume(RequestHandlerUtils.makeErrorResponse(INTERNAL_SERVER_ERROR, e, writer));
} else if (e instanceof TimeoutException) {
LOG.info("Exception processing request", e);
asyncResponse.resume(RequestHandlerUtils.makeErrorResponse(GATEWAY_TIMEOUT, e, writer));
} else {
LOG.info("Exception processing request", e);
asyncResponse.resume(RequestHandlerUtils.makeErrorResponse(BAD_REQUEST, e, writer));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2018 Oath Inc.
// Licensed under the terms of the Apache license. Please see LICENSE.md file distributed with this work for terms.
package com.yahoo.bard.webservice.exception;

import static com.yahoo.bard.webservice.web.ResponseCode.INSUFFICIENT_STORAGE;

import static javax.ws.rs.core.Response.Status.BAD_REQUEST;

import com.yahoo.bard.webservice.web.ErrorMessageFormat;
import com.yahoo.bard.webservice.web.RequestValidationException;
import com.yahoo.bard.webservice.web.RowLimitReachedException;
import com.yahoo.bard.webservice.web.apirequest.ApiRequest;

import com.fasterxml.jackson.core.JsonProcessingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Optional;

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

/**
* Default implementation of the MetadataExceptionHandler for the DimensionServlet.
*/
public class FiliDimensionExceptionHandler implements MetadataExceptionHandler {
private static final Logger LOG = LoggerFactory.getLogger(FiliDimensionExceptionHandler.class);

@Override
public Response handleThrowable(
Throwable e,
Optional<ApiRequest> request,
UriInfo uriInfo,
ContainerRequestContext requestContext,
String metadataEntityName
) {
if (e instanceof RequestValidationException) {
LOG.debug(e.getMessage(), e);
RequestValidationException rve = (RequestValidationException) e;
return Response.status(rve.getStatus()).entity(rve.getErrorHttpMsg()).build();
} else if (e instanceof RowLimitReachedException) {
String msg = String.format("Row limit exceeded for dimension %s: %s", metadataEntityName, e.getMessage());
LOG.debug(msg, e);
return Response.status(INSUFFICIENT_STORAGE).entity(msg).build();
} else if (e instanceof JsonProcessingException) {
String msg = ErrorMessageFormat.INTERNAL_SERVER_ERROR_ON_JSON_PROCESSING.format(e.getMessage());
LOG.error(msg, e);
return Response.status(Status.INTERNAL_SERVER_ERROR).entity(msg).build();
} else {
String msg = ErrorMessageFormat.REQUEST_PROCESSING_EXCEPTION.format(e.getMessage());
LOG.debug(msg, e);
return Response.status(BAD_REQUEST).entity(msg).build();
}
}
}
Loading

0 comments on commit 279a162

Please sign in to comment.