Skip to content

Commit

Permalink
DRILL-7945: Stop patching newly added Guava methods (#2247)
Browse files Browse the repository at this point in the history
In order to support Apache Iceberg, GuavaPatcher adds several methods to
com.google.common.base.Preconditions required by Apache Iceberg runtime
but not available in Guava 18.

When Guava version was updated to 30.1.1, Guava Patcher tries to update
existing methods with some code referencing package protected methods
which do no exist anymore. Patching fails but as the methods do now
exists, there's no compatibility issue. That said, GuavaPatcher emits
some logging which might confuse end user.

Remove the unnecessary patching of the Preconditions class and also add
some unit test designed to fail if patching fails for any reason.
  • Loading branch information
laurentgo committed Jun 4, 2021
1 parent 8c8f86d commit fe85f83
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 106 deletions.
118 changes: 12 additions & 106 deletions common/src/main/java/org/apache/drill/common/util/GuavaPatcher.java
Expand Up @@ -18,11 +18,10 @@
package org.apache.drill.common.util;

import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.apache.drill.shaded.guava.com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javassist.ClassPool;
import javassist.CtClass;
Expand All @@ -31,8 +30,6 @@
import javassist.CtNewMethod;
import javassist.scopedpool.ScopedClassPoolRepository;
import javassist.scopedpool.ScopedClassPoolRepositoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GuavaPatcher {
private static final Logger logger = LoggerFactory.getLogger(GuavaPatcher.class);
Expand All @@ -44,10 +41,16 @@ public static synchronized void patch() {
patchingAttempted = true;
patchStopwatch();
patchCloseables();
patchPreconditions();
}
}

private static boolean patchingSuccessful = true;

@VisibleForTesting
static boolean isPatchingSuccessful() {
return patchingAttempted && patchingSuccessful;
}

/**
* Makes Guava stopwatch look like the old version for compatibility with hbase-server (for test purposes).
*/
Expand Down Expand Up @@ -78,6 +81,7 @@ private static void patchStopwatch() {
}

private static void logUnableToPatchException(Exception e) {
patchingSuccessful = false;
logger.warn("Unable to patch Guava classes: {}", e.getMessage());
logger.debug("Exception:", e);
}
Expand All @@ -102,104 +106,6 @@ private static void patchCloseables() {
}
}

/**
* Patches Guava Preconditions with missing methods, added for the Apache Iceberg.
*/
private static void patchPreconditions() {
try {
ClassPool cp = getClassPool();
CtClass cc = cp.get("com.google.common.base.Preconditions");

// Javassist does not support varargs, generate methods with varying number of arguments
int startIndex = 1;
int endIndex = 5;

List<String> methodsWithVarargsTemplates = Arrays.asList(
"public static void checkArgument(boolean expression, String errorMessageTemplate, %s) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalArgumentException(format(errorMessageTemplate, new Object[] { %s }));\n"
+ " }\n"
+ " }",

"public static Object checkNotNull(Object reference, String errorMessageTemplate, %s) {\n"
+ " if (reference == null) {\n"
+ " throw new NullPointerException(format(errorMessageTemplate, new Object[] { %s }));\n"
+ " } else {\n"
+ " return reference;\n"
+ " }\n"
+ " }",

"public static void checkState(boolean expression, String errorMessageTemplate, %s) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalStateException(format(errorMessageTemplate, new Object[] { %s }));\n"
+ " }\n"
+ " }"
);

List<String> methodsWithPrimitives = Arrays.asList(
"public static void checkArgument(boolean expression, String errorMessageTemplate, int arg1) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalArgumentException(format(errorMessageTemplate, new Object[] { new Integer(arg1) }));\n"
+ " }\n"
+ " }",
"public static void checkArgument(boolean expression, String errorMessageTemplate, long arg1) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalArgumentException(format(errorMessageTemplate, new Object[] { new Long(arg1) }));\n"
+ " }\n"
+ " }",
"public static void checkArgument(boolean expression, String errorMessageTemplate, long arg1, long arg2) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalArgumentException(format(errorMessageTemplate, new Object[] { new Long(arg1), new Long(arg2)}));\n"
+ " }\n"
+ " }",
"public static Object checkNotNull(Object reference, String errorMessageTemplate, int arg1) {\n"
+ " if (reference == null) {\n"
+ " throw new NullPointerException(format(errorMessageTemplate, new Object[] { new Integer(arg1) }));\n"
+ " } else {\n"
+ " return reference;\n"
+ " }\n"
+ " }",
"public static void checkState(boolean expression, String errorMessageTemplate, int arg1) {\n"
+ " if (!expression) {\n"
+ " throw new IllegalStateException(format(errorMessageTemplate, new Object[] { new Integer(arg1) }));\n"
+ " }\n"
+ " }"
);

List<String> newMethods = IntStream.rangeClosed(startIndex, endIndex)
.mapToObj(
i -> {
List<String> args = IntStream.rangeClosed(startIndex, i)
.mapToObj(j -> "arg" + j)
.collect(Collectors.toList());

String methodInput = args.stream()
.map(arg -> "Object " + arg)
.collect(Collectors.joining(", "));

String arrayInput = String.join(", ", args);

return methodsWithVarargsTemplates.stream()
.map(method -> String.format(method, methodInput, arrayInput))
.collect(Collectors.toList());
})
.flatMap(Collection::stream)
.collect(Collectors.toList());

newMethods.addAll(methodsWithPrimitives);

for (String method : newMethods) {
CtMethod newMethod = CtNewMethod.make(method, cc);
cc.addMethod(newMethod);
}

cc.toClass();
logger.info("Google's Preconditions were patched to hold new methods.");
} catch (Exception e) {
logUnableToPatchException(e);
}
}

