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

io.camunda.zeebe.qa.util.cluster.TestSpringApplication Maven / Gradle / Ivy

/*
 * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
 * one or more contributor license agreements. See the NOTICE file distributed
 * with this work for additional information regarding copyright ownership.
 * Licensed under the Zeebe Community License 1.1. You may not use this file
 * except in compliance with the Zeebe Community License 1.1.
 */
package io.camunda.zeebe.qa.util.cluster;

import io.camunda.zeebe.qa.util.cluster.util.ContextOverrideInitializer;
import io.camunda.zeebe.qa.util.cluster.util.ContextOverrideInitializer.Bean;
import io.camunda.zeebe.qa.util.cluster.util.RelaxedCollectorRegistry;
import io.camunda.zeebe.shared.MainSupport;
import io.camunda.zeebe.shared.Profile;
import io.camunda.zeebe.test.util.socket.SocketUtil;
import io.prometheus.client.CollectorRegistry;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.Banner.Mode;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.client.ReactorResourceFactory;

abstract class TestSpringApplication>
    implements TestApplication {
  private final Class springApplication;
  private final Map> beans;
  private final Map propertyOverrides;
  private final Collection additionalProfiles;
  private final ReactorResourceFactory reactorResourceFactory = new ReactorResourceFactory();

  private ConfigurableApplicationContext springContext;

  public TestSpringApplication(final Class springApplication) {
    this(springApplication, new HashMap<>(), new HashMap<>(), new ArrayList<>());
  }

  private TestSpringApplication(
      final Class springApplication,
      final Map> beans,
      final Map propertyOverrides,
      final Collection additionalProfiles) {
    this.springApplication = springApplication;
    this.beans = beans;
    this.propertyOverrides = propertyOverrides;
    this.additionalProfiles = new ArrayList<>(additionalProfiles);
    this.additionalProfiles.add(Profile.TEST.getId());

    // randomize ports to allow multiple concurrent instances
    overridePropertyIfAbsent("server.port", SocketUtil.getNextAddress().getPort());
    overridePropertyIfAbsent("management.server.port", SocketUtil.getNextAddress().getPort());
    overridePropertyIfAbsent("spring.lifecycle.timeout-per-shutdown-phase", "1s");

    if (!beans.containsKey("collectorRegistry")) {
      beans.put(
          "collectorRegistry", new Bean<>(new RelaxedCollectorRegistry(), CollectorRegistry.class));
    }

    // configure each application to use their own resources for the embedded Netty web server,
    // otherwise shutting one down will shut down all embedded servers
    reactorResourceFactory.setUseGlobalResources(false);
    reactorResourceFactory.setShutdownQuietPeriod(Duration.ZERO);
    beans.put(
        "reactorResourceFactory", new Bean<>(reactorResourceFactory, ReactorResourceFactory.class));
  }

  @Override
  public T start() {
    if (!isStarted()) {
      // simulate initialization of singleton bean; since injected singleton are not initialized,
      // but we still want Spring to manage the individual reactor resources. we do this here since
      // this will create the resources required (which are disposed of later in stop)
      reactorResourceFactory.afterPropertiesSet();

      springContext = createSpringBuilder().run(commandLineArgs());
    }

    return self();
  }

  @Override
  public T stop() {
    if (springContext != null) {
      springContext.close();
      springContext = null;
    }

    return self();
  }

  @Override
  public boolean isStarted() {
    return springContext != null && springContext.isActive();
  }

  @Override
  public  T withBean(final String qualifier, final V bean, final Class type) {
    beans.put(qualifier, new Bean<>(bean, type));
    return self();
  }

  @Override
  public int mappedPort(final TestZeebePort port) {
    return switch (port) {
      case REST -> restPort();
      case MONITORING -> monitoringPort();
      default ->
          throw new IllegalArgumentException(
              "No known port %s; must one of MONITORING".formatted(port));
    };
  }

  @Override
  public  V bean(final Class type) {
    if (springContext == null) {
      return beans.values().stream()
          .map(Bean::value)
          .filter(type::isInstance)
          .map(type::cast)
          .findFirst()
          .orElse(null);
    }

    return springContext.getBean(type);
  }

  @Override
  public  V property(final String property, final Class type, final V fallback) {
    if (springContext == null) {
      //noinspection unchecked
      return (V) propertyOverrides.getOrDefault(property, fallback);
    }

    return springContext.getEnvironment().getProperty(property, type, fallback);
  }

  @Override
  public T withProperty(final String key, final Object value) {
    propertyOverrides.put(key, value);
    return self();
  }

  @Override
  public T withAdditionalProfile(final String profile) {
    additionalProfiles.add(profile);
    return self();
  }

  /** Returns the command line arguments that will be passed when the application is started. */
  protected String[] commandLineArgs() {
    return new String[0];
  }

  /**
   * Returns a builder which can be modified to enable more profiles, inject beans, etc. Sub-classes
   * can override this to customize the behavior of the test application.
   */
  protected SpringApplicationBuilder createSpringBuilder() {
    return MainSupport.createDefaultApplicationBuilder()
        .bannerMode(Mode.OFF)
        .lazyInitialization(true)
        .registerShutdownHook(false)
        .initializers(new ContextOverrideInitializer(beans, propertyOverrides))
        .profiles(additionalProfiles.toArray(String[]::new))
        .sources(springApplication);
  }

  @Override
  public String toString() {
    return getClass().getSimpleName() + "{nodeId = " + nodeId() + "}";
  }

  private void overridePropertyIfAbsent(final String key, final Object value) {
    if (!propertyOverrides.containsKey(key)) {
      propertyOverrides.put(key, value);
    }
  }

  private int monitoringPort() {
    return serverPort("management.server.port");
  }

  private int restPort() {
    return serverPort("server.port");
  }

  private int serverPort(final String property) {
    final Object portProperty;
    if (springContext != null) {
      portProperty = springContext.getEnvironment().getProperty(property);
    } else {
      portProperty = propertyOverrides.get(property);
    }

    if (portProperty == null) {
      throw new IllegalStateException(
          "No property '%s' defined anywhere, cannot infer monitoring port".formatted(property));
    }

    if (portProperty instanceof final Integer port) {
      return port;
    }

    return Integer.parseInt(portProperty.toString());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy