RedisEnterpriseClusterStatusUnitTests.java
package redis.clients.jedis.scenario;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
/**
* Pins the `rladmin status` parse offline. This is the one piece of the Redis Enterprise Sentinel
* scenario suite that can regress silently - a column-order change would otherwise only show up as
* a confusing failure against a live cluster.
* <p>
* Named {@code *UnitTests} rather than {@code *Test} because surefire excludes
* {@code **}{@code /scenario/*Test.java}.
*/
@Tag("unit")
public class RedisEnterpriseClusterStatusUnitTests {
/**
* Sections are shaped after a real {@code rladmin status}. Addresses come from the ranges RFC
* 5737 and RFC 1918 reserve for documentation and private use, so nothing here points at a real
* cluster.
*/
private static final String NODES = "CLUSTER NODES:\n"
+ "NODE:ID ROLE ADDRESS EXTERNAL_ADDRESS HOSTNAME SHARDS CORES VERSION STATUS\n"
+ "*node:1 master 10.0.0.1 192.0.2.1 ip-10-0-0-1 3/100 2 8.0.22-44 OK\n"
+ "node:2 slave 10.0.0.2 192.0.2.2 ip-10-0-0-2 3/100 2 8.0.22-44 OK\n"
+ "node:3 slave 10.0.0.3 192.0.2.3 ip-10-0-0-3 1/100 2 8.0.22-44 OK\n" + "\n";
/** Three databases, one of which loads modules, so the MODULE column is present. */
private static final String DATABASES = "DATABASES:\n"
+ "DB:ID NAME TYPE MODULE STATUS ENDPOINT\n"
+ "db:1 re-standalone redis search active redis-12000.example:12000\n"
+ "db:2 m-standard redis active redis-12003.example:12003\n"
+ "db:3 re-single-shard-oss-cluster redis active redis-12001.example:12001\n" + "\n";
private static final String ENDPOINTS = "ENDPOINTS:\n"
+ "DB:ID NAME ID NODE ROLE SSL\n"
+ "db:1 re-standalone endpoint:1:1 node:2 all-master-shards No\n"
+ "db:2 m-standard endpoint:2:1 node:3 single No\n"
+ "db:3 re-single-shard-oss-cluster endpoint:3:1 node:3 all-master-shards No\n" + "\n";
private static final String SHARDS = "SHARDS:\n"
+ "DB:ID NAME ID NODE ROLE SLOTS USED_MEMORY STATUS\n"
+ "db:2 m-standard redis:2 node:3 master 0-8191 2.5MB OK\n"
+ "db:2 m-standard redis:3 node:1 slave 0-8191 2.5MB OK\n";
private static final String STATUS = NODES + DATABASES + ENDPOINTS + SHARDS;
/**
* What {@code rladmin bind endpoint 2:1 include 1} leaves behind: the node is unioned onto the
* proxy set rather than the endpoint being moved, so rladmin prints one row per proxy.
*/
private static final String TWO_PROXY_ENDPOINTS = "ENDPOINTS:\n"
+ "DB:ID NAME ID NODE ROLE SSL\n"
+ "db:2 m-standard endpoint:2:1 node:3 single No\n"
+ "db:2 m-standard endpoint:2:1 node:1 single No\n";
@Test
public void parsesTheNameEndpointAndProxyOfTheRequestedDatabase() {
RedisEnterpriseClusterStatus status = RedisEnterpriseClusterStatus.parse(STATUS, "2");
assertEquals("m-standard", status.getDbName());
assertEquals("2:1", status.getEndpointUid());
assertEquals(Collections.singletonList("3"), status.getProxyNodeUids());
}
@Test
public void picksTheRequestedDatabaseRatherThanTheFirstRow() {
RedisEnterpriseClusterStatus first = RedisEnterpriseClusterStatus.parse(STATUS, "1");
assertEquals("re-standalone", first.getDbName());
assertEquals("1:1", first.getEndpointUid());
assertEquals(Collections.singletonList("2"), first.getProxyNodeUids());
RedisEnterpriseClusterStatus third = RedisEnterpriseClusterStatus.parse(STATUS, "3");
assertEquals("re-single-shard-oss-cluster", third.getDbName());
assertEquals("3:1", third.getEndpointUid());
}
@Test
public void reportsEveryProxyOfAMultiProxyEndpoint() {
// Keeping one arbitrary row is what let the endpoint-move test drain a proxy the discovery
// service was not tracking, which Redis Enterprise does not announce.
RedisEnterpriseClusterStatus status = RedisEnterpriseClusterStatus
.parse(NODES + DATABASES + TWO_PROXY_ENDPOINTS, "2");
assertEquals(Arrays.asList("3", "1"), status.getProxyNodeUids());
assertEquals("2:1", status.getEndpointUid());
}
@Test
public void resolvesANodeFromEitherOfItsAddresses() {
RedisEnterpriseClusterStatus status = RedisEnterpriseClusterStatus.parse(STATUS, "2");
// The discovery service reports external addresses; the @internal master name carries the
// internal ones. The master row is prefixed with '*', which must not hide the node.
assertEquals("1", status.findNodeByAddress("192.0.2.1"));
assertEquals("1", status.findNodeByAddress("10.0.0.1"));
assertEquals("3", status.findNodeByAddress("192.0.2.3"));
}
@Test
public void doesNotGuessANodeForAnUnknownAddress() {
RedisEnterpriseClusterStatus status = RedisEnterpriseClusterStatus.parse(STATUS, "2");
assertNull(status.findNodeByAddress("203.0.113.7"));
assertNull(status.findNodeByAddress(""));
assertNull(status.findNodeByAddress(null));
}
@Test
public void readsTheNameFromDatabasesAndNotFromShardsOrEndpoints() {
// A db:<id> row appears in DATABASES, ENDPOINTS and SHARDS alike. Only sectioning keeps the
// name from being read off a SHARDS row, where column 2 happens to be the name as well but
// column 3 is a shard id rather than an endpoint id.
RedisEnterpriseClusterStatus status = RedisEnterpriseClusterStatus.parse(STATUS, "2");
assertEquals("m-standard", status.getDbName());
assertTrue(status.getRawStatus().contains("SHARDS:"));
}
@Test
public void failsLoudlyWhenTheDatabaseIsAbsent() {
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> RedisEnterpriseClusterStatus.parse(STATUS, "99"));
// The raw output has to travel with the failure, otherwise a format drift is undiagnosable.
assertTrue(e.getMessage().contains("bdb 99"));
assertTrue(e.getMessage().contains("ENDPOINTS:"));
}
@Test
public void failsLoudlyOnEmptyOutput() {
assertThrows(IllegalStateException.class, () -> RedisEnterpriseClusterStatus.parse("", "2"));
assertThrows(IllegalStateException.class, () -> RedisEnterpriseClusterStatus.parse(null, "2"));
}
}