blob: bf6508e37e47c110eb8a1c9926b4bd2a246977d0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.drill.common.exceptions;
import static java.lang.Thread.sleep;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.lang.management.ManagementFactory;
import org.apache.drill.exec.proto.CoordinationProtos;
import org.apache.drill.exec.proto.CoordinationProtos.DrillbitEndpoint;
import org.apache.drill.exec.proto.UserBitShared.DrillPBError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for all user exception. The goal is to separate out common error conditions where we can give users
* useful feedback.
* <p>Throwing a user exception will guarantee it's message will be displayed to the user, along with any context
* information added to the exception at various levels while being sent to the client.
* <p>A specific class of user exceptions are system exception. They represent system level errors that don't display
* any specific error message to the user apart from "A system error has occurred" along with information to retrieve
* the details of the exception from the logs.
* <p>Although system exception should only display a generic message to the user, for now they will display the root
* error message, until all user errors are properly sent from the server side.
* <p>Any thrown exception that is not wrapped inside a user exception will automatically be converted to a system
* exception before being sent to the client.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType
*/
public class UserException extends DrillRuntimeException {
private static final long serialVersionUID = -6720929331624621840L;
private static final Logger logger = LoggerFactory.getLogger(UserException.class);
public static final String MEMORY_ERROR_MSG = "One or more nodes ran out of memory while executing the query.";
/**
* Creates a RESOURCE error with a prebuilt message for out of memory exceptions
*
* @param cause exception that will be wrapped inside a memory error
* @return resource error builder
*/
public static Builder memoryError(final Throwable cause) {
return UserException.resourceError(cause)
.message(MEMORY_ERROR_MSG).addContext(cause.getMessage());
}
public static Builder memoryError(final String format, final Object... args) {
final Builder builder = UserException.resourceError();
builder.message(MEMORY_ERROR_MSG);
if (!format.isEmpty()) {
builder.addContext(String.format(format, args));
}
return builder;
}
/**
* Creates a RESOURCE error with a prebuilt message for out of memory exceptions
*
* @return resource error builder
*/
public static Builder memoryError() {
return memoryError("");
}
/**
* Wraps the passed exception inside a system error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#SYSTEM
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder systemError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.SYSTEM, cause);
}
/**
* Creates a new user exception builder.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#CONNECTION
* @return user exception builder
*/
public static Builder connectionError() {
return connectionError(null);
}
/**
* Wraps the passed exception inside a connection error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#CONNECTION
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder connectionError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.CONNECTION, cause);
}
/**
* Creates a new user exception builder.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#DATA_READ
* @return user exception builder
*/
public static Builder dataReadError() {
return dataReadError(null);
}
/**
* Wraps the passed exception inside a data read error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#DATA_READ
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder dataReadError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.DATA_READ, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#DATA_WRITE
* @return user exception builder
*/
public static Builder dataWriteError() {
return dataWriteError(null);
}
/**
* Wraps the passed exception inside a data write error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#DATA_WRITE
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder dataWriteError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.DATA_WRITE, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#FUNCTION
* @return user exception builder
*/
public static Builder functionError() {
return functionError(null);
}
/**
* Wraps the passed exception inside a function error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#FUNCTION
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder functionError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.FUNCTION, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PARSE
* @return user exception builder
*/
public static Builder parseError() {
return parseError(null);
}
/**
* Wraps the passed exception inside a parse error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PARSE
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder parseError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.PARSE, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#VALIDATION
* @return user exception builder
*/
public static Builder validationError() {
return validationError(null);
}
/**
* wraps the passed exception inside a validation error.
* <p>the cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>if the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#VALIDATION
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder validationError(Throwable cause) {
return new Builder(DrillPBError.ErrorType.VALIDATION, cause);
}
/**
* creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PERMISSION
* @return user exception builder
*/
public static Builder permissionError() {
return permissionError(null);
}
/**
* Wraps the passed exception inside a permission error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PERMISSION
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder permissionError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.PERMISSION, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PLAN
* @return user exception builder
*/
public static Builder planError() {
return planError(null);
}
/**
* Wraps the passed exception inside a plan error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#PLAN
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder planError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.PLAN, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#RESOURCE
* @return user exception builder
*/
public static Builder resourceError() {
return resourceError(null);
}
/**
* Wraps the passed exception inside a resource error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#RESOURCE
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder resourceError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.RESOURCE, cause);
}
/**
* Creates a new user exception builder .
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#UNSUPPORTED_OPERATION
* @return user exception builder
*/
public static Builder unsupportedError() {
return unsupportedError(null);
}
/**
* Wraps the passed exception inside a unsupported error.
* <p>The cause message will be used unless {@link Builder#message(String, Object...)} is called.
* <p>If the wrapped exception is, or wraps, a user exception it will be returned by {@link Builder#build(Logger)}
* instead of creating a new exception. Any added context will be added to the user exception as well.
*
* @see org.apache.drill.exec.proto.UserBitShared.DrillPBError.ErrorType#UNSUPPORTED_OPERATION
*
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder unsupportedError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.UNSUPPORTED_OPERATION, cause);
}
/**
* Report an unsupported schema change.
*
* @param cause the <tt>SchemaChangeException</tt>. (Not typed because that
* class is not visible to this package.)
* @return user exception builder.
*/
public static Builder schemaChangeError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.UNSUPPORTED_OPERATION, cause)
.addContext("Unsupported schema change");
}
public static Builder schemaChangeError() {
return schemaChangeError(null);
}
/**
* Wraps an error that arises from execution due to issues in the query, in
* the environment and so on -- anything other than "this should never occur"
* type checks.
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder executionError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.EXECUTION_ERROR, cause);
}
/**
* Indicates an internal validation failed or similar unexpected error. Indicates
* the problem is likely within Drill itself rather than due to the environment,
* query, etc.
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder internalError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.INTERNAL_ERROR, cause);
}
/**
* Indicates an unspecified error: code caught the exception, but does not have
* visibility into the cause well enough to pick one of the more specific
* error types. In practice, using this exception indicates that error handling
* should be moved closer to the source of the exception so we can provide the
* user with a better explanation than "something went wrong."
* @param cause exception we want the user exception to wrap. If cause is, or wrap, a user exception it will be
* returned by the builder instead of creating a new user exception
* @return user exception builder
*/
public static Builder unspecifiedError(final Throwable cause) {
return new Builder(DrillPBError.ErrorType.UNSPECIFIED_ERROR, cause);
}
/**
* Builder class for DrillUserException. You can wrap an existing exception, in this case it will first check if
* this exception is, or wraps, a DrillUserException. If it does then the builder will use the user exception as it is
* (it will ignore the message passed to the constructor) and will add any additional context information to the
* exception's context
*/
public static class Builder {
private final Throwable cause;
private final DrillPBError.ErrorType errorType;
private final UserException uex;
private final UserExceptionContext context;
private String message;
/**
* Wraps an existing exception inside a user exception.
*
* @param errorType user exception type that should be created if the passed exception isn't,
* or doesn't wrap a user exception
* @param cause exception to wrap inside a user exception. Can be null
*/
private Builder(final DrillPBError.ErrorType errorType, final Throwable cause) {
this.cause = cause;
//TODO handle the improbable case where cause is a SYSTEM exception ?
uex = ErrorHelper.findWrappedUserException(cause);
if (uex != null) {
this.errorType = null;
this.context = uex.context;
} else {
// we will create a new user exception
this.errorType = errorType;
this.context = new UserExceptionContext();
this.message = cause != null ? cause.getMessage() : null;
}
}
private Builder(UserException uex) {
this.uex = uex;
cause = uex.getCause();
errorType = uex.errorType;
context = uex.context;
message = uex.getOriginalMessage();
}
/**
* sets or replaces the error message.
* <p>This will be ignored if this builder is wrapping a user exception
*
* @see String#format(String, Object...)
*
* @param format format string
* @param args Arguments referenced by the format specifiers in the format string
* @return this builder
*/
public Builder message(final String format, final Object... args) {
// we can't replace the message of a user exception
if (uex == null && format != null) {
if (args.length == 0) {
message = format;
} else {
message = String.format(format, args);
}
}
return this;
}
/**
* add DrillbitEndpoint identity to the context.
* <p>if the context already has a drillbitEndpoint identity, the new identity will be ignored
*
* @param endpoint drillbit endpoint identity
*/
public Builder addIdentity(final CoordinationProtos.DrillbitEndpoint endpoint) {
context.add(endpoint);
return this;
}
/**
* add a string line to the bottom of the context
* @param value string line
* @return this builder
*/
public Builder addContext(final String value) {
context.add(value);
return this;
}
/**
* add a string line to the bottom of the context
* @param value string line
* @return this builder
*/
public Builder addContext(final String value, Object... args) {
context.add(String.format(value, args));
return this;
}
/**
* add a string value to the bottom of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder addContext(final String name, final String value) {
context.add(name, value);
return this;
}
/**
* add a long value to the bottom of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder addContext(final String name, final long value) {
context.add(name, value);
return this;
}
/**
* add a double value to the bottom of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder addContext(final String name, final double value) {
context.add(name, value);
return this;
}
public Builder addContext(CustomErrorContext context) {
if (context != null) {
context.addContext(this);
}
return this;
}
/**
* pushes a string value to the top of the context
*
* @param value context value
* @return this builder
*/
public Builder pushContext(final String value) {
context.push(value);
return this;
}
/**
* pushes a string value to the top of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder pushContext(final String name, final String value) {
context.push(name, value);
return this;
}
/**
* pushes a long value to the top of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder pushContext(final String name, final long value) {
context.push(name, value);
return this;
}
/**
* pushes a double value to the top of the context
*
* @param name context name
* @param value context value
* @return this builder
*/
public Builder pushContext(final String name, final double value) {
context.push(name, value);
return this;
}
private static final File SPIN_FILE = new File("/tmp/drill/spin");
/**
* builds a user exception or returns the wrapped one. If the error is a system error, the error message is logged
* to the given {@link Logger}.
*
* @param logger the logger to write to
* @return user exception
*/
public UserException build(final Logger logger) {
// To allow for debugging:
//
// A spinner code to make the execution stop here while the file '/tmp/drill/spin' exists
// Can be used to attach a debugger, use jstack, etc
// (do "clush -a touch /tmp/drill/spin" to turn this on across all the cluster nodes, and to
// release the spinning threads do "clush -a rm /tmp/drill/spin")
// The processID of the spinning thread (along with the error message) should then be found
// in a file like /tmp/drill/spin4148663301172491613.tmp
if (SPIN_FILE.exists()) {
final File tmpDir = new File("/tmp/drill");
File outErr = null;
try {
outErr = File.createTempFile("spin", ".tmp", tmpDir);
final BufferedWriter bw = new BufferedWriter(new FileWriter(outErr));
bw.write("Spinning process: " + ManagementFactory.getRuntimeMXBean().getName()
/* After upgrading to JDK 9 - replace with: ProcessHandle.current().getPid() */);
bw.write("\nError cause: " +
(errorType == DrillPBError.ErrorType.SYSTEM ? ("SYSTEM ERROR: " + ErrorHelper.getRootMessage(cause)) : message));
bw.close();
} catch (final Exception ex) {
logger.warn("Failed creating a spinner tmp message file: {}", ex);
}
while (SPIN_FILE.exists()) {
try { sleep(1_000); } catch (final Exception ex) { /* ignore interruptions */ }
}
try { outErr.delete(); } catch (final Exception ex) { } // cleanup - remove err msg file
}
if (uex != null) {
return uex;
}
final boolean isSystemError = errorType == DrillPBError.ErrorType.SYSTEM;
// make sure system errors use the root error message and display the root cause class name
if (isSystemError) {
message = ErrorHelper.getRootMessage(cause);
}
final UserException newException = new UserException(this);
// since we just created a new exception, we should log it for later reference. If this is a system error, this is
// an issue that the Drill admin should pay attention to and we should log as ERROR. However, if this is a user
// mistake or data read issue, the system admin should not be concerned about these and thus we'll log this
// as an INFO message.
if (isSystemError) {
logger.error(newException.getMessage(), newException);
} else {
final StringBuilder buf = new StringBuilder();
buf.append("User Error Occurred");
if (message != null) {
buf.append(": ").append(message);
}
if (cause != null) {
buf.append(" (").append(cause.getMessage()).append(")");
}
logger.info(buf.toString(), newException);
}
return newException;
}
/**
* Builds a user exception or returns the wrapped one.
*
* @return user exception
* @deprecated Use {@link #build(Logger)} instead. If the error is a system error, the error message is logged to
* this {@link UserException#logger}.
*/
@Deprecated
public UserException build() {
return build(logger);
}
}
private final DrillPBError.ErrorType errorType;
private final UserExceptionContext context;
protected UserException(final DrillPBError.ErrorType errorType, final String message, final Throwable cause) {
super(message, cause);
this.errorType = errorType;
this.context = new UserExceptionContext();
}
private UserException(final Builder builder) {
super(builder.message, builder.cause);
this.errorType = builder.errorType;
this.context = builder.context;
}
public Builder rebuild() {
return new Builder(this);
}
/**
* generates the message that will be displayed to the client without the stack trace.
*
* @return non verbose error message
*/
@Override
public String getMessage() {
return generateMessage(true);
}
public String getMessage(boolean includeErrorIdAndIdentity) {
return generateMessage(includeErrorIdAndIdentity);
}
/**
*
* @return the error message that was passed to the builder
*/
public String getOriginalMessage() {
return super.getMessage();
}
/**
* generates the message that will be displayed to the client. The message also contains the stack trace.
*
* @return verbose error message
*/
public String getVerboseMessage() {
return getVerboseMessage(true);
}
public String getVerboseMessage(boolean includeErrorIdAndIdentity) {
return generateMessage(includeErrorIdAndIdentity) + "\n\n" + ErrorHelper.buildCausesMessage(getCause());
}
/**
* returns or creates a DrillPBError object corresponding to this user exception.
*
* @param verbose should the error object contain the verbose error message ?
* @return protobuf error object
*/
public DrillPBError getOrCreatePBError(final boolean verbose) {
final String message = verbose ? getVerboseMessage() : getMessage();
final DrillPBError.Builder builder = DrillPBError.newBuilder();
builder.setErrorType(errorType);
builder.setErrorId(context.getErrorId());
if (context.getEndpoint() != null) {
builder.setEndpoint(context.getEndpoint());
}
builder.setMessage(message);
if (getCause() != null) {
// some unit tests use this information to make sure a specific exception was thrown in the server
builder.setException(ErrorHelper.getWrapper(getCause()));
} else {
// not a wrapper exception
builder.setException(ErrorHelper.getWrapper(this));
}
return builder.build();
}
public String getErrorId() {
return context.getErrorId();
}
public DrillPBError.ErrorType getErrorType() {
return errorType;
}
public String getErrorLocation() {
final DrillbitEndpoint ep = context.getEndpoint();
if (ep != null) {
return ep.getAddress() + ":" + ep.getUserPort();
} else {
return null;
}
}
/**
* Generates a user error message that has the following structure:
* ERROR TYPE: ERROR_MESSAGE
* CONTEXT
* [ERROR_ID on DRILLBIT_IP:DRILLBIT_USER_PORT]
*
* @return generated user error message
*/
private String generateMessage(boolean includeErrorIdAndIdentity) {
final boolean seeLogsMessage = errorType == DrillPBError.ErrorType.INTERNAL_ERROR
|| errorType == DrillPBError.ErrorType.SYSTEM;
return errorType + " ERROR: " + super.getMessage() + "\n\n" +
context.generateContextMessage(includeErrorIdAndIdentity, seeLogsMessage);
}
}