Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow sharded cluster with custom directory/executable provider to be restarted #28

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
44 changes: 44 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
HELP.md
.gradle
target/
!.mvn/wrapper/maven-wrapper.jar
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/

### Kotlin ###
.kotlin
44 changes: 41 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@
<maven.compiler.release>8</maven.compiler.release>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<dependency>
Expand All @@ -56,9 +68,29 @@
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>

Expand Down Expand Up @@ -124,6 +156,12 @@
<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>

<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
Expand Down
44 changes: 41 additions & 3 deletions src/main/java/redis/embedded/RedisInstance.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package redis.embedded;

import java.io.File;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.embedded.model.RedisConfig;
import redis.embedded.util.IO;
import redis.embedded.util.RedisConfigParser;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import static redis.embedded.util.IO.*;

public abstract class RedisInstance implements Redis {

private static final Logger LOGGER = LoggerFactory.getLogger(RedisInstance.class);

private final Pattern readyPattern;
private final int port;
private final List<String> args;
Expand All @@ -38,7 +47,7 @@ public synchronized void start() throws IOException {

try {
process = new ProcessBuilder(args)
.directory(new File(args.get(0)).getParentFile())
.directory(getRedisDir().toFile())
.start();
addShutdownHook("RedisInstanceCleaner", checkedToRuntime(this::stop));
awaitServerReady(process, readyPattern, soutListener, serrListener);
Expand All @@ -54,6 +63,10 @@ public synchronized void start() throws IOException {
}
}

private Path getRedisDir() {
return Paths.get(args.get(0)).getParent();
}

// You might get an error when you try to start the default binary without having openssl installed. The default
// binaries have TLS support but require a library on the host OS. On MacOS you will probably get an error that
// looks like this:
Expand Down Expand Up @@ -108,9 +121,34 @@ public synchronized void stop() throws IOException {
active = false;
} catch (final InterruptedException e) {
throw new IOException("Failed to stop redis service", e);
} finally {
findAndDeleteClusterConfigFiles();
}
}

private void findAndDeleteClusterConfigFiles() {
findRedisConfigFile().ifPresent(this::findAndDeleteClusterConfigFiles);
}

private Optional<Path> findRedisConfigFile() {
return args.stream().filter(arg -> arg.endsWith(".conf")).findFirst().map(Paths::get);
}

private void findAndDeleteClusterConfigFiles(final Path redisConfig) {
try {
final RedisConfig config = new RedisConfigParser().parse(redisConfig);
config.directives("cluster-config-file")
.stream()
.map(RedisConfig.Directive::arguments)
.flatMap(List::stream)
.map(clusterConfFile -> getRedisDir().resolve(clusterConfFile))
.forEach(IO::deleteSafely);
} catch (IOException e) {
LOGGER.error("Unable to parse redis config file", e);
}

}

public boolean isActive() {
return active;
}
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/redis/embedded/RedisShardedCluster.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package redis.embedded;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
Expand All @@ -14,6 +16,8 @@

public final class RedisShardedCluster implements Redis {

private static final Logger LOGGER = LoggerFactory.getLogger(RedisShardedCluster.class);

private static final String CLUSTER_IP = "127.0.0.1";
private static final int MAX_NUMBER_OF_SLOTS_PER_CLUSTER = 16384;
private static final Duration SLEEP_DURATION = Duration.ofMillis(300);
Expand Down Expand Up @@ -56,8 +60,22 @@ public void start() throws IOException {

@Override
public void stop() throws IOException {
for (final Redis redis : servers) {
final List<Exception> exceptions = new ArrayList<>();
for (final Redis redis : servers) {
stopSafely(redis).ifPresent(exceptions::add);
}
if (!exceptions.isEmpty()) {
throw new IOException("Failed to stop Redis cluster", exceptions.get(0));
}
}

private Optional<Exception> stopSafely(final Redis redis) {
try {
redis.stop();
return Optional.empty();
} catch (final IOException | RuntimeException e) {
LOGGER.error("Failed to stop Redis instance", e);
return Optional.of(e);
}
}

Expand Down
65 changes: 65 additions & 0 deletions src/main/java/redis/embedded/model/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package redis.embedded.model;

import redis.embedded.util.StringUtils;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
* Representation of a {@code redis.conf} file, i.e. the configuration of a Redis server.
*/
public class RedisConfig {

private final List<Directive> directives;

public RedisConfig(final List<Directive> directives) {
Objects.requireNonNull(directives);
this.directives = Collections.unmodifiableList(directives);
}

public List<Directive> directives() {
return directives;
}

public List<Directive> directives(final String keyword) {
return directives.stream().filter(dir -> dir.keyword().equals(keyword)).collect(Collectors.toList());
}

public static class Directive {

private final String keyword;

private final List<String> arguments;

public Directive(final String keyword, final String... arguments) {
this(keyword, Arrays.asList(arguments));
}

public Directive(final String keyword, final List<String> arguments) {
require(StringUtils::isNotBlank, keyword, "Keyword must not be blank");
require(str -> str.matches("[a-zA-Z0-9_-]+"), keyword, "Keyword '" + keyword + "' contains illegal characters. Only alphanumeric characters, hyphens and underscores are allowed");
require(list -> list != null && !list.isEmpty(), arguments, "At least one argument is required");

this.keyword = Objects.requireNonNull(keyword);
this.arguments = Collections.unmodifiableList(arguments);
}

public String keyword() {
return keyword;
}

public List<String> arguments() {
return arguments;
}

private <T> void require(final Predicate<T> predicate, final T value, final String message) {
if (!predicate.test(value)) {
throw new IllegalArgumentException(message);
}
}
}
}
12 changes: 12 additions & 0 deletions src/main/java/redis/embedded/util/IO.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package redis.embedded.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
Expand All @@ -15,6 +18,8 @@

public enum IO {;

private static final Logger LOGGER = LoggerFactory.getLogger(IO.class);

public static File newTempDirForBinary() throws IOException {
final File tempDirectory = createDirectories(createTempDirectory("redis-")).toFile();
tempDirectory.deleteOnExit();
Expand Down Expand Up @@ -105,4 +110,11 @@ private static Path findBinaryInPath(final String name, final String pathVar) th
return location.get();
}

public static void deleteSafely(final Path path) {
try {
Files.deleteIfExists(path);
} catch (final IOException e) {
LOGGER.warn("Failed to delete path " + path, e);
}
}
}
Loading