Skip to content

Commit

Permalink
Merge pull request ozimov#22 from fstech/sentinel_support
Browse files Browse the repository at this point in the history
Added Sentinel support as well as an ability to create arbitrary clusters on arbitrary or >>ephemeral<< ports
  • Loading branch information
kstyrc committed Feb 16, 2015
2 parents 83c4da1 + 025ab14 commit 19b9dc4
Show file tree
Hide file tree
Showing 27 changed files with 1,444 additions and 239 deletions.
96 changes: 61 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,34 @@ embedded-redis

Redis embedded server for Java integration testing


Maven dependency
==============

Currently embedded-redis is available in clojars repository:
```
<repository>
<id>clojars.org</id>
<url>http://clojars.org/repo</url>
</repository>
```

Dependency configuration:
```
<dependency>
<groupId>redis.embedded</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.3</version>
</dependency>
```
More at https://clojars.org/redis.embedded/embedded-redis

Usage example
Usage
==============

Running RedisServer is as simple as:
```
```java
RedisServer redisServer = new RedisServer(6379);
redisServer.start();
// do some work
redisServer.stop();
```

You can also provide RedisServer with your own redis executable to run:
```
```java
RedisServer redisServer = new RedisServer("/path/to/your/redis", 6379);
```

You can also use fluent API to create RedisServer:
```
```java
RedisServer redisServer = RedisServer.builder()
.executable("/path/to/your/redis")
.port(6379)
.slaveOf("locahost", 6378)
.configFile("/path/to/your/redis.conf")
.build();
```

Or even create simple redis.conf file from scratch:
```
```java
RedisServer redisServer = RedisServer.builder()
.executable("/path/to/your/redis")
.port(6379)
Expand All @@ -58,35 +39,80 @@ RedisServer redisServer = RedisServer.builder()
.setting("appendonly no")
.build();
```
A simple redis integration test would look like this:
```

## Setting up a cluster

Our Embedded Redis has support for HA Redis clusters with Sentinels and master-slave replication

#### Using ephemeral ports
A simple redis integration test with Redis cluster on ephemeral ports, with setup similar to that from production would look like this:
```java
public class SomeIntegrationTestThatRequiresRedis {
private RedisServer redisServer;
private RedisCluster cluster;
private Set<String> jedisSentinelHosts;

@Before
public void setup() throws Exception {
redisServer = new RedisServer(6379); // or new RedisServer("/path/to/your/redis", 6379);
redisServer.start();
//creates a cluster with 3 sentinels, quorum size of 2 and 3 replication groups, each with one master and one slave
cluster = RedisCluster.builder().ephemeral().sentinelCount(3).quorumSize(2)
.replicationGroup("master1", 1)
.replicationGroup("master2", 1)
.replicationGroup("master3", 1)
.build();
cluster.start();

//retrieve ports on which sentinels have been started, using a simple Jedis utility class
jedisSentinelHosts = JedisUtil.sentinelHosts(cluster);
}

@Test
public void test() throws Exception {
// testing code that requires redis running
JedisSentinelPool pool = new JedisSentinelPool("master1", jedisSentinelHosts);
}

@After
public void tearDown() throws Exception {
redisServer.stop();
cluster.stop();
}
}
```

#### Retrieving ports
The above example starts Redis cluster on ephemeral ports, which you can later get with ```cluster.ports()```,
which will return a list of all ports of the cluster. You can also get ports of sentinels with ```cluster.sentinelPorts()```
or servers with ```cluster.serverPorts()```. ```JedisUtil``` class contains utility methods for use with Jedis client.

#### Using predefined ports
You can also start Redis cluster on predefined ports and even mix both approaches:
```java
public class SomeIntegrationTestThatRequiresRedis {
private RedisCluster cluster;

@Before
public void setup() throws Exception {
final List<Integer> sentinels = Arrays.asList(26739, 26912);
final List<Integer> group1 = Arrays.asList(6667, 6668);
final List<Integer> group2 = Arrays.asList(6387, 6379);
//creates a cluster with 3 sentinels, quorum size of 2 and 3 replication groups, each with one master and one slave
cluster = RedisCluster.builder().sentinelPorts(sentinels).quorumSize(2)
.serverPorts(group1).replicationGroup("master1", 1)
.serverPorts(group2).replicationGroup("master2", 1)
.ephemeralServers().replicationGroup("master3", 1)
.build();
cluster.start();
}
//(...)
```
The above will create and start a cluster with sentinels on ports ```26739, 26912```, first replication group on ```6667, 6668```,
second replication group on ```6387, 6379``` and third replication group on ephemeral ports.

Redis version
==============

When not provided with the desired redis executable, RedisServer runs os-dependent executable enclosed in jar. Currently is uses:
- Redis 2.6.14 in case of Linux/Unix
- Redis 2.8.19 in case of Linux/Unix
- Redis 2.8.19 in case of OSX
- unofficial Win32/64 port from https://github.com/MSOpenTech/redis (branch 2.6) in case of Windows

However, you should provide RedisServer with redis executable if you need specific version.
22 changes: 16 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,23 @@
</distributionManagement>

<dependencies>

<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.1.0</version>
<version>2.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.8.5</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
Expand All @@ -75,7 +85,7 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.1.0.RELEASE</version>
<version>1.4.1.RELEASE</version>
<scope>test</scope>
</dependency>

Expand All @@ -88,9 +98,9 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<compilerVersion>1.6</compilerVersion>
<source>1.6</source>
<target>1.6</target>
<compilerVersion>1.8</compilerVersion>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

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

import redis.embedded.exceptions.EmbeddedRedisException;

import java.io.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* Created by piotrturek on 22/01/15.
*/
abstract class AbstractRedisInstance implements Redis {
protected List<String> args = Collections.emptyList();
private volatile boolean active = false;
private Process redisProcess;
private final int port;

private final ExecutorService executor = Executors.newSingleThreadExecutor();

protected AbstractRedisInstance(int port) {
this.port = port;
}

@Override
public boolean isActive() {
return active;
}

@Override
public synchronized void start() throws EmbeddedRedisException {
if (active) {
throw new EmbeddedRedisException("This redis server instance is already running...");
}
try {
redisProcess = createRedisProcessBuilder().start();
logErrors();
awaitRedisServerReady();
active = true;
} catch (IOException e) {
throw new EmbeddedRedisException("Failed to start Reddis instance", e);
}
}

private void logErrors() {
final InputStream errorStream = redisProcess.getErrorStream();
executor.submit(() -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
});

}

private void awaitRedisServerReady() throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(redisProcess.getInputStream()))) {
String outputLine;
do {
outputLine = reader.readLine();
if (outputLine == null) {
//Something goes wrong. Stream is ended before server was activated.
throw new RuntimeException("Can't start redis server. Check logs for details.");
}
} while (!outputLine.matches(redisReadyPattern()));
}
}

protected abstract String redisReadyPattern();

private ProcessBuilder createRedisProcessBuilder() {
File executable = new File(args.get(0));
ProcessBuilder pb = new ProcessBuilder(args);
pb.directory(executable.getParentFile());
return pb;
}

@Override
public synchronized void stop() throws EmbeddedRedisException {
if (active) {
redisProcess.destroy();
tryWaitFor();
active = false;
}
}

private void tryWaitFor() {
try {
redisProcess.waitFor();
} catch (InterruptedException e) {
throw new EmbeddedRedisException("Failed to stop redis instance", e);
}
}

@Override
public List<Integer> ports() {
return Arrays.asList(port);
}
}
8 changes: 8 additions & 0 deletions src/main/java/redis/embedded/PortProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package redis.embedded;

/**
* Created by piotrturek on 29/01/15.
*/
public interface PortProvider {
int next();
}
18 changes: 18 additions & 0 deletions src/main/java/redis/embedded/Redis.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package redis.embedded;

import redis.embedded.exceptions.EmbeddedRedisException;

import java.util.List;

/**
* Created by piotrturek on 22/01/15.
*/
public interface Redis {
boolean isActive();

void start() throws EmbeddedRedisException;

void stop() throws EmbeddedRedisException;

List<Integer> ports();
}
Loading

0 comments on commit 19b9dc4

Please sign in to comment.