HikariDataSourceConfig.java

package com.maybeitssquid.rotatingsecrets.hikari;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

/**
 * Spring configuration for HikariCP DataSource with dynamic credential rotation support.
 *
 * <p>This configuration creates a HikariCP {@link HikariDataSource} as the primary application
 * DataSource. HikariCP is a high-performance JDBC connection pool that is the default for Spring
 * Boot applications. This configuration integrates HikariCP with the credential rotation system via
 * {@link HikariCredentialsUpdater}.
 *
 * <h2>Credential Rotation</h2>
 *
 * <p>Unlike standard HikariCP configurations that use static credentials, this configuration uses a
 * {@link com.zaxxer.hikari.HikariCredentialsProvider} to support dynamic credentials. When
 * credentials are rotated:
 *
 * <ol>
 *   <li>The {@link HikariCredentialsUpdater} receives the new credentials
 *   <li>The updater stores the new credentials for future connections
 *   <li>Existing connections are soft-evicted via {@code softEvictConnections()}
 *   <li>New connections automatically use the updated credentials
 * </ol>
 *
 * <h2>Configuration Properties</h2>
 *
 * <p>Standard Spring datasource properties:
 *
 * <ul>
 *   <li>{@code spring.datasource.url} - JDBC URL (required)
 *   <li>{@code spring.datasource.driver-class-name} - JDBC driver class (required)
 *   <li>{@code spring.datasource.username} - Initial database username (required)
 *   <li>{@code spring.datasource.password} - Initial database password (required)
 * </ul>
 *
 * <p>HikariCP-specific properties (prefixed with {@code spring.datasource.hikari.}):
 *
 * <ul>
 *   <li>{@code pool-name} - Connection pool name (default: HikariPool)
 *   <li>{@code maximum-pool-size} - Maximum connections in pool (default: 10)
 *   <li>{@code minimum-idle} - Minimum idle connections (default: 2)
 *   <li>{@code idle-timeout} - Milliseconds before idle connection is removed (default: 30000)
 *   <li>{@code connection-timeout} - Milliseconds to wait for connection (default: 20000)
 *   <li>{@code max-lifetime} - Maximum connection lifetime in milliseconds (default: 1800000)
 * </ul>
 *
 * @see HikariDataSource
 * @see HikariCredentialsUpdater
 */
@Configuration
public class HikariDataSourceConfig {

  /** Default constructor. */
  public HikariDataSourceConfig() {
    // Configuration class for HikariCP DataSource
  }

  private static final Logger log = LoggerFactory.getLogger(HikariDataSourceConfig.class);

  /** Reference to the created DataSource for cleanup. */
  private HikariDataSource createdDataSource;

  /** JDBC URL for the database connection. */
  @Value("${spring.datasource.url}")
  private String url;

  /** Fully qualified class name of the JDBC driver. */
  @Value("${spring.datasource.driver-class-name}")
  private String driverClassName;

  /** Initial database username for pool connections. */
  @Value("${spring.datasource.username}")
  private String username;

  /** Initial database password for pool connections. */
  @Value("${spring.datasource.password}")
  private String password;

  /** Name of the connection pool for identification and JMX registration. */
  @Value("${spring.datasource.hikari.pool-name:HikariPool}")
  private String poolName;

  /** Maximum number of connections in the pool (active + idle). */
  @Value("${spring.datasource.hikari.maximum-pool-size:10}")
  private int maximumPoolSize;

  /** Minimum number of idle connections the pool maintains. */
  @Value("${spring.datasource.hikari.minimum-idle:2}")
  private int minimumIdle;

  /** Milliseconds a connection can remain idle before being removed. */
  @Value("${spring.datasource.hikari.idle-timeout:30000}")
  private long idleTimeout;

  /** Milliseconds to wait when requesting a connection before timing out. */
  @Value("${spring.datasource.hikari.connection-timeout:20000}")
  private long connectionTimeout;

  /** Maximum lifetime of a connection in milliseconds before it is retired. */
  @Value("${spring.datasource.hikari.max-lifetime:1800000}")
  private long maxLifetime;

  /**
   * Creates the HikariCP configuration bean.
   *
   * <p>This configuration is used to create the {@link HikariDataSource}. Note that while initial
   * credentials are set here, the actual credentials used for connections come from the {@link
   * HikariCredentialsUpdater} via the credentials provider interface.
   *
   * @return a configured HikariConfig
   */
  @Bean
  public HikariConfig hikariConfig() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl(url);
    config.setDriverClassName(driverClassName);
    config.setUsername(username);
    config.setPassword(password);
    config.setPoolName(poolName);
    config.setMaximumPoolSize(maximumPoolSize);
    config.setMinimumIdle(minimumIdle);
    config.setIdleTimeout(idleTimeout);
    config.setConnectionTimeout(connectionTimeout);
    config.setMaxLifetime(maxLifetime);
    return config;
  }

  /**
   * Creates the primary HikariCP DataSource bean with credential rotation support.
   *
   * <p>This method wires up the credential rotation infrastructure:
   *
   * <ol>
   *   <li>Sets the {@link HikariCredentialsUpdater} as the credentials provider on the config
   *   <li>Creates the {@link HikariDataSource} from the config
   *   <li>Injects the DataSource back into the updater for connection eviction support
   * </ol>
   *
   * <p>The resulting DataSource is marked as {@code @Primary}, making it the default DataSource for
   * the application when multiple DataSource beans exist.
   *
   * @param hikariConfig the HikariCP configuration
   * @param credentialsUpdater the credentials updater that provides dynamic credentials
   * @return a configured HikariDataSource with credential rotation support
   */
  @Bean
  @Primary
  public HikariDataSource dataSource(
      HikariConfig hikariConfig, HikariCredentialsUpdater credentialsUpdater) {
    // Set the credentials provider BEFORE creating the datasource
    hikariConfig.setCredentialsProvider(credentialsUpdater);
    HikariDataSource dataSource = new HikariDataSource(hikariConfig);
    // Inject datasource back into updater for connection eviction
    credentialsUpdater.setDataSource(dataSource);
    // Store reference for cleanup
    this.createdDataSource = dataSource;
    return dataSource;
  }

  /**
   * Explicitly closes the HikariDataSource on application shutdown.
   *
   * <p>While Spring typically handles DataSource cleanup, explicit closure ensures all connections
   * are properly released.
   */
  @PreDestroy
  public void closeDataSource() {
    if (createdDataSource != null && !createdDataSource.isClosed()) {
      log.info("Closing HikariDataSource: {}", createdDataSource.getPoolName());
      createdDataSource.close();
    }
  }

  /**
   * Creates the HikariCP credentials updater bean for credential rotation support.
   *
   * <p>The updater is initialized with the configured username and password, and is registered with
   * the {@link com.maybeitssquid.rotatingsecrets.CredentialsProviderService} to receive
   * notifications when credentials are rotated.
   *
   * @return a credentials updater initialized with the configured credentials
   * @see HikariCredentialsUpdater
   */
  @Bean("hikariUpdater")
  public HikariCredentialsUpdater hikariCredentialsUpdater() {
    return new HikariCredentialsUpdater(username, password);
  }
}