-
Notifications
You must be signed in to change notification settings - Fork 980
EVF Tutorial Errors
One of the typical user complaints about Drill (or really any Java-based service) is that errors often include a wonderfully robust set of stack traces, but no actual information about what went wrong in a form that helps the user. You can help your user by providing good error messages. We'll discus two techniques for doing so.
Drill, as a Java application is a bit odd: it does not use the Java checked exception feature. If it did, you'd know what kind of exception to throw when things go wrong. Instead, Drill uses unchecked exceptions. But, which one should you use?
You can actually use any kind you like (IllegalArgumentException
, IllegalStateException
and so on.) EVF will catch such exceptions and translate them to Drill's UserException
class. (The name means, "exception to be sent to the user", not "exception caused by the user.")
UserException
provides context information to help the user understand where things went off the rails. While EVF will try to guess if you use your own exceptions, you can do a better job by throwing UserException
directly.
The log reader we looked at earlier has some nice examples:
throw UserException
.validationError()
.message("Undefined column types")
.addContext("Position", patternIndex)
.addContext("Field name", name)
.addContext("Type", typeName)
.build(logger);
UserException
works by creating a builder for the type of error you want to throw. For a reader, validationError()
or dataReadError()
are the most common. Use validation error if something in the setup fails. Use the read error for problems with external data sources or files. You can also use systemError
if you detect an internal error, such as a violation of an invariant, that indicates a bug in Drill rather than a problem that the user can fix.
The addContext()
methods are key: they allow you to fill in additional information to tell the user what went wrong.
EVF maintains additional context, such as the name of the plugin and the name of the table. You can use that context as follows. Once you've created a ResultSetLoader
, you can obtain the error context from the loader:
throw UserException
.dataReadError(e)
.message("Failed to open input file")
.addContext("File path:", split.getPath())
.addContext(loader.context())
.build(logger);
The ResultSetLoader.context()
provides context for the plugin as a whole, and for the file currently being read. It will fill in information such as the plugin name, the plugin class name, the file name, the user name and so on.
The log format plugin first parses the config, then creates the ResultSetLoader
. In this case, we can use the parent error context: the one that provides just the plugin context. For example:
@Override
public boolean open(FileSchemaNegotiator negotiator) {
split = negotiator.split();
setupPattern(negotiator.parentErrorContext());
...
}
private void setupPattern(CustomErrorContext errorContext) {
try {
// Turns out the only way to learn the capturing group count
// is to create a matcher. We do so with a dummy string.
Matcher m = pattern.matcher("dummy");
capturingGroups = m.groupCount();
} catch (PatternSyntaxException e) {
throw UserException
.validationError(e)
.message("Failed to parse regex: \"%s\"", formatConfig.getRegex())
.addContext(errorContext)
.build(logger);
}
}
(Note: you won't see the above in actual source code: pattern parsing moved into the plugin for other reasons. The above however, illustrates the parent context idea.)
You can customize the context with more information about your plugin or reader. You create an error context object which you pass to the SchemaNegotiator
object in your open()
method:
RowSetContext errorContext;
@Override
public boolean open(FileSchemaNegotiator negotiator) {
CustomErrorContext myContext = new ChildErrorContext(negotiator.parentErrorContext()) {
@Override
public void addContext(UserException.Builder builder) {
super.addContext(builder);
builder.addContext("Your field", yourValue);
}
negotiator.setErrorContext(myContext)
...
The context you just built will be the one that the ResultSetLoader
returns from its context()
method.
This is a bit of an advanced topic; ask on the dev list if you want the details.