Skip to content

Commit 6fdc2c1

Browse files
feat: split emulator into core without deps and a higher level wrapper with grpc helpers (#1264)
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 Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-bigtable/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> ☕️ If you write sample code, please follow the [samples format]( https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
1 parent 6304d88 commit 6fdc2c1

File tree

8 files changed

+366
-216
lines changed

8 files changed

+366
-216
lines changed

.repo-metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"codeowner_team": "@googleapis/api-bigtable",
1313
"api_id": "bigtable.googleapis.com",
1414
"library_type": "GAPIC_COMBO",
15-
"extra_versioned_modules": "google-cloud-bigtable-emulator",
15+
"extra_versioned_modules": "google-cloud-bigtable-emulator,google-cloud-bigtable-emulator-core",
1616
"excluded_poms": "google-cloud-bigtable-bom"
1717
}

google-cloud-bigtable-bom/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@
6969
<artifactId>google-cloud-bigtable-emulator</artifactId>
7070
<version>0.144.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
7171
</dependency>
72+
<dependency>
73+
<groupId>com.google.cloud</groupId>
74+
<artifactId>google-cloud-bigtable-emulator-core</artifactId>
75+
<version>0.144.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
76+
</dependency>
7277
<dependency>
7378
<groupId>com.google.api.grpc</groupId>
7479
<artifactId>grpc-google-cloud-bigtable-admin-v2</artifactId>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<artifactId>google-cloud-bigtable-parent</artifactId>
9+
<groupId>com.google.cloud</groupId>
10+
<version>2.7.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable:current} -->
11+
</parent>
12+
13+
<artifactId>google-cloud-bigtable-emulator-core</artifactId>
14+
<version>0.144.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigtable-emulator:current} -->
15+
16+
<description>
17+
A Java wrapper for the Cloud Bigtable emulator.
18+
</description>
19+
20+
<url>https://github.com/googleapis/java-bigtable</url>
21+
<scm>
22+
<connection>scm:git:git@github.com:googleapis/java-bigtable.git</connection>
23+
<developerConnection>scm:git:git@github.com:googleapis/java-bigtable.git</developerConnection>
24+
<url>https://github.com/googleapis/java-bigtable</url>
25+
<tag>HEAD</tag>
26+
</scm>
27+
<developers>
28+
<developer>
29+
<id>igorberstein</id>
30+
<name>Igor Bernstein</name>
31+
<email>igorbernstein@google.com</email>
32+
<organization>Google</organization>
33+
<roles>
34+
<role>Developer</role>
35+
</roles>
36+
</developer>
37+
</developers>
38+
39+
<properties>
40+
<maven.compiler.source>8</maven.compiler.source>
41+
<maven.compiler.target>8</maven.compiler.target>
42+
</properties>
43+
44+
<build>
45+
<plugins>
46+
<plugin>
47+
<!-- https://github.com/googleapis/java-gcloud-maven-plugin -->
48+
<groupId>com.google.cloud</groupId>
49+
<artifactId>google-cloud-gcloud-maven-plugin</artifactId>
50+
<version>0.1.5</version>
51+
52+
<executions>
53+
<execution>
54+
<id>gen-sources</id>
55+
<phase>generate-resources</phase>
56+
<goals>
57+
<goal>download</goal>
58+
</goals>
59+
<configuration>
60+
<componentNames>
61+
<componentName>bigtable-darwin-arm</componentName>
62+
<componentName>bigtable-darwin-x86_64</componentName>
63+
<componentName>bigtable-linux-arm</componentName>
64+
<componentName>bigtable-linux-x86</componentName>
65+
<componentName>bigtable-linux-x86_64</componentName>
66+
<componentName>bigtable-windows-x86</componentName>
67+
<componentName>bigtable-windows-x86_64</componentName>
68+
</componentNames>
69+
</configuration>
70+
</execution>
71+
</executions>
72+
</plugin>
73+
</plugins>
74+
</build>
75+
</project>
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigtable.emulator.core;
17+
18+
import java.io.BufferedReader;
19+
import java.io.File;
20+
import java.io.FileNotFoundException;
21+
import java.io.FileOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.InputStreamReader;
25+
import java.net.InetAddress;
26+
import java.net.ServerSocket;
27+
import java.net.Socket;
28+
import java.net.UnknownHostException;
29+
import java.nio.file.Path;
30+
import java.util.Locale;
31+
import java.util.Optional;
32+
import java.util.concurrent.TimeoutException;
33+
import java.util.logging.Level;
34+
import java.util.logging.Logger;
35+
36+
/**
37+
* Wraps the Bigtable emulator in a java api.
38+
*
39+
* <p>This class will use the golang binaries embedded in this jar to launch the emulator as an
40+
* external process and redirect its output to a {@link Logger}.
41+
*/
42+
public class EmulatorController {
43+
private static final Logger LOGGER = Logger.getLogger(EmulatorController.class.getName());
44+
45+
private final Path executable;
46+
private Process process;
47+
private boolean isStopped = true;
48+
private Thread shutdownHook;
49+
50+
private int port;
51+
52+
public static EmulatorController createFromPath(Path path) {
53+
return new EmulatorController(path);
54+
}
55+
/**
56+
* Create a new instance of emulator. The emulator will use the bundled binaries in this jar.
57+
* Please note that the emulator is created in a stopped state, please use {@link #start()} after
58+
* creating it.
59+
*/
60+
public static EmulatorController createBundled() throws IOException {
61+
String resourcePath = getBundledResourcePath();
62+
63+
File tmpEmulator = File.createTempFile("cbtemulator", "");
64+
tmpEmulator.deleteOnExit();
65+
66+
try (InputStream is = EmulatorController.class.getResourceAsStream(resourcePath);
67+
FileOutputStream os = new FileOutputStream(tmpEmulator)) {
68+
69+
if (is == null) {
70+
throw new FileNotFoundException(
71+
"Failed to find the bundled emulator binary: " + resourcePath);
72+
}
73+
74+
byte[] buff = new byte[2048];
75+
int length;
76+
77+
while ((length = is.read(buff)) != -1) {
78+
os.write(buff, 0, length);
79+
}
80+
}
81+
tmpEmulator.setExecutable(true);
82+
83+
return new EmulatorController(tmpEmulator.toPath());
84+
}
85+
86+
private EmulatorController(Path executable) {
87+
this.executable = executable;
88+
}
89+
90+
public synchronized boolean isRunning() {
91+
return !isStopped;
92+
}
93+
/** Starts the emulator process and waits for it to be ready. */
94+
public synchronized void start() throws IOException, TimeoutException, InterruptedException {
95+
if (!isStopped) {
96+
throw new IllegalStateException("Emulator is already started");
97+
}
98+
this.port = getAvailablePort();
99+
100+
// Try to align the localhost address across java & golang emulator
101+
// This should fix issues on systems that default to ipv4 but the jvm is started with
102+
// -Djava.net.preferIPv6Addresses=true
103+
Optional<String> localhostAddress = Optional.empty();
104+
try {
105+
localhostAddress = Optional.of(InetAddress.getByName(null).getHostAddress());
106+
} catch (UnknownHostException e) {
107+
}
108+
109+
// Workaround https://bugs.openjdk.java.net/browse/JDK-8068370
110+
for (int attemptsLeft = 3; process == null; attemptsLeft--) {
111+
try {
112+
String cmd = executable.toString();
113+
if (localhostAddress.isPresent()) {
114+
cmd += String.format(" -host [%s]", localhostAddress.get());
115+
}
116+
cmd += String.format(" -port %d", port);
117+
process = Runtime.getRuntime().exec(cmd);
118+
} catch (IOException e) {
119+
if (attemptsLeft > 0) {
120+
Thread.sleep(1000);
121+
continue;
122+
}
123+
throw e;
124+
}
125+
}
126+
pipeStreamToLog(process.getInputStream(), Level.INFO);
127+
pipeStreamToLog(process.getErrorStream(), Level.WARNING);
128+
isStopped = false;
129+
130+
shutdownHook =
131+
new Thread(
132+
() -> {
133+
if (!isStopped) {
134+
isStopped = true;
135+
process.destroy();
136+
}
137+
});
138+
139+
Runtime.getRuntime().addShutdownHook(shutdownHook);
140+
141+
waitForPort(port);
142+
}
143+
144+
/** Stops the emulator process. */
145+
public synchronized void stop() {
146+
if (isStopped) {
147+
throw new IllegalStateException("Emulator already stopped");
148+
}
149+
150+
try {
151+
Runtime.getRuntime().removeShutdownHook(shutdownHook);
152+
shutdownHook = null;
153+
} finally {
154+
isStopped = true;
155+
process.destroy();
156+
}
157+
}
158+
159+
public synchronized int getPort() {
160+
if (isStopped) {
161+
throw new IllegalStateException("Emulator is not running");
162+
}
163+
return port;
164+
}
165+
// <editor-fold desc="Helpers">
166+
167+
/** Gets the current platform, which will be used to select the appropriate emulator binary. */
168+
private static String getBundledResourcePath() {
169+
String unformattedOs = System.getProperty("os.name", "unknown").toLowerCase(Locale.ENGLISH);
170+
String os;
171+
String suffix = "";
172+
173+
if (unformattedOs.contains("mac") || unformattedOs.contains("darwin")) {
174+
os = "darwin";
175+
} else if (unformattedOs.contains("win")) {
176+
os = "windows";
177+
suffix = ".exe";
178+
} else if (unformattedOs.contains("linux")) {
179+
os = "linux";
180+
} else {
181+
throw new UnsupportedOperationException(
182+
"Emulator is not supported on your platform: " + unformattedOs);
183+
}
184+
185+
String unformattedArch = System.getProperty("os.arch");
186+
String arch;
187+
188+
switch (unformattedArch) {
189+
case "x86":
190+
arch = "x86";
191+
break;
192+
case "x86_64":
193+
case "amd64":
194+
arch = "x86_64";
195+
break;
196+
case "aarch64":
197+
arch = "arm";
198+
break;
199+
default:
200+
throw new UnsupportedOperationException("Unsupported architecture: " + unformattedArch);
201+
}
202+
203+
return String.format(
204+
"/gcloud/bigtable-%s-%s/platform/bigtable-emulator/cbtemulator%s", os, arch, suffix);
205+
}
206+
207+
/** Gets a random open port number. */
208+
private static int getAvailablePort() {
209+
try (ServerSocket serverSocket = new ServerSocket(0)) {
210+
return serverSocket.getLocalPort();
211+
} catch (IOException e) {
212+
throw new RuntimeException("Failed to find open port");
213+
}
214+
}
215+
216+
/** Waits for a port to open. It's used to wait for the emulator's gRPC server to be ready. */
217+
private static void waitForPort(int port) throws InterruptedException, TimeoutException {
218+
for (int i = 0; i < 100; i++) {
219+
try (Socket ignored = new Socket("localhost", port)) {
220+
return;
221+
} catch (IOException e) {
222+
Thread.sleep(200);
223+
}
224+
}
225+
226+
throw new TimeoutException("Timed out waiting for server to start");
227+
}
228+
229+
/** Creates a thread that will pipe an {@link InputStream} to this class' Logger. */
230+
private static void pipeStreamToLog(final InputStream stream, final Level level) {
231+
final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
232+
233+
Thread thread =
234+
new Thread(
235+
() -> {
236+
try {
237+
String line;
238+
while ((line = reader.readLine()) != null) {
239+
LOGGER.log(level, line);
240+
}
241+
} catch (IOException e) {
242+
if (!"Stream closed".equals(e.getMessage())) {
243+
LOGGER.log(Level.WARNING, "Failed to read process stream", e);
244+
}
245+
}
246+
});
247+
thread.setDaemon(true);
248+
thread.start();
249+
}
250+
// </editor-fold>
251+
}

0 commit comments

Comments
 (0)