HikariCredentialsUpdater.java

package com.maybeitssquid.rotatingsecrets.hikari;

import com.maybeitssquid.rotatingsecrets.UpdatableCredential;
import com.zaxxer.hikari.HikariCredentialsProvider;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.util.Credentials;

/**
 * Handles credential updates for HikariCP data sources.
 *
 * <p>This class implements both {@link UpdatableCredential} and {@link HikariCredentialsProvider}
 * interfaces, serving as a bridge between the credential rotation system and HikariCP's built-in
 * credential provider mechanism. It receives credential change notifications from the {@link
 * com.maybeitssquid.rotatingsecrets.CredentialsProviderService} and provides those credentials to
 * HikariCP when new connections are created.
 *
 * <h2>How It Works</h2>
 *
 * <ol>
 *   <li>HikariCP is configured to use this class as its {@link HikariCredentialsProvider}
 *   <li>When HikariCP needs credentials for a new connection, it calls {@link #getCredentials()}
 *   <li>When credentials are rotated, {@link #setCredential(String, String)} is called
 *   <li>The updater stores the new credentials and triggers soft eviction of existing connections
 *   <li>Soft eviction marks connections for closure after they are returned to the pool
 *   <li>New connections use the updated credentials from {@link #getCredentials()}
 * </ol>
 *
 * <h2>Thread Safety</h2>
 *
 * <p>This class is thread-safe. The {@link Credentials} object is immutable and is replaced
 * atomically when credentials are updated. The soft eviction operation is thread-safe as
 * implemented by HikariCP.
 *
 * @see UpdatableCredential
 * @see HikariCredentialsProvider
 * @see HikariDataSourceConfig
 */
public class HikariCredentialsUpdater
    implements UpdatableCredential<String>, HikariCredentialsProvider {

  /**
   * Reference to the HikariCP DataSource, used for connection eviction. May be null during
   * initialization.
   */
  private volatile HikariDataSource dataSource;

  /** Current credentials to provide to HikariCP. Replaced atomically on credential updates. */
  private Credentials credentials;

  /**
   * Creates a new credentials updater with the specified initial credentials.
   *
   * @param username the initial database username
   * @param password the initial database password
   */
  public HikariCredentialsUpdater(String username, String password) {
    this.credentials = new Credentials(username, password);
  }

  /**
   * Sets the HikariCP DataSource reference for connection eviction.
   *
   * <p>This method is called after the DataSource is created to enable soft eviction of connections
   * when credentials are rotated. The circular dependency (DataSource needs credentials provider,
   * credentials provider needs DataSource for eviction) is resolved by injecting the DataSource
   * after creation.
   *
   * @param dataSource the HikariCP DataSource to manage
   */
  public void setDataSource(HikariDataSource dataSource) {
    this.dataSource = dataSource;
  }

  /**
   * Updates the stored credentials and soft-evicts existing connections.
   *
   * <p>This method atomically replaces the stored credentials with new ones, then triggers a soft
   * eviction of all existing connections in the pool. Soft eviction marks connections for closure
   * after they are returned to the pool, allowing in-flight transactions to complete while ensuring
   * new checkouts get fresh connections with the updated credentials.
   *
   * @param username the new database username
   * @param credential the new database password
   */
  @Override
  public void setCredential(final String username, final String credential) {
    this.credentials = new Credentials(username, credential);
    if (dataSource != null && dataSource.getHikariPoolMXBean() != null) {
      dataSource.getHikariPoolMXBean().softEvictConnections();
    }
  }

  /**
   * Provides the current credentials to HikariCP for new connection creation.
   *
   * <p>This method is called by HikariCP's connection factory when creating new connections. It
   * returns the most recently set credentials.
   *
   * @return the current credentials for database connections
   */
  @Override
  public Credentials getCredentials() {
    return this.credentials;
  }
}