/**
* Returns {@link javassist.scopedpool.ScopedClassPool} instance which uses the same class loader
* which was used for loading current class.
Expand Down
@@ -0,0 +1,243 @@
/*
* 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.util;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.drill.test.BaseTest;
import org.junit.Test;

// Easier to test Guava patching if we can reference Guava classes directly for the tests
// CHECKSTYLE:OFF
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
import com.google.common.io.Closeables;
//CHECKSTYLE:ON

/**
* Test class for {@code GuavaPatcher}
*
*/
public class TestGuavaPatcher extends BaseTest {

@Test
public void checkSuccessfulPatching() {
// Catch-all test to see if Guava patching was successful
assertTrue("Guava Patcher ran with errors, check error log messages",
GuavaPatcher.isPatchingSuccessful());
}

@Test
public void checkStopwatchEllapsedMillis() throws Exception {
long[] currentTimeMillis = new long[] { 0L };
final Ticker ticker = new Ticker() {
@Override
public long read() {
return TimeUnit.MILLISECONDS.toNanos(currentTimeMillis[0]);
}
};
final Stopwatch stopwatch = Stopwatch.createStarted(ticker);
currentTimeMillis[0] = 12345L;
stopwatch.stop();

assertEquals(currentTimeMillis[0],
stopwatch.elapsed(TimeUnit.MILLISECONDS));
assertEquals(currentTimeMillis[0], (long) invokeMethod(Stopwatch.class,
"elapsedMillis", new Class[] {}, stopwatch));
}

@Test
public void checkCloseablesCloseQuietly() throws Exception {
final Closeable alwaysThrows = () -> {
throw new IOException("Always fail");
};

invokeMethod(Closeables.class, "closeQuietly",
new Class[] { Closeable.class }, null, alwaysThrows);
}

// All the preconditions checks are method which were previously added for
// compatibility with Apache Iceberg but are not necessary anymore because
// Guava's version has been updated since.

@Test
public void checkPreconditionsCheckArgumentIntParam() throws Exception {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, int.class }, null, true,
"Error Message %s", 1);
try {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, int.class }, null, false,
"Error Message %s", 1);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(IllegalArgumentException.class, cause.getClass());
assertEquals("Error Message 1", cause.getMessage());
}
}

