All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.datastore.v1.client.DatastoreEmulator Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * Copyright 2015 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
 *
 *     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 com.google.datastore.v1.client;

import static com.google.api.client.util.Preconditions.checkNotNull;
import static com.google.api.client.util.Preconditions.checkState;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * An extension to {@link Datastore} that provides lifecycle management for a datastore emulator.
 *
 * 

In order to use the emulator for a JUnit 4 test you might do something like this: * *

 * public class MyTest {
 *
 *   static DatastoreEmulator datastore;
 *
 *   {@literal @}BeforeClass
 *   public static void startEmulator() throws DatastoreEmulatorException {
 *     DatastoreOptions options = new DatastoreOptions.Builder()
 *         .localHost("localhost:8080")
 *         .projectId("my-project-id")
 *         .build();
 *     datastore = DatastoreEmulatorFactory.get().create(options);
 *     datastore.start("/usr/local/cloud-datastore-emulator", "my-project-id");
 *   }
 *
 *   {@literal @}Before
 *   public void setUp() throws DatastoreEmulatorException {
 *     datastore.clear();
 *   }
 *
 *   {@literal @}AfterClass
 *   public static void stopEmulator() throws DatastoreEmulatorException {
 *     datastore.stop();
 *   }
 *
 *   {@literal @}Test
 *   public void testFoo1() { }
 *
 *   {@literal @}Test
 *   public void testFoo2() { }
 *
 * }
 * 
