PoolMaintenanceTest.java

package redis.clients.jedis;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

import org.apache.commons.pool2.impl.EvictionPolicy;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;

/**
 * Unit tests for {@link PoolMaintenance}: what each family contributes to its pool ��� controller
 * identity, factory knobs, pool-side reactions ��� and the controller lifetime the kit owns.
 */
public class PoolMaintenanceTest {

  private static final HostAndPort NODE = new HostAndPort("127.0.0.1", 6379);

  private final MaintenanceNotificationsConfig enabled = MaintenanceNotificationsConfig.builder()
      .build();

  // --- OFF ---

  @Test
  public void standaloneIsOffForNullOrDisabledConfig() {
    assertSame(PoolMaintenance.OFF, PoolMaintenance.standalone(null));
    assertSame(PoolMaintenance.OFF,
      PoolMaintenance.standalone(MaintenanceNotificationsConfig.DISABLED));
  }

  @Test
  public void clusterIsOffForNullCoordinator() {
    assertSame(PoolMaintenance.OFF, PoolMaintenance.cluster(null));
  }

  @Test
  public void offHasNoControllerConfiguresNothingAndAttachesNothing() {
    ConnectionFactory.Builder factoryBuilder = mock(ConnectionFactory.Builder.class);
    ConnectionPool pool = mock(ConnectionPool.class);

    assertNull(PoolMaintenance.OFF.controller());
    assertSame(factoryBuilder, PoolMaintenance.OFF.configure(factoryBuilder));
    PoolMaintenance.OFF.attach(pool);
    assertDoesNotThrow(PoolMaintenance.OFF::close);

    verifyNoInteractions(factoryBuilder, pool);
  }

  // --- standalone family ---

  @Test
  public void standaloneWiresItsControllerAsControllerAndAddressMapper() {
    PoolMaintenance kit = PoolMaintenance.standalone(enabled);
    try {
      MaintenanceEventController controller = assertInstanceOf(MaintenanceEventController.class,
        kit.controller());
      assertSame(enabled, controller.getConfig());
      ConnectionFactory.Builder factoryBuilder = mock(ConnectionFactory.Builder.class);
      when(factoryBuilder.maintenanceController(controller)).thenReturn(factoryBuilder);
      when(factoryBuilder.socketAddressMapper(controller)).thenReturn(factoryBuilder);

      assertSame(factoryBuilder, kit.configure(factoryBuilder));

      verify(factoryBuilder).maintenanceController(same(controller));
      verify(factoryBuilder).socketAddressMapper(same(controller));
    } finally {
      kit.close();
    }
  }

  @Test
  public void standaloneAttachInstallsRetiredAwareEvictionAndEvictsOnHandoff() throws Exception {
    PoolMaintenance kit = PoolMaintenance.standalone(enabled);
    try {
      MaintenanceEventController controller = (MaintenanceEventController) kit.controller();
      ConnectionPool pool = mock(ConnectionPool.class);
      @SuppressWarnings("unchecked")
      EvictionPolicy<Connection> delegate = mock(EvictionPolicy.class);
      when(pool.getEvictionPolicy()).thenReturn(delegate);

      kit.attach(pool);

      verify(pool).setEvictionPolicy(isA(RebindAwareEvictionPolicy.class));
      verify(pool, never()).evict();
      controller.getHandoffHook().run(); // a processed handoff evicts the retired idles right away
      verify(pool).evict();
    } finally {
      kit.close();
    }
  }

  @Test
  public void standaloneHandoffEvictionSkipsAClosedPoolAndNeverPropagates() throws Exception {
    PoolMaintenance kit = PoolMaintenance.standalone(enabled);
    try {
      MaintenanceEventController controller = (MaintenanceEventController) kit.controller();
      ConnectionPool pool = mock(ConnectionPool.class);
      kit.attach(pool);

      when(pool.isClosed()).thenReturn(true);
      controller.getHandoffHook().run();
      verify(pool, never()).evict();

      when(pool.isClosed()).thenReturn(false);
      doThrow(new IllegalStateException("evictor failed")).when(pool).evict();
      assertDoesNotThrow(controller.getHandoffHook()::run,
        "a failed pass degrades to lazy recycling on return; it must not reach the notifier");
      verify(pool).evict();
    } finally {
      kit.close();
    }
  }

  @Test
  public void standaloneCloseClosesItsController() {
    try (MockedConstruction<MaintenanceEventController> constructed = mockConstruction(
      MaintenanceEventController.class)) {
      PoolMaintenance kit = PoolMaintenance.standalone(enabled);
      assertEquals(1, constructed.constructed().size());
      MaintenanceEventController controller = constructed.constructed().get(0);
      assertSame(controller, kit.controller());

      kit.close();

      verify(controller).close();
    }
  }

  // --- cluster family ---

  @Test
  public void clusterWiresItsControllerWithoutAnAddressMapper() {
    ClusterMaintenanceCoordinator coordinator = new ClusterMaintenanceCoordinator(
        mock(JedisClusterInfoCache.class), enabled);
    PoolMaintenance kit = PoolMaintenance.cluster(coordinator);
    try {
      ClusterMaintenanceController controller = assertInstanceOf(ClusterMaintenanceController.class,
        kit.controller());
      assertSame(enabled, controller.getConfig());
      ConnectionFactory.Builder factoryBuilder = mock(ConnectionFactory.Builder.class);
      when(factoryBuilder.maintenanceController(controller)).thenReturn(factoryBuilder);

      assertSame(factoryBuilder, kit.configure(factoryBuilder));

      verify(factoryBuilder).maintenanceController(same(controller));
      // no MOVING in the cluster family: new connections are never remapped
      verify(factoryBuilder, never()).socketAddressMapper(any());
    } finally {
      kit.close();
    }
  }

  @Test
  public void clusterAttachesNothingToThePool() {
    ClusterMaintenanceCoordinator coordinator = new ClusterMaintenanceCoordinator(
        mock(JedisClusterInfoCache.class), enabled);
    PoolMaintenance kit = PoolMaintenance.cluster(coordinator);
    try {
      ConnectionPool pool = mock(ConnectionPool.class);

      kit.attach(pool);

      verifyNoInteractions(pool);
    } finally {
      kit.close();
    }
  }

  @Test
  public void clusterCloseClosesItsController() {
    ClusterMaintenanceCoordinator coordinator = new ClusterMaintenanceCoordinator(
        mock(JedisClusterInfoCache.class), enabled);
    try (MockedConstruction<ClusterMaintenanceController> constructed = mockConstruction(
      ClusterMaintenanceController.class)) {
      PoolMaintenance kit = PoolMaintenance.cluster(coordinator);
      ClusterMaintenanceController controller = constructed.constructed().get(0);
      assertSame(controller, kit.controller());

      kit.close();

      verify(controller).close();
    }
  }

  // --- the pool runs the kit ---

  @Test
  public void poolExposesTheKitsControllerAndClosesItOnDestroy() {
    ClusterMaintenanceCoordinator coordinator = new ClusterMaintenanceCoordinator(
        mock(JedisClusterInfoCache.class), enabled);
    try (MockedConstruction<ClusterMaintenanceController> constructed = mockConstruction(
      ClusterMaintenanceController.class,
      (mock, context) -> when(mock.getConfig()).thenReturn(enabled))) {
      ConnectionPool pool = new ConnectionPool(NODE, DefaultJedisClientConfig.builder().build(),
          null, new GenericObjectPoolConfig<>(), PoolMaintenance.cluster(coordinator));
      ClusterMaintenanceController controller = constructed.constructed().get(0);
      assertSame(controller, pool.getMaintenanceController());
      verify(controller, never()).close();

      pool.destroy();

      verify(controller).close();
    }
  }

  @Test
  public void poolWithMaintenanceOffHasNoController() {
    ConnectionPool pool = new ConnectionPool(NODE, DefaultJedisClientConfig.builder().build(), null,
        new GenericObjectPoolConfig<>(), PoolMaintenance.OFF);
    try {
      assertNull(pool.getMaintenanceController());
    } finally {
      pool.destroy();
    }
  }
}