Skip to content

Understanding the Problem

Elliotte Rusty Harold edited this page Jan 16, 2019 · 8 revisions

tldr; some of the libraries GCP publishes cannot be used together in the same project.

At worst, library A requires version 1 of library X and will not work with version 2 of library X. At the same time library B requires version 2 of library X and will not work with version 1. Thus no other project can use both A and B.

A slightly more common variant is that library A requires version 1 of library X but will work with version 2. Library B requires version 2 of library X and will not work with version 1. However the build path is configured such that version 1 is selected instead of version 2.

Is this a real problem? Yes. Most of the time A and B work perfectly well together, but not always; and when a problem like this does arise it's typically quite hard to diagnose and fix.

Here is one of the simplest examples we've found.

Start with this pom.xml. It has exactly two dependencies, com.google.api-client:google-api-client:1.27.0 and io.grpc:grpc-core:1.17.1. Both are official, supported GCP products. At the time of this writing these are the latest, most up-to-date versions of these artifacts.

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.google.cloud.tools.opensource</groupId>
  <artifactId>no-such-method-error-example</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>no-such-method-error-example</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.google.api-client</groupId>
      <artifactId>google-api-client</artifactId>
      <version>1.27.0</version>
    </dependency>
    <dependency>
      <groupId>io.grpc</groupId>
      <artifactId>grpc-core</artifactId>
      <version>1.17.1</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.6.0</version>
        <configuration>
          <mainClass>io.grpc.internal.App</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

We also need a single .java source file:

package io.grpc.internal;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import java.util.Random;

public class App {

  public static void main(String[] args) {
    Map<String, Object> choice = ImmutableMap.of("clientLanguage", ImmutableList.of("en"));
    DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost");
  }
}

(You can find this project ready to run in the source repo.)

The project compiles without error. Now let's run it:

$ mvn exec:java
[INFO] Scanning for projects...
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] Building no-such-method-error-example 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ no-such-method-error-example ---
[WARNING] 
java.lang.NoSuchMethodError: com.google.common.base.Verify.verify(ZLjava/lang/String;Ljava/lang/Object;)V
	at io.grpc.internal.DnsNameResolver.maybeChooseServiceConfig(DnsNameResolver.java:514)
	at io.grpc.internal.App.main(App.java:31)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:282)
	at java.lang.Thread.run(Thread.java:748)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.800 s
[INFO] Finished at: 2019-01-16T14:34:56-05:00
[INFO] Final Memory: 9M/155M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.6.0:java (default-cli) on project no-such-method-error-example: An exception occured while executing the Java class. com.google.common.base.Verify.verify(ZLjava/lang/String;Ljava/lang/Object;)V -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

What happened? In brief, io.grpc:grpc-core:1.17.1 needs a particular overloaded verify method that is present in Guava 26.0. However, com.google.api-client:google-api-client:1.27.0 instead pulls in Guava 20.0 which does not have this method. This is not discovered until runtime when something follows a code path that tries to call the missing method and fails.

Is this example too simple?

Indeed this is deliberately as small an example as we could contrive to make the problem clear. There are two features that make it look perhaps too much of a contrived problem:

  1. We put it in the io.grpc.internal package instead of a user package. This is because DnsNameResolver.maybeChooseServiceConfig is package protected. However that code is not dead. It is indirectly reachable from user code. However, it's easier to see and understand what's happening if we invoke the method directly.

  2. This program does not actually use com.google.api-client:google-api-client:1.27.0. You could simply eliminate this dependency and the problem goes away. However we can easily imagine a program that uses both com.google.api-client:google-api-client:1.27.0 and io.grpc:grpc-core:1.17.1. It would simply be slightly larger than the one we have here.

How can we fix this?