*/ public class DatastoreEmulator extends Datastore { private static final int STARTUP_TIMEOUT_SECS = 30; private final String host; private final DatastoreEmulatorOptions options; /** Internal state lifecycle management. */ enum State { NEW, STARTED, STOPPED } private volatile State state = State.NEW; private File projectDirectory; DatastoreEmulator(RemoteRpc rpc, String localHost, DatastoreEmulatorOptions options) { super(rpc); this.host = "http://" + localHost; this.options = options; } /** * Clears all data in the emulator. * * @throws DatastoreEmulatorException if the clear fails */ public void clear() throws DatastoreEmulatorException { sendEmptyRequest("/reset", "POST"); } /** * Starts the emulator. It is the caller's responsibility to call {@link #stop}. Note that * receiving an exception does not indicate that the server did not start. We recommend calling * {@link #stop} to ensure the server is not running regardless of the result of this method. * * @param emulatorDir The path to the emulator directory, e.g. /usr/local/cloud-datastore-emulator * @param projectId The project ID * @param commandLineOptions Command line options to pass to the emulator on startup * @throws DatastoreEmulatorException If {@link #start} has already been called or the server does * not start successfully. * @deprecated prefer setting options in the emulator options and calling {#start()}. */ @Deprecated public synchronized void start(String emulatorDir, String projectId, String... commandLineOptions) throws DatastoreEmulatorException { checkNotNull(emulatorDir, "emulatorDir cannot be null"); checkNotNull(projectId, "projectId cannot be null"); checkState(state == State.NEW, "Cannot call start() more than once."); try { startEmulatorInternal( emulatorDir + "/cloud_datastore_emulator", projectId, Arrays.asList(commandLineOptions)); state = State.STARTED; } finally { if (state != State.STARTED) { // If we're not able to start the server we don't want people trying again. Just move it // straight to the STOPPED state. state = State.STOPPED; } } } public synchronized void start() throws DatastoreEmulatorException { checkState(state == State.NEW, "Cannot call start() more than once."); try { startEmulatorInternal(options.getCmd(), options.getProjectId(), options.getCmdLineOptions()); state = State.STARTED; } finally { if (state != State.STARTED) { // If we're not able to start the server we don't want people trying again. Just move it // straight to the STOPPED state. state = State.STOPPED; } } } void startEmulatorInternal(String emulatorCmd, String projectId, List commandLineOptions) throws DatastoreEmulatorException { projectDirectory = createProjectDirectory(emulatorCmd, projectId); List cmd = new ArrayList<>(Arrays.asList(emulatorCmd, "start", "--testing")); cmd.addAll(commandLineOptions); cmd.add(projectDirectory.getPath()); final Process emulatorStartProcess; try { emulatorStartProcess = newEmulatorProcess(cmd).start(); } catch (IOException e) { throw new DatastoreEmulatorException("Could not start emulator", e); } // Ensure we don't leak the emulator instance if tests end prematurely. Runtime.getRuntime() .addShutdownHook( new Thread() { @Override public void run() { emulatorStartProcess.destroy(); } }); StartupMonitor monitor = new StartupMonitor(emulatorStartProcess.getInputStream()); try { monitor.start(); if (!monitor.startupCompleteLatch.await(STARTUP_TIMEOUT_SECS, TimeUnit.SECONDS)) { throw new DatastoreEmulatorException("Emulator did not start within 30 seconds"); } if (!monitor.success) { throw new DatastoreEmulatorException("Emulator did not start normally"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new DatastoreEmulatorException("Received an interrupt", e); } } private File createProjectDirectory(String emulatorCmd, String projectId) throws DatastoreEmulatorException { File projectDirectory; try { projectDirectory = Files.createTempDirectory("datastore-emulator").toFile(); } catch (IOException e) { throw new DatastoreEmulatorException("Could not create temporary project directory", e); } List cmd = Arrays.asList( emulatorCmd, "create", "--project_id=" + projectId, projectDirectory.getPath()); try { int retCode = newEmulatorProcess(cmd).start().waitFor(); if (retCode != 0) { throw new DatastoreEmulatorException( String.format("Could not create project (retcode=%d)", retCode)); } } catch (IOException e) { throw new DatastoreEmulatorException("Could not create project", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new DatastoreEmulatorException("Received an interrupt", e); } return projectDirectory; } private ProcessBuilder newEmulatorProcess(List cmd) { ProcessBuilder builder = new ProcessBuilder(cmd); builder.redirectErrorStream(true); builder.environment().putAll(options.getEnvVars()); return builder; } /** * Stops the emulator. Multiple calls are allowed. * * @throws DatastoreEmulatorException if the emulator cannot be stopped */ public synchronized void stop() throws DatastoreEmulatorException { // We intentionally don't check the internal state. If people want to try and stop the server // multiple times that's fine. stopEmulatorInternal(); if (state != State.STOPPED) { state = State.STOPPED; if (projectDirectory != null) { try { Process process = new ProcessBuilder("rm", "-r", projectDirectory.getAbsolutePath()).start(); if (process.waitFor() != 0) { throw new IOException( "Temporary project directory deletion exited with " + process.exitValue()); } } catch (IOException e) { throw new IllegalStateException("Could not delete temporary project directory", e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException("Could not delete temporary project directory", e); } } } } protected void stopEmulatorInternal() throws DatastoreEmulatorException { sendEmptyRequest("/shutdown", "POST"); } public synchronized File getProjectDirectory() { checkState(state == State.STARTED); return projectDirectory; } /** * Monitors the provided input stream for evidence that the emulator has started successfully and * redirects the output of the emulator process to sysout for this process. */ static class StartupMonitor extends Thread { private final InputStream inputStream; private volatile boolean success = false; /** This latch will reach 0 once server startup has completed. */ private final CountDownLatch startupCompleteLatch = new CountDownLatch(1); StartupMonitor(InputStream inputStream) { this.inputStream = inputStream; setDaemon(true); } @Override public void run() { try { BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = br.readLine()) != null) { // Redirect to sysout for our process. System.out.println(line); // TODO(pcostello): Just make a request to "/" and look for an HTTP 200. if (!success && line.contains("Dev App Server is now running")) { success = true; startupCompleteLatch.countDown(); } } } catch (IOException ioe) { if (!success) { System.err.println( "Received an IOException before emulator startup completed. " + "Emulator is in an unknown state."); } else { // We got an exception after the server started successfully. We'll lose the ability // to log the output of the emulator but there's no need to shut anything down. System.err.println( "Received an exception handling output from the emulator. " + "Logging will stop but the emulator is probably ok."); } ioe.printStackTrace(); } finally { if (!success) { // Either the stream is closed (indicates server shut down) or we received an Exception // while processing the stream contents. Either way we can tell the calling thread to stop // waiting. startupCompleteLatch.countDown(); } } } } /** Send an empty request using a standard HTTP connection. */ private void sendEmptyRequest(String path, String method) throws DatastoreEmulatorException { HttpURLConnection connection = null; try { URL url = new URL(this.host + path); connection = (HttpURLConnection) url.openConnection(); connection.setDoOutput(true); connection.setRequestMethod(method); connection.getOutputStream().close(); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { throw new DatastoreEmulatorException( String.format( "%s request to %s returned HTTP status %s", method, path, connection.getResponseCode())); } } catch (IOException e) { throw new DatastoreEmulatorException( String.format("Exception connecting to emulator on %s request to %s", method, path), e); } finally { if (connection != null) { connection.disconnect(); } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy