FaultInjectorScenarioBase.java

package redis.clients.jedis.scenario;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.redis.test.fi.Database;
import com.redis.test.fi.FaultInjectorClient;
import com.redis.test.fi.Scenario;
import com.redis.test.fi.StandaloneTriggerCatalog;

import org.junit.jupiter.api.AfterEach;

/**
 * Shared environment for scenario tests driven by the fault injector: each test creates its own
 * database from a discovery-provided dbconfig and deletes it on teardown. Holds no client ��� a
 * subclass builds the client it tests and closes it in its own {@code @AfterEach}, which JUnit runs
 * before this class deletes the database.
 */
abstract class FaultInjectorScenarioBase {

  // Short JVM DNS cache: these tests reconnect across endpoint rebinds, where the default 30s
  // positive cache can outlive the DNS repoint, and await fresh database names, where the
  // default 10s negative cache delays convergence. The JVM reads these once, at its first name
  // lookup ��� this block must run before that (it does under the surefire fork; an IDE may
  // resolve earlier, in which case pass -Dsun.net.inetaddr.ttl=2 and
  // -Dsun.net.inetaddr.negative.ttl=2 in the run configuration).
  static {
    java.security.Security.setProperty("networkaddress.cache.ttl", "2");
    java.security.Security.setProperty("networkaddress.cache.negative.ttl", "2");
  }

  /** One FI client for parameter collection and test execution. */
  static final FaultInjectorClient faultInjector = new FaultInjectorClient();

  /**
   * The full topology-change-standalone catalog, resolved once: effect -> triggers -> requirements.
   */
  static final StandaloneTriggerCatalog CATALOG = StandaloneTriggerCatalog.resolve(faultInjector);

  Database database;
  private DnsDiagnostics dnsDiag;

  /**
   * Creates the scenario's test database and waits for its endpoint. The dbconfig comes from a
   * fresh discovery call: the FI generates a unique name and port per call, so every database gets
   * its own endpoint DNS name. Teardown deletes it.
   */
  Database deployDatabase(Scenario scenario) {
    Map<String, Object> dbConfig = faultInjector.getStandaloneTriggers(scenario.effect())
        .trigger(scenario.trigger().name()).requirement(scenario.requirement().config()).dbConfig();
    database = faultInjector.createDatabase(dbConfig);
    awaitEndpointConnectable(database.endpoint());
    dnsDiag = DnsDiagnostics.follow(database.host());
    return database;
  }

  /**
   * A fresh endpoint's DNS record may not have propagated yet (or a negative lookup may be cached)
   * ��� wait until the endpoint accepts connections before building the client.
   */
  static void awaitEndpointConnectable(URI endpoint) {
    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60);
    IOException last = null;
    while (System.nanoTime() < deadline) {
      try (Socket socket = new Socket()) {
        socket.connect(new InetSocketAddress(endpoint.getHost(), endpoint.getPort()), 2_000);
        return;
      } catch (IOException e) {
        last = e;
        sleepQuietly(1_000);
      }
    }
    throw new IllegalStateException("Endpoint " + endpoint + " not connectable within 60 s", last);
  }

  @AfterEach
  final void tearDownDatabase() {
    try {
      if (dnsDiag != null) {
        dnsDiag.close();
        dnsDiag = null;
      }
    } finally {
      database = deleteIfPresent(database);
    }
  }

  /** Deletes the database if one was deployed; returns null so the field is cleared in one step. */
  static Database deleteIfPresent(Database database) {
    if (database != null) {
      faultInjector.deleteDatabase(database.id());
    }
    return null;
  }

  static long elapsedMillis(long startNanos) {
    return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
  }

  static void sleepQuietly(long millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt();
    }
  }
}