org.apache.drill.TestDynamicUDFSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.drill.TestDynamicUDFSupport.java

Source

/*
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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;

import com.google.common.collect.Lists;
import mockit.Deencapsulation;
import org.apache.drill.common.config.CommonConstants;
import org.apache.drill.common.config.DrillConfig;
import org.apache.drill.common.exceptions.UserRemoteException;
import org.apache.drill.common.util.TestTools;
import org.apache.drill.exec.exception.VersionMismatchException;
import org.apache.drill.exec.expr.fn.FunctionImplementationRegistry;
import org.apache.drill.exec.expr.fn.registry.LocalFunctionRegistry;
import org.apache.drill.exec.expr.fn.registry.RemoteFunctionRegistry;
import org.apache.drill.exec.proto.UserBitShared.Jar;
import org.apache.drill.exec.proto.UserBitShared.Registry;
import org.apache.drill.exec.server.DrillbitContext;
import org.apache.drill.exec.store.sys.store.DataChangeVersion;
import org.apache.drill.exec.util.JarUtil;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;

import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@RunWith(MockitoJUnitRunner.class)
public class TestDynamicUDFSupport extends BaseTestQuery {

    private static final File jars = new File(TestTools.getWorkingPath() + "/src/test/resources/jars");
    private static final String default_binary_name = "DrillUDF-1.0.jar";
    private static final String default_source_name = JarUtil.getSourceName(default_binary_name);

    @Rule
    public final TemporaryFolder base = new TemporaryFolder();

    @Before
    public void setup() {
        Properties overrideProps = new Properties();
        overrideProps.setProperty("drill.exec.udf.directory.root", base.getRoot().getPath());
        overrideProps.setProperty("drill.tmp-dir", base.getRoot().getPath());
        updateTestCluster(1, DrillConfig.create(overrideProps));
    }

    @Test
    public void testSyntax() throws Exception {
        test("create function using jar 'jar_name.jar'");
        test("drop function using jar 'jar_name.jar'");
    }

    @Test
    public void testEnableDynamicSupport() throws Exception {
        try {
            test("alter system set `exec.udf.enable_dynamic_support` = true");
            test("create function using jar 'jar_name.jar'");
            test("drop function using jar 'jar_name.jar'");
        } finally {
            test("alter system reset `exec.udf.enable_dynamic_support`");
        }
    }

    @Test
    public void testDisableDynamicSupport() throws Exception {
        try {
            test("alter system set `exec.udf.enable_dynamic_support` = false");
            String[] actions = new String[] { "create", "drop" };
            String query = "%s function using jar 'jar_name.jar'";
            for (String action : actions) {
                try {
                    test(query, action);
                } catch (UserRemoteException e) {
                    assertThat(e.getMessage(), containsString("Dynamic UDFs support is disabled."));
                }
            }
        } finally {
            test("alter system reset `exec.udf.enable_dynamic_support`");
        }
    }

    @Test
    public void testAbsentBinaryInStaging() throws Exception {
        Path staging = getDrillbitContext().getRemoteFunctionRegistry().getStagingArea();

        String summary = String.format("File %s does not exist",
                new Path(staging, default_binary_name).toUri().getPath());

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, summary).go();
    }

    @Test
    public void testAbsentSourceInStaging() throws Exception {
        Path staging = getDrillbitContext().getRemoteFunctionRegistry().getStagingArea();
        copyJar(getDrillbitContext().getRemoteFunctionRegistry().getFs(), new Path(jars.toURI()), staging,
                default_binary_name);

        String summary = String.format("File %s does not exist",
                new Path(staging, default_source_name).toUri().getPath());

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, summary).go();
    }

    @Test
    public void testJarWithoutMarkerFile() throws Exception {
        String jarWithNoMarkerFile = "DrillUDF_NoMarkerFile-1.0.jar";
        copyJarsToStagingArea(jarWithNoMarkerFile, JarUtil.getSourceName(jarWithNoMarkerFile));

        String summary = "Marker file %s is missing in %s";

        testBuilder().sqlQuery("create function using jar '%s'", jarWithNoMarkerFile).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary,
                        CommonConstants.DRILL_JAR_MARKER_FILE_RESOURCE_PATHNAME, jarWithNoMarkerFile))
                .go();
    }

    @Test
    public void testJarWithoutFunctions() throws Exception {
        String jarWithNoFunctions = "DrillUDF_Empty-1.0.jar";
        copyJarsToStagingArea(jarWithNoFunctions, JarUtil.getSourceName(jarWithNoFunctions));

        String summary = "Jar %s does not contain functions";

        testBuilder().sqlQuery("create function using jar '%s'", jarWithNoFunctions).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary, jarWithNoFunctions))
                .go();
    }

    @Test
    public void testSuccessfulRegistration() throws Exception {
        copyDefaultJarsToStagingArea();

        String summary = "The following UDFs in jar %s have been registered:\n"
                + "[custom_lower(VARCHAR-REQUIRED)]";

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(true, String.format(summary, default_binary_name))
                .go();

        RemoteFunctionRegistry remoteFunctionRegistry = getDrillbitContext().getRemoteFunctionRegistry();
        FileSystem fs = remoteFunctionRegistry.getFs();

        assertFalse("Staging area should be empty",
                fs.listFiles(remoteFunctionRegistry.getStagingArea(), false).hasNext());
        assertFalse("Temporary area should be empty",
                fs.listFiles(remoteFunctionRegistry.getTmpArea(), false).hasNext());

        assertTrue("Binary should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_binary_name)));
        assertTrue("Source should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_source_name)));

        Registry registry = remoteFunctionRegistry.getRegistry(new DataChangeVersion());
        assertEquals("Registry should contain one jar", registry.getJarList().size(), 1);
        assertEquals(registry.getJar(0).getName(), default_binary_name);
    }

    @Test
    public void testDuplicatedJarInRemoteRegistry() throws Exception {
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        copyDefaultJarsToStagingArea();

        String summary = "Jar with %s name has been already registered";

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary, default_binary_name))
                .go();
    }

    @Test
    public void testDuplicatedJarInLocalRegistry() throws Exception {
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        test("select custom_lower('A') from (values(1))");
        copyDefaultJarsToStagingArea();

        String summary = "Jar with %s name has been already registered";

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary, default_binary_name))
                .go();
    }

    @Test
    public void testDuplicatedFunctionsInRemoteRegistry() throws Exception {
        String jarWithDuplicate = "DrillUDF_Copy-1.0.jar";
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        copyJarsToStagingArea(jarWithDuplicate, JarUtil.getSourceName(jarWithDuplicate));

        String summary = "Found duplicated function in %s: custom_lower(VARCHAR-REQUIRED)";

        testBuilder().sqlQuery("create function using jar '%s'", jarWithDuplicate).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary, default_binary_name))
                .go();
    }

    @Test
    public void testDuplicatedFunctionsInLocalRegistry() throws Exception {
        String jarWithDuplicate = "DrillUDF_DupFunc-1.0.jar";
        copyJarsToStagingArea(jarWithDuplicate, JarUtil.getSourceName(jarWithDuplicate));

        String summary = "Found duplicated function in %s: lower(VARCHAR-REQUIRED)";

        testBuilder().sqlQuery("create function using jar '%s'", jarWithDuplicate).unOrdered()
                .baselineColumns("ok", "summary")
                .baselineValues(false, String.format(summary, LocalFunctionRegistry.BUILT_IN)).go();
    }

    @Test
    public void testSuccessfulRegistrationAfterSeveralRetryAttempts() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        copyDefaultJarsToStagingArea();

        doThrow(new VersionMismatchException("Version mismatch detected", 1))
                .doThrow(new VersionMismatchException("Version mismatch detected", 1)).doCallRealMethod()
                .when(remoteFunctionRegistry).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        String summary = "The following UDFs in jar %s have been registered:\n"
                + "[custom_lower(VARCHAR-REQUIRED)]";

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(true, String.format(summary, default_binary_name))
                .go();

        verify(remoteFunctionRegistry, times(3)).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        FileSystem fs = remoteFunctionRegistry.getFs();

        assertFalse("Staging area should be empty",
                fs.listFiles(remoteFunctionRegistry.getStagingArea(), false).hasNext());
        assertFalse("Temporary area should be empty",
                fs.listFiles(remoteFunctionRegistry.getTmpArea(), false).hasNext());

        assertTrue("Binary should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_binary_name)));
        assertTrue("Source should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_source_name)));

        Registry registry = remoteFunctionRegistry.getRegistry(new DataChangeVersion());
        assertEquals("Registry should contain one jar", registry.getJarList().size(), 1);
        assertEquals(registry.getJar(0).getName(), default_binary_name);
    }

    @Test
    public void testSuccessfulUnregistrationAfterSeveralRetryAttempts() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);

        reset(remoteFunctionRegistry);
        doThrow(new VersionMismatchException("Version mismatch detected", 1))
                .doThrow(new VersionMismatchException("Version mismatch detected", 1)).doCallRealMethod()
                .when(remoteFunctionRegistry).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        String summary = "The following UDFs in jar %s have been unregistered:\n"
                + "[custom_lower(VARCHAR-REQUIRED)]";

        testBuilder().sqlQuery("drop function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(true, String.format(summary, default_binary_name))
                .go();

        verify(remoteFunctionRegistry, times(3)).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        FileSystem fs = remoteFunctionRegistry.getFs();

        assertFalse("Registry area should be empty",
                fs.listFiles(remoteFunctionRegistry.getRegistryArea(), false).hasNext());
        assertEquals("Registry should be empty",
                remoteFunctionRegistry.getRegistry(new DataChangeVersion()).getJarList().size(), 0);
    }

    @Test
    public void testExceedRetryAttemptsDuringRegistration() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        copyDefaultJarsToStagingArea();

        doThrow(new VersionMismatchException("Version mismatch detected", 1)).when(remoteFunctionRegistry)
                .updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        String summary = "Failed to update remote function registry. Exceeded retry attempts limit.";

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, summary).go();

        verify(remoteFunctionRegistry, times(remoteFunctionRegistry.getRetryAttempts() + 1))
                .updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        FileSystem fs = remoteFunctionRegistry.getFs();

        assertTrue("Binary should be present in staging area",
                fs.exists(new Path(remoteFunctionRegistry.getStagingArea(), default_binary_name)));
        assertTrue("Source should be present in staging area",
                fs.exists(new Path(remoteFunctionRegistry.getStagingArea(), default_source_name)));

        assertFalse("Registry area should be empty",
                fs.listFiles(remoteFunctionRegistry.getRegistryArea(), false).hasNext());
        assertFalse("Temporary area should be empty",
                fs.listFiles(remoteFunctionRegistry.getTmpArea(), false).hasNext());

        assertEquals("Registry should be empty",
                remoteFunctionRegistry.getRegistry(new DataChangeVersion()).getJarList().size(), 0);
    }

    @Test
    public void testExceedRetryAttemptsDuringUnregistration() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);

        reset(remoteFunctionRegistry);
        doThrow(new VersionMismatchException("Version mismatch detected", 1)).when(remoteFunctionRegistry)
                .updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        String summary = "Failed to update remote function registry. Exceeded retry attempts limit.";

        testBuilder().sqlQuery("drop function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, summary).go();

        verify(remoteFunctionRegistry, times(remoteFunctionRegistry.getRetryAttempts() + 1))
                .updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        FileSystem fs = remoteFunctionRegistry.getFs();

        assertTrue("Binary should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_binary_name)));
        assertTrue("Source should be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_source_name)));

        Registry registry = remoteFunctionRegistry.getRegistry(new DataChangeVersion());
        assertEquals("Registry should contain one jar", registry.getJarList().size(), 1);
        assertEquals(registry.getJar(0).getName(), default_binary_name);
    }

    @Test
    public void testLazyInit() throws Exception {
        try {
            test("select custom_lower('A') from (values(1))");
        } catch (UserRemoteException e) {
            assertThat(e.getMessage(),
                    containsString("No match found for function signature custom_lower(<CHARACTER>)"));
        }

        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        testBuilder().sqlQuery("select custom_lower('A') as res from (values(1))").unOrdered()
                .baselineColumns("res").baselineValues("a").go();

        Path localUdfDirPath = Deencapsulation.getField(getDrillbitContext().getFunctionImplementationRegistry(),
                "localUdfDir");
        File localUdfDir = new File(localUdfDirPath.toUri().getPath());

        assertTrue("Binary should exist in local udf directory",
                new File(localUdfDir, default_binary_name).exists());
        assertTrue("Source should exist in local udf directory",
                new File(localUdfDir, default_source_name).exists());
    }

    @Test
    public void testLazyInitWhenDynamicUdfSupportIsDisabled() throws Exception {
        try {
            test("select custom_lower('A') from (values(1))");
        } catch (UserRemoteException e) {
            assertThat(e.getMessage(),
                    containsString("No match found for function signature custom_lower(<CHARACTER>)"));
        }

        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);

        try {
            testBuilder().sqlQuery("select custom_lower('A') as res from (values(1))")
                    .optionSettingQueriesForTestQuery("alter system set `exec.udf.enable_dynamic_support` = false")
                    .unOrdered().baselineColumns("res").baselineValues("a").go();
        } finally {
            test("alter system reset `exec.udf.enable_dynamic_support`");
        }
    }

    @Test
    public void testOverloadedFunctionPlanningStage() throws Exception {
        String jarName = "DrillUDF-overloading-1.0.jar";
        copyJarsToStagingArea(jarName, JarUtil.getSourceName(jarName));
        test("create function using jar '%s'", jarName);

        testBuilder().sqlQuery("select abs('A', 'A') as res from (values(1))").unOrdered().baselineColumns("res")
                .baselineValues("ABS was overloaded. Input: A, A").go();
    }

    @Test
    public void testOverloadedFunctionExecutionStage() throws Exception {
        String jarName = "DrillUDF-overloading-1.0.jar";
        copyJarsToStagingArea(jarName, JarUtil.getSourceName(jarName));
        test("create function using jar '%s'", jarName);

        testBuilder().sqlQuery("select log('A') as res from (values(1))").unOrdered().baselineColumns("res")
                .baselineValues("LOG was overloaded. Input: A").go();
    }

    @Test
    public void testDropFunction() throws Exception {
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        test("select custom_lower('A') from (values(1))");

        Path localUdfDirPath = Deencapsulation.getField(getDrillbitContext().getFunctionImplementationRegistry(),
                "localUdfDir");
        File localUdfDir = new File(localUdfDirPath.toUri().getPath());

        assertTrue("Binary should exist in local udf directory",
                new File(localUdfDir, default_binary_name).exists());
        assertTrue("Source should exist in local udf directory",
                new File(localUdfDir, default_source_name).exists());

        String summary = "The following UDFs in jar %s have been unregistered:\n"
                + "[custom_lower(VARCHAR-REQUIRED)]";

        testBuilder().sqlQuery("drop function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(true, String.format(summary, default_binary_name))
                .go();

        try {
            test("select custom_lower('A') from (values(1))");
        } catch (UserRemoteException e) {
            assertThat(e.getMessage(),
                    containsString("No match found for function signature custom_lower(<CHARACTER>)"));
        }

        RemoteFunctionRegistry remoteFunctionRegistry = getDrillbitContext().getRemoteFunctionRegistry();
        assertEquals("Remote registry should be empty",
                remoteFunctionRegistry.getRegistry(new DataChangeVersion()).getJarList().size(), 0);

        FileSystem fs = remoteFunctionRegistry.getFs();
        assertFalse("Binary should not be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_binary_name)));
        assertFalse("Source should not be present in registry area",
                fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_source_name)));

        assertFalse("Binary should not be present in local udf directory",
                new File(localUdfDir, default_binary_name).exists());
        assertFalse("Source should not be present in local udf directory",
                new File(localUdfDir, default_source_name).exists());
    }

    @Test
    public void testReRegisterTheSameJarWithDifferentContent() throws Exception {
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);
        testBuilder().sqlQuery("select custom_lower('A') as res from (values(1))").unOrdered()
                .baselineColumns("res").baselineValues("a").go();
        test("drop function using jar '%s'", default_binary_name);

        Thread.sleep(1000);

        Path src = new Path(jars.toURI().getPath(), "v2");
        copyJarsToStagingArea(src, default_binary_name, default_source_name);
        test("create function using jar '%s'", default_binary_name);
        testBuilder().sqlQuery("select custom_lower('A') as res from (values(1))").unOrdered()
                .baselineColumns("res").baselineValues("a_v2").go();
    }

    @Test
    public void testDropAbsentJar() throws Exception {
        String summary = "Jar %s is not registered in remote registry";

        testBuilder().sqlQuery("drop function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String.format(summary, default_binary_name))
                .go();
    }

    @Test
    public void testRegistrationFailDuringRegistryUpdate() throws Exception {
        final RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        final FileSystem fs = remoteFunctionRegistry.getFs();
        final String errorMessage = "Failure during remote registry update.";
        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                assertTrue("Binary should be present in registry area",
                        fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_binary_name)));
                assertTrue("Source should be present in registry area",
                        fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), default_source_name)));
                throw new RuntimeException(errorMessage);
            }
        }).when(remoteFunctionRegistry).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        copyDefaultJarsToStagingArea();

        testBuilder().sqlQuery("create function using jar '%s'", default_binary_name).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, errorMessage).go();

        assertFalse("Registry area should be empty",
                fs.listFiles(remoteFunctionRegistry.getRegistryArea(), false).hasNext());
        assertFalse("Temporary area should be empty",
                fs.listFiles(remoteFunctionRegistry.getTmpArea(), false).hasNext());

        assertTrue("Binary should be present in staging area",
                fs.exists(new Path(remoteFunctionRegistry.getStagingArea(), default_binary_name)));
        assertTrue("Source should be present in staging area",
                fs.exists(new Path(remoteFunctionRegistry.getStagingArea(), default_source_name)));
    }

    @Test
    public void testConcurrentRegistrationOfTheSameJar() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();

        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(1);

        doAnswer(new Answer<String>() {
            @Override
            public String answer(InvocationOnMock invocation) throws Throwable {
                String result = (String) invocation.callRealMethod();
                latch2.countDown();
                latch1.await();
                return result;
            }
        }).doCallRealMethod().doCallRealMethod().when(remoteFunctionRegistry).addToJars(anyString(),
                any(RemoteFunctionRegistry.Action.class));

        final String query = String.format("create function using jar '%s'", default_binary_name);

        Thread thread = new Thread(new SimpleQueryRunner(query));
        thread.start();
        latch2.await();

        try {
            String summary = "Jar with %s name is used. Action: REGISTRATION";

            testBuilder().sqlQuery(query).unOrdered().baselineColumns("ok", "summary")
                    .baselineValues(false, String.format(summary, default_binary_name)).go();

            testBuilder().sqlQuery("drop function using jar '%s'", default_binary_name).unOrdered()
                    .baselineColumns("ok", "summary")
                    .baselineValues(false, String.format(summary, default_binary_name)).go();

        } finally {
            latch1.countDown();
            thread.join();
        }
    }

    @Test
    public void testConcurrentRemoteRegistryUpdateWithDuplicates() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();

        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(1);
        final CountDownLatch latch3 = new CountDownLatch(1);

        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch3.countDown();
                latch1.await();
                invocation.callRealMethod();
                latch2.countDown();
                return null;
            }
        }).doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch1.countDown();
                latch2.await();
                invocation.callRealMethod();
                return null;
            }
        }).when(remoteFunctionRegistry).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        final String jarName1 = default_binary_name;
        final String jarName2 = "DrillUDF_Copy-1.0.jar";
        final String query = "create function using jar '%s'";

        copyDefaultJarsToStagingArea();
        copyJarsToStagingArea(jarName2, JarUtil.getSourceName(jarName2));

        Thread thread1 = new Thread(new TestBuilderRunner(testBuilder().sqlQuery(query, jarName1).unOrdered()
                .baselineColumns("ok", "summary")
                .baselineValues(true, String.format(
                        "The following UDFs in jar %s have been registered:\n" + "[custom_lower(VARCHAR-REQUIRED)]",
                        jarName1))));

        Thread thread2 = new Thread(new TestBuilderRunner(testBuilder().sqlQuery(query, jarName2).unOrdered()
                .baselineColumns("ok", "summary").baselineValues(false, String
                        .format("Found duplicated function in %s: custom_lower(VARCHAR-REQUIRED)", jarName1))));

        thread1.start();
        latch3.await();
        thread2.start();

        thread1.join();
        thread2.join();

        DataChangeVersion version = new DataChangeVersion();
        Registry registry = remoteFunctionRegistry.getRegistry(version);
        assertEquals("Remote registry version should match", 1, version.getVersion());
        List<Jar> jarList = registry.getJarList();
        assertEquals("Only one jar should be registered", 1, jarList.size());
        assertEquals("Jar name should match", jarName1, jarList.get(0).getName());

        verify(remoteFunctionRegistry, times(2)).updateRegistry(any(Registry.class), any(DataChangeVersion.class));
    }

    @Test
    public void testConcurrentRemoteRegistryUpdateForDifferentJars() throws Exception {
        RemoteFunctionRegistry remoteFunctionRegistry = spyRemoteFunctionRegistry();
        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(2);

        doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) throws Throwable {
                latch2.countDown();
                latch1.await();
                invocation.callRealMethod();
                return null;
            }
        }).when(remoteFunctionRegistry).updateRegistry(any(Registry.class), any(DataChangeVersion.class));

        final String jarName1 = default_binary_name;
        final String jarName2 = "DrillUDF-2.0.jar";
        final String query = "create function using jar '%s'";

        copyDefaultJarsToStagingArea();
        copyJarsToStagingArea(jarName2, JarUtil.getSourceName(jarName2));

        Thread thread1 = new Thread(new TestBuilderRunner(testBuilder().sqlQuery(query, jarName1).unOrdered()
                .baselineColumns("ok", "summary")
                .baselineValues(true, String.format(
                        "The following UDFs in jar %s have been registered:\n" + "[custom_lower(VARCHAR-REQUIRED)]",
                        jarName1))));

        Thread thread2 = new Thread(new TestBuilderRunner(testBuilder().sqlQuery(query, jarName2).unOrdered()
                .baselineColumns("ok", "summary")
                .baselineValues(true, String.format(
                        "The following UDFs in jar %s have been registered:\n" + "[custom_upper(VARCHAR-REQUIRED)]",
                        jarName2))));

        thread1.start();
        thread2.start();

        latch2.await();
        latch1.countDown();

        thread1.join();
        thread2.join();

        DataChangeVersion version = new DataChangeVersion();
        Registry registry = remoteFunctionRegistry.getRegistry(version);
        assertEquals("Remote registry version should match", 2, version.getVersion());

        List<Jar> actualJars = registry.getJarList();
        List<String> expectedJars = Lists.newArrayList(jarName1, jarName2);

        assertEquals("Only one jar should be registered", 2, actualJars.size());
        for (Jar jar : actualJars) {
            assertTrue("Jar should be present in remote function registry", expectedJars.contains(jar.getName()));
        }

        verify(remoteFunctionRegistry, times(3)).updateRegistry(any(Registry.class), any(DataChangeVersion.class));
    }

    @Test
    public void testLazyInitConcurrent() throws Exception {
        FunctionImplementationRegistry functionImplementationRegistry = spyFunctionImplementationRegistry();
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);

        final CountDownLatch latch1 = new CountDownLatch(1);
        final CountDownLatch latch2 = new CountDownLatch(1);

        final String query = "select custom_lower('A') from (values(1))";

        doAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                latch1.await();
                boolean result = (boolean) invocation.callRealMethod();
                assertTrue("syncWithRemoteRegistry() should return true", result);
                latch2.countDown();
                return true;
            }
        }).doAnswer(new Answer() {
            @Override
            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                latch1.countDown();
                latch2.await();
                boolean result = (boolean) invocation.callRealMethod();
                assertTrue("syncWithRemoteRegistry() should return true", result);
                return true;
            }
        }).when(functionImplementationRegistry).syncWithRemoteRegistry(anyLong());

        SimpleQueryRunner simpleQueryRunner = new SimpleQueryRunner(query);
        Thread thread1 = new Thread(simpleQueryRunner);
        Thread thread2 = new Thread(simpleQueryRunner);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        verify(functionImplementationRegistry, times(2)).syncWithRemoteRegistry(anyLong());
        LocalFunctionRegistry localFunctionRegistry = Deencapsulation.getField(functionImplementationRegistry,
                "localFunctionRegistry");
        assertEquals("Sync function registry version should match", 1L, localFunctionRegistry.getVersion());
    }

    @Test
    public void testLazyInitNoReload() throws Exception {
        FunctionImplementationRegistry functionImplementationRegistry = spyFunctionImplementationRegistry();
        copyDefaultJarsToStagingArea();
        test("create function using jar '%s'", default_binary_name);

        doAnswer(new Answer<Boolean>() {
            @Override
            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                boolean result = (boolean) invocation.callRealMethod();
                assertTrue("syncWithRemoteRegistry() should return true", result);
                return true;
            }
        }).doAnswer(new Answer() {
            @Override
            public Boolean answer(InvocationOnMock invocation) throws Throwable {
                boolean result = (boolean) invocation.callRealMethod();
                assertFalse("syncWithRemoteRegistry() should return false", result);
                return false;
            }
        }).when(functionImplementationRegistry).syncWithRemoteRegistry(anyLong());

        test("select custom_lower('A') from (values(1))");

        try {
            test("select unknown_lower('A') from (values(1))");
        } catch (UserRemoteException e) {
            assertThat(e.getMessage(),
                    containsString("No match found for function signature unknown_lower(<CHARACTER>)"));
        }

        verify(functionImplementationRegistry, times(2)).syncWithRemoteRegistry(anyLong());
        LocalFunctionRegistry localFunctionRegistry = Deencapsulation.getField(functionImplementationRegistry,
                "localFunctionRegistry");
        assertEquals("Sync function registry version should match", 1L, localFunctionRegistry.getVersion());
    }

    private void copyDefaultJarsToStagingArea() throws IOException {
        copyJarsToStagingArea(new Path(jars.toURI()), default_binary_name, default_source_name);
    }

    private void copyJarsToStagingArea(String binaryName, String sourceName) throws IOException {
        copyJarsToStagingArea(new Path(jars.toURI()), binaryName, sourceName);
    }

    private void copyJarsToStagingArea(Path src, String binaryName, String sourceName) throws IOException {
        RemoteFunctionRegistry remoteFunctionRegistry = getDrillbitContext().getRemoteFunctionRegistry();
        copyJar(remoteFunctionRegistry.getFs(), src, remoteFunctionRegistry.getStagingArea(), binaryName);
        copyJar(remoteFunctionRegistry.getFs(), src, remoteFunctionRegistry.getStagingArea(), sourceName);
    }

    private void copyJar(FileSystem fs, Path src, Path dest, String name) throws IOException {
        Path jarPath = new Path(src, name);
        fs.copyFromLocalFile(jarPath, dest);
    }

    private RemoteFunctionRegistry spyRemoteFunctionRegistry() {
        FunctionImplementationRegistry functionImplementationRegistry = getDrillbitContext()
                .getFunctionImplementationRegistry();
        RemoteFunctionRegistry remoteFunctionRegistry = functionImplementationRegistry.getRemoteFunctionRegistry();
        RemoteFunctionRegistry spy = spy(remoteFunctionRegistry);
        Deencapsulation.setField(functionImplementationRegistry, "remoteFunctionRegistry", spy);
        return spy;
    }

    private FunctionImplementationRegistry spyFunctionImplementationRegistry() {
        DrillbitContext drillbitContext = getDrillbitContext();
        FunctionImplementationRegistry spy = spy(drillbitContext.getFunctionImplementationRegistry());
        Deencapsulation.setField(drillbitContext, "functionRegistry", spy);
        return spy;
    }

    private class SimpleQueryRunner implements Runnable {

        private final String query;

        SimpleQueryRunner(String query) {
            this.query = query;
        }

        @Override
        public void run() {
            try {
                test(query);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    private class TestBuilderRunner implements Runnable {

        private final TestBuilder testBuilder;

        TestBuilderRunner(TestBuilder testBuilder) {
            this.testBuilder = testBuilder;
        }

        @Override
        public void run() {
            try {
                testBuilder.go();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

}