GoogleCloudPlatform / cloud-spanner-emulator

An open source emulator for Cloud Spanner.
Apache License 2.0
273 stars 45 forks source link

Cleanly shutdown `gcloud emulators spanner start` on SIGTERM/SIGINT #62

Open nielm opened 2 years ago

nielm commented 2 years ago

If emulator is run interactively via gcloud, sending CTRL-C in the terminal shuts down the emulator and subprocesses cleanly.

gcloud emulators spanner start
...
^C
Command killed by keyboard interrupt
$ ps -x | grep spanner_emulator\\/

However, killing the gcloud process with -INT -HUP or -TERM does not do this clean shutdown, leaving emulator_main and gateway_main processes still running, which makes it difficult to programmatically start up and shut down the emulator for testing.

$ gcloud emulators spanner start &
[1] 2314084
...
$ kill 2314084
[1]+  Terminated              gcloud emulators spanner start
...
$ ps -x | grep spanner_emulator\\/
2314115 pts/2    Sl     0:00 /usr/lib/google-cloud-sdk/bin/cloud_spanner_emulator/gateway_main --hostname localhost --grpc_port 9010 --http_port 9020
2314121 pts/2    Sl     0:00 /usr/lib/google-cloud-sdk/bin/cloud_spanner_emulator/emulator_main --host_port localhost:9010

These processes must be found and killed manually.

nielm commented 2 years ago

FWIW, here is some code I used to cleanly setup and teardown the emulator for use in unit testing:

@RunWith(JUnit4.class)
public class SpannerIntegrationTest {

  private static Process emulatorProcess = null;
  private static DatabaseClient dbClient = null;
  private static Spanner spanner = null;

  // Use custom ports to avoid collision with other emulators.
  private static final int EMULATOR_PORT = 29010;
  private static final String EMULATOR_HOST = "localhost:" + EMULATOR_PORT;
  private static final int EMULATOR_REST_PORT = 29020;

  @BeforeClass
  public static void startEmulator() throws IOException, InterruptedException, ExecutionException {
    assertThat(emulatorProcess).isNull();
    emulatorProcess =
        new ProcessBuilder()
            .inheritIO()
            .command(
                "gcloud",
                "emulators",
                "spanner",
                "start",
                "--host-port=" + EMULATOR_HOST,
                "--rest-port=" + EMULATOR_REST_PORT)
            .start();
    // check for startup failure
    if (emulatorProcess.waitFor(5, TimeUnit.SECONDS)) {
      assertWithMessage("Emulator failed to start").fail();
      emulatorProcess = null;
    }
    System.err.println("Spanner Emulator started");

    spanner =
        SpannerOptions.newBuilder()
            .setEmulatorHost(EMULATOR_HOST)
            .setProjectId("dummy-project-id")
            .build()
            .getService();
    InstanceConfig config =
        spanner.getInstanceAdminClient().listInstanceConfigs().iterateAll().iterator().next();
    InstanceId instanceId = InstanceId.of("dummy-project-id", "test");
    System.err.println("Creating instance");
    Instance instance =
        spanner
            .getInstanceAdminClient()
            .createInstance(
                InstanceInfo.newBuilder(instanceId)
                    .setInstanceConfigId(config.getId())
                    .setNodeCount(1)
                    .build())
            .get();
    System.err.println("Creating database");
    Database db =
        spanner
            .getDatabaseAdminClient()
            .createDatabase(
                "test",
                "test",
                ImmutableList.of(
                    "CREATE TABLE test1 (key INT64, value STRING(MAX)) PRIMARY KEY(key)",
                    "CREATE TABLE test2 (key INT64, value STRING(MAX)) PRIMARY KEY(key)"))
            .get();

    dbClient = spanner.getDatabaseClient(db.getId());
    System.err.println("Emulator ready");
  }

  @AfterClass
  public static void endEmulator() throws InterruptedException, IOException {
    spanner.close();
    spanner = null;
    dbClient = null;
    if (emulatorProcess != null && emulatorProcess.isAlive()) {
      System.err.println("Stopping Spanner Emulator");
      emulatorProcess.destroy();
      if (!emulatorProcess.waitFor(5, TimeUnit.SECONDS)) {
        emulatorProcess.destroyForcibly();
      }
      if (!emulatorProcess.waitFor(5, TimeUnit.SECONDS)) {
        assertWithMessage("Emulator could not be killed").fail();
      }
    }
    emulatorProcess = null;

    // Cleanup any leftover emulator processes
    System.err.println("Stopping Spanner Emulator subprocesses");
    new ProcessBuilder()
        .inheritIO()
        .command(
            "bash",
            "-c",
            "kill $(ps -xo pid,command | grep 'spanner_emulator.*"
                + EMULATOR_PORT
                + "' | cut -f1 \"-d \" )")
        .start()
        .waitFor();
    System.err.println("Emulator stopped");
  }
snehashah16 commented 2 years ago

Hey nielm@, could u please contribute this sample to the https://github.com/cloudspannerecosystem/emulator-samples repo ?

nielm commented 2 years ago

Hey nielm@, could u please contribute this sample to the https://github.com/cloudspannerecosystem/emulator-samples repo ?

I cleaned up the code a bit, and made it more generic so that it could be used as a JUnit @ClassRule in unit tests. https://github.com/cloudspannerecosystem/emulator-samples/pull/6