From c3e99cdfef91ca57e484d2c5add9e7cd8998191d Mon Sep 17 00:00:00 2001 From: Igor Bernstein Date: Thu, 26 May 2022 11:27:23 -0400 Subject: [PATCH] chore: split emulator into core without deps and a higher level wrapper with grpc helpers Currently the emulator exists in a single artifact with optional deps. The reason for this is that bigtable-hbase needs the emulator w/o grpc. However this is causing issues in graalvm packaging in #1234. This PR makes this easier to manage: a -core artifact without dependencies that just wraps the golang binary that bigtable-hbase can use and a wrapper that has a hard dep on grpc & gax. This is technically a breaking change but the emulator artifact is pre-GA an is marked with BetaApi --- google-cloud-bigtable-bom/pom.xml | 5 + google-cloud-bigtable-emulator-core/pom.xml | 75 ++++++ .../emulator/core/EmulatorController.java | 251 ++++++++++++++++++ google-cloud-bigtable-emulator/pom.xml | 28 +- .../cloud/bigtable/emulator/v2/Emulator.java | 219 ++------------- pom.xml | 20 +- 6 files changed, 373 insertions(+), 225 deletions(-) create mode 100644 google-cloud-bigtable-emulator-core/pom.xml create mode 100644 google-cloud-bigtable-emulator-core/src/main/java/com/google/cloud/bigtable/emulator/core/EmulatorController.java diff --git a/google-cloud-bigtable-bom/pom.xml b/google-cloud-bigtable-bom/pom.xml index e9a8d0aafe..c1b3e40ed9 100644 --- a/google-cloud-bigtable-bom/pom.xml +++ b/google-cloud-bigtable-bom/pom.xml @@ -69,6 +69,11 @@ google-cloud-bigtable-emulator 0.144.1-SNAPSHOT + + com.google.cloud + google-cloud-bigtable-emulator-core + 0.144.1-SNAPSHOT + com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 diff --git a/google-cloud-bigtable-emulator-core/pom.xml b/google-cloud-bigtable-emulator-core/pom.xml new file mode 100644 index 0000000000..0e0e6e1ef6 --- /dev/null +++ b/google-cloud-bigtable-emulator-core/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + + google-cloud-bigtable-parent + com.google.cloud + 2.7.1-SNAPSHOT + + + google-cloud-bigtable-emulator-core + 0.144.1-SNAPSHOT + + + A Java wrapper for the Cloud Bigtable emulator. + + + https://github.com/googleapis/java-bigtable + + scm:git:git@github.com:googleapis/java-bigtable.git + scm:git:git@github.com:googleapis/java-bigtable.git + https://github.com/googleapis/java-bigtable + HEAD + + + + igorberstein + Igor Bernstein + igorbernstein@google.com + Google + + Developer + + + + + + 8 + 8 + + + + + + + com.google.cloud + google-cloud-gcloud-maven-plugin + 0.1.5 + + + + gen-sources + generate-resources + + download + + + + bigtable-darwin-arm + bigtable-darwin-x86_64 + bigtable-linux-arm + bigtable-linux-x86 + bigtable-linux-x86_64 + bigtable-windows-x86 + bigtable-windows-x86_64 + + + + + + + + diff --git a/google-cloud-bigtable-emulator-core/src/main/java/com/google/cloud/bigtable/emulator/core/EmulatorController.java b/google-cloud-bigtable-emulator-core/src/main/java/com/google/cloud/bigtable/emulator/core/EmulatorController.java new file mode 100644 index 0000000000..9ac9245f22 --- /dev/null +++ b/google-cloud-bigtable-emulator-core/src/main/java/com/google/cloud/bigtable/emulator/core/EmulatorController.java @@ -0,0 +1,251 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed 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 + * + * https://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 com.google.cloud.bigtable.emulator.core; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.TimeoutException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Wraps the Bigtable emulator in a java api. + * + *

This class will use the golang binaries embedded in this jar to launch the emulator as an + * external process and redirect its output to a {@link Logger}. + */ +public class EmulatorController { + private static final Logger LOGGER = Logger.getLogger(EmulatorController.class.getName()); + + private final Path executable; + private Process process; + private boolean isStopped = true; + private Thread shutdownHook; + + private int port; + + public static EmulatorController createFromPath(Path path) { + return new EmulatorController(path); + } + /** + * Create a new instance of emulator. The emulator will use the bundled binaries in this jar. + * Please note that the emulator is created in a stopped state, please use {@link #start()} after + * creating it. + */ + public static EmulatorController createBundled() throws IOException { + String resourcePath = getBundledResourcePath(); + + File tmpEmulator = File.createTempFile("cbtemulator", ""); + tmpEmulator.deleteOnExit(); + + try (InputStream is = EmulatorController.class.getResourceAsStream(resourcePath); + FileOutputStream os = new FileOutputStream(tmpEmulator)) { + + if (is == null) { + throw new FileNotFoundException( + "Failed to find the bundled emulator binary: " + resourcePath); + } + + byte[] buff = new byte[2048]; + int length; + + while ((length = is.read(buff)) != -1) { + os.write(buff, 0, length); + } + } + tmpEmulator.setExecutable(true); + + return new EmulatorController(tmpEmulator.toPath()); + } + + private EmulatorController(Path executable) { + this.executable = executable; + } + + public synchronized boolean isRunning() { + return !isStopped; + } + /** Starts the emulator process and waits for it to be ready. */ + public synchronized void start() throws IOException, TimeoutException, InterruptedException { + if (!isStopped) { + throw new IllegalStateException("Emulator is already started"); + } + this.port = getAvailablePort(); + + // Try to align the localhost address across java & golang emulator + // This should fix issues on systems that default to ipv4 but the jvm is started with + // -Djava.net.preferIPv6Addresses=true + Optional localhostAddress = Optional.empty(); + try { + localhostAddress = Optional.of(InetAddress.getByName(null).getHostAddress()); + } catch (UnknownHostException e) { + } + + // Workaround https://bugs.openjdk.java.net/browse/JDK-8068370 + for (int attemptsLeft = 3; process == null; attemptsLeft--) { + try { + String cmd = executable.toString(); + if (localhostAddress.isPresent()) { + cmd += String.format(" -host [%s]", localhostAddress.get()); + } + cmd += String.format(" -port %d", port); + process = Runtime.getRuntime().exec(cmd); + } catch (IOException e) { + if (attemptsLeft > 0) { + Thread.sleep(1000); + continue; + } + throw e; + } + } + pipeStreamToLog(process.getInputStream(), Level.INFO); + pipeStreamToLog(process.getErrorStream(), Level.WARNING); + isStopped = false; + + shutdownHook = + new Thread( + () -> { + if (!isStopped) { + isStopped = true; + process.destroy(); + } + }); + + Runtime.getRuntime().addShutdownHook(shutdownHook); + + waitForPort(port); + } + + /** Stops the emulator process. */ + public synchronized void stop() { + if (isStopped) { + throw new IllegalStateException("Emulator already stopped"); + } + + try { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + shutdownHook = null; + } finally { + isStopped = true; + process.destroy(); + } + } + + public synchronized int getPort() { + if (isStopped) { + throw new IllegalStateException("Emulator is not running"); + } + return port; + } + // + + /** Gets the current platform, which will be used to select the appropriate emulator binary. */ + private static String getBundledResourcePath() { + String unformattedOs = System.getProperty("os.name", "unknown").toLowerCase(Locale.ENGLISH); + String os; + String suffix = ""; + + if (unformattedOs.contains("mac") || unformattedOs.contains("darwin")) { + os = "darwin"; + } else if (unformattedOs.contains("win")) { + os = "windows"; + suffix = ".exe"; + } else if (unformattedOs.contains("linux")) { + os = "linux"; + } else { + throw new UnsupportedOperationException( + "Emulator is not supported on your platform: " + unformattedOs); + } + + String unformattedArch = System.getProperty("os.arch"); + String arch; + + switch (unformattedArch) { + case "x86": + arch = "x86"; + break; + case "x86_64": + case "amd64": + arch = "x86_64"; + break; + case "aarch64": + arch = "arm"; + break; + default: + throw new UnsupportedOperationException("Unsupported architecture: " + unformattedArch); + } + + return String.format( + "/gcloud/bigtable-%s-%s/platform/bigtable-emulator/cbtemulator%s", os, arch, suffix); + } + + /** Gets a random open port number. */ + private static int getAvailablePort() { + try (ServerSocket serverSocket = new ServerSocket(0)) { + return serverSocket.getLocalPort(); + } catch (IOException e) { + throw new RuntimeException("Failed to find open port"); + } + } + + /** Waits for a port to open. It's used to wait for the emulator's gRPC server to be ready. */ + private static void waitForPort(int port) throws InterruptedException, TimeoutException { + for (int i = 0; i < 100; i++) { + try (Socket ignored = new Socket("localhost", port)) { + return; + } catch (IOException e) { + Thread.sleep(200); + } + } + + throw new TimeoutException("Timed out waiting for server to start"); + } + + /** Creates a thread that will pipe an {@link InputStream} to this class' Logger. */ + private static void pipeStreamToLog(final InputStream stream, final Level level) { + final BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + + Thread thread = + new Thread( + () -> { + try { + String line; + while ((line = reader.readLine()) != null) { + LOGGER.log(level, line); + } + } catch (IOException e) { + if (!"Stream closed".equals(e.getMessage())) { + LOGGER.log(Level.WARNING, "Failed to read process stream", e); + } + } + }); + thread.setDaemon(true); + thread.start(); + } + // +} diff --git a/google-cloud-bigtable-emulator/pom.xml b/google-cloud-bigtable-emulator/pom.xml index 0da780bd8a..bccb3aeaf0 100644 --- a/google-cloud-bigtable-emulator/pom.xml +++ b/google-cloud-bigtable-emulator/pom.xml @@ -69,8 +69,8 @@ org.apache.maven.plugins maven-dependency-plugin - - io.grpc:grpc-netty-shaded + + com.google.api:gax-grpc @@ -96,16 +96,28 @@ - + + com.google.cloud + google-cloud-bigtable-emulator-core + 0.144.1-SNAPSHOT + + com.google.api api-common + + com.google.guava + guava + io.grpc grpc-api - - provided + + + + com.google.api + gax-grpc @@ -160,11 +172,5 @@ - - - io.grpc - grpc-netty-shaded - test - diff --git a/google-cloud-bigtable-emulator/src/main/java/com/google/cloud/bigtable/emulator/v2/Emulator.java b/google-cloud-bigtable-emulator/src/main/java/com/google/cloud/bigtable/emulator/v2/Emulator.java index b43322831c..b30fad7ebb 100644 --- a/google-cloud-bigtable-emulator/src/main/java/com/google/cloud/bigtable/emulator/v2/Emulator.java +++ b/google-cloud-bigtable-emulator/src/main/java/com/google/cloud/bigtable/emulator/v2/Emulator.java @@ -16,29 +16,18 @@ package com.google.cloud.bigtable.emulator.v2; import com.google.api.core.BetaApi; +import com.google.cloud.bigtable.emulator.core.EmulatorController; +import com.google.common.base.Preconditions; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.UnknownHostException; import java.nio.file.Path; -import java.util.Locale; -import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.logging.Level; import java.util.logging.Logger; /** - * Wraps the Bigtable emulator in a java api. + * Wraps the Bigtable emulator in a java api and decorates it with grpc channel builders. * *

This class will use the golang binaries embedded in this jar to launch the emulator as an * external process and redirect its output to a {@link Logger}. @@ -47,17 +36,12 @@ public class Emulator { private static final Logger LOGGER = Logger.getLogger(Emulator.class.getName()); - private final Path executable; - private Process process; - private boolean isStopped = true; - private Thread shutdownHook; - - private int port; + private final EmulatorController controller; private ManagedChannel dataChannel; private ManagedChannel adminChannel; public static Emulator createFromPath(Path path) { - return new Emulator(path); + return new Emulator(EmulatorController.createFromPath(path)); } /** * Create a new instance of emulator. The emulator will use the bundled binaries in this jar. @@ -65,98 +49,23 @@ public static Emulator createFromPath(Path path) { * creating it. */ public static Emulator createBundled() throws IOException { - String resourcePath = getBundledResourcePath(); - - File tmpEmulator = File.createTempFile("cbtemulator", ""); - tmpEmulator.deleteOnExit(); - - try (InputStream is = Emulator.class.getResourceAsStream(resourcePath); - FileOutputStream os = new FileOutputStream(tmpEmulator)) { - - if (is == null) { - throw new FileNotFoundException( - "Failed to find the bundled emulator binary: " + resourcePath); - } - - byte[] buff = new byte[2048]; - int length; - - while ((length = is.read(buff)) != -1) { - os.write(buff, 0, length); - } - } - tmpEmulator.setExecutable(true); - - return new Emulator(tmpEmulator.toPath()); + return new Emulator(EmulatorController.createBundled()); } - private Emulator(Path executable) { - this.executable = executable; + private Emulator(EmulatorController controller) { + this.controller = controller; } /** Starts the emulator process and waits for it to be ready. */ public synchronized void start() throws IOException, TimeoutException, InterruptedException { - if (!isStopped) { - throw new IllegalStateException("Emulator is already started"); - } - this.port = getAvailablePort(); - - // Try to align the localhost address across java & golang emulator - // This should fix issues on systems that default to ipv4 but the jvm is started with - // -Djava.net.preferIPv6Addresses=true - Optional localhostAddress = Optional.empty(); - try { - localhostAddress = Optional.of(InetAddress.getByName(null).getHostAddress()); - } catch (UnknownHostException e) { - } - - // Workaround https://bugs.openjdk.java.net/browse/JDK-8068370 - for (int attemptsLeft = 3; process == null; attemptsLeft--) { - try { - String cmd = executable.toString(); - if (localhostAddress.isPresent()) { - cmd += String.format(" -host [%s]", localhostAddress.get()); - } - cmd += String.format(" -port %d", port); - process = Runtime.getRuntime().exec(cmd); - } catch (IOException e) { - if (attemptsLeft > 0) { - Thread.sleep(1000); - continue; - } - throw e; - } - } - pipeStreamToLog(process.getInputStream(), Level.INFO); - pipeStreamToLog(process.getErrorStream(), Level.WARNING); - isStopped = false; - - shutdownHook = - new Thread() { - @Override - public void run() { - if (!isStopped) { - isStopped = true; - process.destroy(); - } - } - }; - - Runtime.getRuntime().addShutdownHook(shutdownHook); - - waitForPort(port); + controller.start(); } /** Stops the emulator process. */ public synchronized void stop() { - if (isStopped) { - throw new IllegalStateException("Emulator already stopped"); - } + controller.stop(); try { - Runtime.getRuntime().removeShutdownHook(shutdownHook); - shutdownHook = null; - // Shutdown channels in parallel if (dataChannel != null) { dataChannel.shutdownNow(); @@ -177,27 +86,19 @@ public synchronized void stop() { } catch (InterruptedException e) { LOGGER.warning("Interrupted while waiting for client channels to close"); Thread.currentThread().interrupt(); - } finally { - isStopped = true; - process.destroy(); } } public synchronized int getPort() { - if (isStopped) { - throw new IllegalStateException("Emulator is not running"); - } - return port; + return controller.getPort(); } public synchronized ManagedChannel getDataChannel() { - if (isStopped) { - throw new IllegalStateException("Emulator is not running"); - } + Preconditions.checkState(controller.isRunning(), "Emulator is not running"); if (dataChannel == null) { dataChannel = - newChannelBuilder(port) + newChannelBuilder(controller.getPort()) .maxInboundMessageSize(256 * 1024 * 1024) .keepAliveTimeout(10, TimeUnit.SECONDS) .keepAliveTime(10, TimeUnit.SECONDS) @@ -208,110 +109,20 @@ public synchronized ManagedChannel getDataChannel() { } public synchronized ManagedChannel getAdminChannel() { - if (isStopped) { - throw new IllegalStateException("Emulator is not running"); - } + Preconditions.checkState(controller.isRunning(), "Emulator is not running"); if (adminChannel == null) { - adminChannel = newChannelBuilder(port).build(); + adminChannel = newChannelBuilder(controller.getPort()).build(); } return adminChannel; } - // - /** Gets the current platform, which will be used to select the appropriate emulator binary. */ - private static String getBundledResourcePath() { - String unformattedOs = System.getProperty("os.name", "unknown").toLowerCase(Locale.ENGLISH); - String os; - String suffix = ""; - - if (unformattedOs.contains("mac") || unformattedOs.contains("darwin")) { - os = "darwin"; - } else if (unformattedOs.contains("win")) { - os = "windows"; - suffix = ".exe"; - } else if (unformattedOs.contains("linux")) { - os = "linux"; - } else { - throw new UnsupportedOperationException( - "Emulator is not supported on your platform: " + unformattedOs); - } - - String unformattedArch = System.getProperty("os.arch"); - String arch; - - switch (unformattedArch) { - case "x86": - arch = "x86"; - break; - case "x86_64": - case "amd64": - arch = "x86_64"; - break; - case "aarch64": - arch = "arm"; - break; - default: - throw new UnsupportedOperationException("Unsupported architecture: " + unformattedArch); - } - - return String.format( - "/gcloud/bigtable-%s-%s/platform/bigtable-emulator/cbtemulator%s", os, arch, suffix); - } - - /** Gets a random open port number. */ - private static int getAvailablePort() { - try (ServerSocket serverSocket = new ServerSocket(0)) { - return serverSocket.getLocalPort(); - } catch (IOException e) { - throw new RuntimeException("Failed to find open port"); - } - } - - /** Waits for a port to open. It's used to wait for the emulator's gRPC server to be ready. */ - private static void waitForPort(int port) throws InterruptedException, TimeoutException { - for (int i = 0; i < 100; i++) { - try (Socket ignored = new Socket("localhost", port)) { - return; - } catch (IOException e) { - Thread.sleep(200); - } - } - - throw new TimeoutException("Timed out waiting for server to start"); - } - /** Creates a {@link io.grpc.ManagedChannelBuilder} preconfigured for the emulator's port. */ private static ManagedChannelBuilder newChannelBuilder(int port) { // NOTE: usePlaintext is currently @ExperimentalAPI. // See https://github.com/grpc/grpc-java/issues/1772 for discussion return ManagedChannelBuilder.forAddress("localhost", port).usePlaintext(); } - - /** Creates a thread that will pipe an {@link java.io.InputStream} to this class' Logger. */ - private static void pipeStreamToLog(final InputStream stream, final Level level) { - final BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); - - Thread thread = - new Thread( - new Runnable() { - @Override - public void run() { - try { - String line; - while ((line = reader.readLine()) != null) { - LOGGER.log(level, line); - } - } catch (IOException e) { - if (!"Stream closed".equals(e.getMessage())) { - LOGGER.log(Level.WARNING, "Failed to read process stream", e); - } - } - } - }); - thread.setDaemon(true); - thread.start(); - } // } diff --git a/pom.xml b/pom.xml index a8f4cedfc9..d1a7d2166f 100644 --- a/pom.xml +++ b/pom.xml @@ -350,14 +350,14 @@ - google-cloud-bigtable - grpc-google-cloud-bigtable-admin-v2 - grpc-google-cloud-bigtable-v2 - proto-google-cloud-bigtable-admin-v2 - proto-google-cloud-bigtable-v2 - google-cloud-bigtable-emulator - google-cloud-bigtable-bom - google-cloud-bigtable-deps-bom - - + google-cloud-bigtable + grpc-google-cloud-bigtable-admin-v2 + grpc-google-cloud-bigtable-v2 + proto-google-cloud-bigtable-admin-v2 + proto-google-cloud-bigtable-v2 + google-cloud-bigtable-emulator-core + google-cloud-bigtable-emulator + google-cloud-bigtable-bom + google-cloud-bigtable-deps-bom +