Skip to content

UDF Troubleshooting

Paul Rogers edited this page Jan 14, 2018 · 7 revisions

Drill is a production, non-nonsense engine that does not waste time with fancy APIs, detailed error messages or nice-to-haves for developers. With Drill, you are working with a power tool and must respect it on its own terms.

Nowhere is that clearer than when trying to diagnose problems with UDFs. Either you know what you are doing, or you spend hours trying to figure out what you did wrong. The notes here describe the many problems that this author encountered in the hope that they can save you effort if you make the same mistakes.

Drill Won't Find your UDF Class

The Structure of a UDF section mentioned that Drill finds UDFs by their annotations and interface. This is only part of the picture. Drill performs a class scan to locate the classes, but scans only selected packages. To identify which, look for this line in the Drill log file:

13:11:07.064 [main] INFO  o.a.d.common.scanner.BuildTimeScan - 
Loaded prescanned packages [org.apache.drill.exec.store.mock,
org.apache.drill.common.logical, 
org.apache.drill.exec.expr,
...] 
from locations [file:/.../drill/exec/java-exec/target/classes/META-INF/drill-module-scan/registry.json,
...]

The above tells us that Drill looks in only selected packages. Thus, when we are experimenting with functions within Drill source code, we must use one of the above. We do not want to attempt to modify the registry.json files as they are very complex and appear to be generated. Drill provides no configuration options to extend the above programmatically. (As we will see, in a production system, we add our own packages to the list. But, we cannot use those mechanisms here.)

The best solution is to use the package org.apache.drill.exec.expr.contrib as a temporary location.

Debugging Class Path Scan Issues

Unfortunately, Drill does not provide logging for the class path scan mechanism to help you understand why Drill stubbornly ignores your UDF class. However, if you are using source code, you can insert your own debug code. The place to start is in ClassPathScanner:

    @Override
    public void scan(final Object cls) {
      final ClassFile classFile = (ClassFile)cls;
      System.out.println(classFile.getName()); // Add me

The result will be a long list of all the classes that Drill will scan for function definitions. Check that your class appears. If not, check the package name of the class against the list of scanned packages shown above.

It is also helpful to see which classes and functions are actually being loaded. Again, there is no logging, but you can insert debug statements in LocalFunctionRegistry:

  public List<String> validate(String jarName, ScanResult scanResult) {
    ...
    System.out.println("\n\n-- Function Classes\n\n"); // Add me
    for (AnnotatedClassDescriptor func : providerClasses) {
      System.out.println(func.getClassName()); // Add me
      ...
        for (String name : names) {
          String functionName = name.toLowerCase();
          System.out.println(functionName); // Add me

You can also enable WARN level logging in org.apache.drill.exec.expr.fn.FunctionConverter, which will log messages if Drill finds a candidate function class, but cannot use the function class for some reason.

Forcing a Class Scan

Drill has a very clever mechanism to register functions which builds up a list of functions at build time. So, even if you find the right package for your code, Drill will still ignore it unless you run a full build. Unfortunately, that step is done only by Maven, nor your IDE. So, for your function to be seen by Drill when testing from the IDE, you must disable class path caching in your tests:

  @BeforeClass
  public static void setup() throws Exception {
    ClusterFixtureBuilder builder = ClusterFixture.builder(dirTestWatcher)
        .configProperty("drill.classpath.scanning.cache.enabled", false);
    startCluster(builder);
  }

Add Source to Class Path

As we have explained, Drill uses your function source, not the compiled class file, to run your function. You must ensure that your source code is on the class path, along with the class files. This is a particular problem in Eclipse. You will see an error such as:

org.apache.drill.common.exceptions.UserRemoteException: FUNCTION ERROR: Failure reading Function class.

Function Class org.apache.drill.exec.expr.contrib.udfExample.Log2Wrapper

To solve this problem, add your source folders to the class path as discussed in the [Debugging UDFs section.

Non-Annotated Fields Not Allowed

Suppose you were to create a field in your class:

public class Log2Function implements DrillSimpleFunc {
  private double LOG_2;

  public void setup() {
    LOG_2 = Math.log(2.0D);
  }

Drill will fail to read your function and will report the following error in the log:

org.apache.drill.exec.expr.udfExample.Log2Function
00:27:16.706 [main] WARN  org.reflections.Reflections - could not scan file /Users/paul/git/drill/exec/java-exec/target/classes/org/apache/drill/exec/expr/udfExample/Log2Function.class with scanner AnnotationScanner
org.reflections.ReflectionsException: could not create class file from Log2Function.class
	at org.reflections.scanners.AbstractScanner.scan(AbstractScanner.java:30) ~[reflections-0.9.8.jar:na]
	at org.reflections.Reflections.scan(Reflections.java:217) [reflections-0.9.8.jar:na]
...
Caused by: java.lang.NullPointerException: null
	at org.apache.drill.common.scanner.ClassPathScanner$AnnotationScanner.getAnnotationDescriptors(ClassPathScanner.java:286) ~[classes/:na]
	at org.apache.drill.common.scanner.ClassPathScanner$AnnotationScanner.scan(ClassPathScanner.java:278) ~[classes/:na]
	at org.reflections.scanners.AbstractScanner.scan(AbstractScanner.java:28) ~[reflections-0.9.8.jar:na]

The cause is a null-pointer exception (NPE) inside the reflections library on access to the unannotated field.

If you see is error, the reason is that you omitted the required @Workspace annotation. The correct form of the above code is:

public class Log2Function implements DrillSimpleFunc {
  @Workspace private double LOG_2;

Displaying Log Output when Debugging

Drill logging is not readily available when running unit tests from the IDE. However, you can use an alternative mechanism to redirect the log to the console (which is displayed in the IDE.) Use the LoggingFixture within the test harness as follows:

  @Test
  public void testIntegration2() {
    LogFixtureBuilder logBuilder = new LogFixtureBuilder()
        .rootLogger(Level.INFO)
        ;
    try (LogFixture logFixture = logBuilder.build()) {
      String sql = "SELECT log2(4) FROM (VALUES (1))";
      client.queryBuilder().sql(sql).printCsv();
    }
  }

The above sets the root log level to INFO for the duration of the one test. You can also request logging of a specific subsystem or change the log level.

Static Fields (Constants) Not Supported

The the following compiles, but does not work:

public class Log2Function implements DrillSimpleFunc {
  @Workspace public static final double LOG_2 = Math.log(2.0D);

This classic good programming: declare a constant to hold your special numbers. The above works just fine if you test the function outside of Drill. But, when run within Drill, the LOG_2 constant is not set; it defaults to 0, causing the function to return the wrong results.

See the Structure of a UDF section for alternatives.

No Same-Package References

You might want your UDF to use other classes within the same Java package. However, as explained in Structure of a UDF, this will not work and will fail with a runtime failure:

01:17:39.680 [25b0bf82-1dfc-1c92-8460-3bcb6db74f7c:frag:0:0] ERROR o.a.d.e.r.AbstractSingleRecordBatch - Failure during query
org.apache.drill.exec.exception.SchemaChangeException: Failure while attempting to load generated class
...
Caused by: org.apache.drill.exec.exception.ClassTransformationException: java.util.concurrent.ExecutionException: org.apache.drill.exec.exception.ClassTransformationException: Failure generating transformation classes for value: 

Along with several hundred lines of error messages, including multiple copies of the generated code.

See Structure of a UDF for how to use fully-qualified references instead.

No Imports

Similarly, it is common Java practice to import other code if it resides in a package other than the one that holds our code. But, if we do that in a UDF, Drill will fail with an error similar to the one above (since Drill won't copy imports into the generated code.) Again, the workaround is to use fully-qualified references.

Duplicate Function Name

If you accidentally duplicate an existing function, Drill will fail to start. Look for the following error in the log:

Found duplicated function in null: repeat(VARCHAR-REQUIRED,INT-REQUIRED)

Where repeat is the name of the duplicated function the the part in parens are the arguments.

Forgot to Resize an Output VarChar Buffer

If your code returns a VARCHAR or VARBINARY, and you return values larger than 256 bytes, you must remember to resize the buffer. Otherwise, your query will fail with an IndexOutOfBounds exception.

Aggregate Workspace Variables Must Be Holders

Drill does not allow unannotated fields in a UDF. For simple (non-aggregate) functions, the @Workspace annotation can be used to mark a working field of any type (such as int or double). But, for aggregates, all fields must be holders. Example:

  @FunctionTemplate(
      name = "myAvg",
      scope = FunctionScope.POINT_AGGREGATE,
      nulls = NullHandling.NULL_IF_NULL)

  public static class MyAvgFunc implements DrillAggFunc {
    @Param Float8Holder input;
    @Workspace double sum;
    @Workspace int count;
    @Output Float8Holder output;

Produces this error in the log:

18:16:33.593 [main] WARN  o.a.d.exec.expr.fn.FunctionConverter - Failure loading function class
 org.apache.drill.exec.expr.contrib.udfExample.WeightedAverageImpl$MyAvgFunc, field sum.
 Message: Aggregate function 'org.apache.drill.exec.expr.contrib.udfExample.WeightedAverageImpl$MyAvgFunc'
 workspace variable 'sum' is of type 'double'. Please change it to Holder type.

Aggregate Functions Must Do Null Handling

A simple function can tell Drill to handle nulls. But, an aggregate cannot. For example:

  @FunctionTemplate(
      name = "myAvg",
      scope = FunctionScope.POINT_AGGREGATE,
      nulls = NullHandling.NULL_IF_NULL)

  public static class MyAvgFunc implements DrillAggFunc {

Will produce the following error in the log file:

19:05:47.047 [main] WARN  o.a.d.exec.expr.fn.FunctionConverter -
   Failure loading function class [org.apache.drill.exec.expr.contrib.udfExample.WeightedAverageImpl$MyAvgFunc].
   Message: Failure while creating function holder.
java.lang.IllegalArgumentException: An aggregation function is required to do its own null handling.
Clone this wiki locally