Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id C2AE8200B7C for ; Thu, 8 Sep 2016 17:23:41 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id C149C160ABD; Thu, 8 Sep 2016 15:23:41 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id E2061160AAD for ; Thu, 8 Sep 2016 17:23:40 +0200 (CEST) Received: (qmail 16111 invoked by uid 500); 8 Sep 2016 15:23:40 -0000 Mailing-List: contact dev-help@drill.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@drill.apache.org Delivered-To: mailing list dev@drill.apache.org Received: (qmail 16096 invoked by uid 99); 8 Sep 2016 15:23:39 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 08 Sep 2016 15:23:39 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id A3724E0230; Thu, 8 Sep 2016 15:23:39 +0000 (UTC) From: paul-rogers To: dev@drill.apache.org Reply-To: dev@drill.apache.org References: In-Reply-To: Subject: [GitHub] drill pull request #574: DRILL-4726: Dynamic UDFs support Content-Type: text/plain Message-Id: <20160908152339.A3724E0230@git1-us-west.apache.org> Date: Thu, 8 Sep 2016 15:23:39 +0000 (UTC) archived-at: Thu, 08 Sep 2016 15:23:41 -0000 Github user paul-rogers commented on a diff in the pull request: https://github.com/apache/drill/pull/574#discussion_r78028875 --- Diff: exec/java-exec/src/test/java/org/apache/drill/TestDynamicUDFSupport.java --- @@ -0,0 +1,292 @@ +/** + * 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; + +import mockit.NonStrictExpectations; +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.ExecConstants; +import org.apache.drill.exec.expr.fn.RemoteFunctionRegistry; +import org.apache.drill.exec.proto.UserBitShared.Registry; +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 java.io.File; +import java.io.IOException; +import java.util.Properties; + +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; + +public class TestDynamicUDFSupport extends BaseTestQuery { + + private static final File jars = new File(TestTools.getWorkingPath() + "/src/test/resources/jars"); + private static final String jName = "DrillUDF-1.0.jar"; + private static String sName = JarUtil.getSourceName(jName); + + @Rule + public final TemporaryFolder drillUdfDir = new TemporaryFolder(); + + @Rule + public final TemporaryFolder base = new TemporaryFolder(); + + @Before + public void setEnvVariables() + { + new NonStrictExpectations(System.class) + { + { + invoke(System.class, "getenv", "DRILL_UDF_DIR"); + returns(drillUdfDir.getRoot().getPath()); + } + }; + } + + @Before + public void setup() { + String root = base.getRoot().getPath(); + if (!root.equals(getDrillbitContext().getConfig().getString(ExecConstants.UDF_DIRECTORY_STAGING))) { + Properties overrideProps = new Properties(); + overrideProps.setProperty(ExecConstants.UDF_DIRECTORY_BASE, root); + 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(String.format(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 testAbsentJarInStaging() throws Exception { + String jar = "jar_name.jar"; + String staging = getDrillbitContext().getRemoteFunctionRegistry().getStagingArea().toUri().getPath(); + String summary = String.format("Binary [%s] or source [%s-sources.jar] is absent in udf staging area [%s].", jar, jar.split("\\.")[0], staging); + + testBuilder() + .sqlQuery(String.format("create function using jar '%s'", jar)) + .unOrdered() + .baselineColumns("ok", "summary") + .baselineValues(false, summary) + .go(); + } + + @Test + public void testSuccessfulCreate() throws Exception { + copyJarsToStagingArea(); + + String summary = "The following UDFs in jar %s have been registered:\n" + + "[name: \"custom_lower\"\n" + + "major_type {\n" + + " minor_type: VARCHAR\n" + + " mode: REQUIRED\n" + + "}\n" + + "]"; + + testBuilder() + .sqlQuery(String.format("create function using jar '%s'", jName)) + .unOrdered() + .baselineColumns("ok", "summary") + .baselineValues(true, String.format(summary, jName)) + .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(), jName))); + assertTrue("Source should be present in registry area", fs.exists(new Path(remoteFunctionRegistry.getRegistryArea(), sName))); + + Registry registry = remoteFunctionRegistry.getRegistry(); + assertEquals("Registry should contain one jar", registry.getJarList().size(), 1); + assertEquals(registry.getJar(0).getName(), jName); + test(String.format("drop function using jar '%s'", jName)); + } + + @Test + public void testDuplicatedJar() throws Exception { + copyJarsToStagingArea(); + test(String.format("create function using jar '%s'", jName)); + copyJarsToStagingArea(); + + testBuilder() + .sqlQuery(String.format("create function using jar '%s'", jName)) + .unOrdered() + .baselineColumns("ok", "summary") + .baselineValues(false, String.format("Jar with %s name has been already registered", jName)) + .go(); + + test(String.format("drop function using jar '%s'", jName)); + } + + @Test + public void testDuplicatedFunctions() throws Exception { + copyJarsToStagingArea(); + test(String.format("create function using jar '%s'", jName)); + copyJarsToStagingArea(); + + RemoteFunctionRegistry remoteFunctionRegistry = getDrillbitContext().getRemoteFunctionRegistry(); + FileSystem fs = remoteFunctionRegistry.getFs(); + String newJar = "DrillUDF-2.0.jar"; + String newSource = JarUtil.getSourceName(newJar); + Path renamedBinary = new Path(remoteFunctionRegistry.getStagingArea(), newJar); + Path renamedSource = new Path(remoteFunctionRegistry.getStagingArea(), newSource); + fs.rename(new Path(remoteFunctionRegistry.getStagingArea(), jName), renamedBinary); + fs.rename(new Path(remoteFunctionRegistry.getStagingArea(), sName), renamedSource); + + String summary = "Found duplicated function in %s - name: \"custom_lower\"\n" + + "major_type {\n" + + " minor_type: VARCHAR\n" + + " mode: REQUIRED\n" + + "}\n"; + + testBuilder() + .sqlQuery(String.format("create function using jar '%s'", newJar)) + .unOrdered() + .baselineColumns("ok", "summary") + .baselineValues(false, String.format(summary, jName)) + .go(); + + fs.delete(renamedBinary, false); + fs.delete(renamedSource, false); + test(String.format("drop function using jar '%s'", jName)); + } + + @Test + public void testLazyInit() throws Exception { + try { + test("select custom_lower(version) from sys.version"); + } catch (UserRemoteException e){ + assertThat(e.getMessage(), containsString("No match found for function signature custom_lower()")); + } + + copyJarsToStagingArea(); + + test(String.format("create function using jar '%s'", jName)); + test("select custom_lower(version) from sys.version"); + + assertTrue("Binary should be present in local UDF area", new File(System.getenv("DRILL_UDF_DIR"), jName).exists()); + assertTrue("Source should be present in local UDF area", new File(System.getenv("DRILL_UDF_DIR"), sName).exists()); + test(String.format("drop function using jar '%s'", jName)); + } + + @Test + public void testDropFunction() throws Exception { --- End diff -- Maybe a test of the trick case in which user A registers a UDF and starts a long-running query that uses the UDF. User B deregisters that same function. Does the query for user A continue to run, perhaps using a cached version of the function? Then, user B registers a new UDF of the same name. Does the registration work? Should it? If it does, does it create a new copy of the function so that the old and new versions exist in memory? Now, user B runs a query. It should use the second UDF. But, user A's query should still be running using the first version. This will work if the functions are loaded in separate class loaders, and the generate code is aware of which class loader to use for each function, so that it resolves the correct one. This, in turn, means that function lookup must be dynamic (by name, based on class loader), not implicit (which uses a single class loader tree.) Such a test case might 1) prove the code is rock solid, or 2) suggest ways of enhancing the code, or 3) point out inherient limitations in the design that we must document (if the above use case causes failures and we accept those failures as expected behavior.) --- If your project is set up for it, you can reply to this email and have your reply appear on GitHub as well. If your project does not have this feature enabled and wishes so, or if the feature is enabled but not working, please contact infrastructure at infrastructure@apache.org or file a JIRA ticket with INFRA. ---