@Test
public void checkPreconditionsCheckArgumentLongParam() throws Exception {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, long.class }, null, true,
"Error Message %s", 2L);
try {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, long.class }, null, false,
"Error Message %s", 2L);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(IllegalArgumentException.class, cause.getClass());
assertEquals("Error Message 2", cause.getMessage());
}
}

@Test
public void checkPreconditionsCheckArgumentLongLongParam() throws Exception {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, long.class, long.class },
null, true, "Error Message %s %s", 3L, 4L);
try {
invokeMethod(Preconditions.class, "checkArgument",
new Class[] { boolean.class, String.class, long.class, long.class },
null, false, "Error Message %s %s", 3L, 4L);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(IllegalArgumentException.class, cause.getClass());
assertEquals("Error Message 3 4", cause.getMessage());
}
}

@Test
public void checkPreconditionsCheckNotNullIntParam() throws Exception {
invokeMethod(Preconditions.class, "checkNotNull",
new Class[] { Object.class, String.class, int.class }, null, this,
"Error Message %s", 5);
try {
invokeMethod(Preconditions.class, "checkNotNull",
new Class[] { Object.class, String.class, int.class }, null, null,
"Error Message %s", 5);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(NullPointerException.class, cause.getClass());
assertEquals("Error Message 5", cause.getMessage());
}
}

@Test
public void checkPreconditionsCheckStateIntParam() throws Exception {
invokeMethod(Preconditions.class, "checkState",
new Class[] { boolean.class, String.class, int.class }, null, true,
"Error Message %s", 6);
try {
invokeMethod(Preconditions.class, "checkState",
new Class[] { boolean.class, String.class, int.class }, null, false,
"Error Message %s", 6);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(IllegalStateException.class, cause.getClass());
assertEquals("Error Message 6", cause.getMessage());
}
}

@Test
public void checkPreconditionsCheckNotNullVarargs() throws Exception {
checkPreconditionsCheckVarargMethod("checkNotNull", Object.class, this,
null, NullPointerException.class);
}

@Test
public void checkPreconditionsCheckArgumentVarargs() throws Exception {
checkPreconditionsCheckVarargMethod("checkArgument", boolean.class, true,
false, IllegalArgumentException.class);
}

@Test
public void checkPreconditionsCheckStateVarargs() throws Exception {
checkPreconditionsCheckVarargMethod("checkState", boolean.class, true,
false, IllegalStateException.class);
}

private <T> void checkPreconditionsCheckVarargMethod(String methodName,
Class<T> argClass, T goodValue, T badValue,
Class<? extends Throwable> exceptionClass) throws Exception {
for (int i = 1; i < 5; i++) {
String[] templatePlaceholders = new String[i];
Arrays.fill(templatePlaceholders, "%s");

Object[] templateArguments = new Object[i];
Arrays.setAll(templateArguments, Integer::valueOf);

String template = "Error message: "
+ Stream.of(templatePlaceholders).collect(Collectors.joining(","));
String message = "Error message: " + Stream.of(templateArguments)
.map(Object::toString).collect(Collectors.joining(","));

Class<?>[] parameterTypes = new Class[2 + i];
parameterTypes[0] = argClass;
parameterTypes[1] = String.class;
Arrays.fill(parameterTypes, 2, parameterTypes.length, Object.class);

Object[] parameters = new Object[2 + i];
parameters[0] = goodValue;
parameters[1] = template;
System.arraycopy(templateArguments, 0, parameters, 2, i);

// Check successful call
invokeMethod(Preconditions.class, methodName, parameterTypes, null,
parameters);

try {
parameters[0] = badValue;
invokeMethod(Preconditions.class, methodName, parameterTypes, null,
parameters);
fail();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
assertEquals(exceptionClass, cause.getClass());
assertEquals(message, cause.getMessage());
}
}
}

@SuppressWarnings("unchecked")
private static <T> T invokeMethod(Class<?> clazz, String methodName,
Class<?>[] argumentTypes, Object object, Object... arguments)
throws Exception {
Method method = clazz.getMethod(methodName, argumentTypes);
return (T) method.invoke(object, arguments);
}

}

0 comments on commit fe85f83

Please sign in to comment.