Repository: fppt/jedis-mock (java)

Issue:
RedisServer is ready support
Currently, the RedisServer class lacks a convenient mechanism to notify external code when the server has fully started and is ready to accept requests. I would be happy if a method or mechanism is introduced in the RedisServer class that signals when the server has completed its startup and initialization processes. 

Example.
```java

// Example if server is already running:
server.stop();
// - Here assert request that are using the mock server fails
server.start();
Awaitility.await().until(server::isReady);
// Proceed with assertions or operations requiring the server to be operational
```

Pull request description:
implement isRunning and fix a minor thread leak on RedisServer::stop
Fix #475

Patch:
diff --git a/src/main/java/com/github/fppt/jedismock/RedisServer.java b/src/main/java/com/github/fppt/jedismock/RedisServer.java
index 0e90517..8420189 100644
--- a/src/main/java/com/github/fppt/jedismock/RedisServer.java
+++ b/src/main/java/com/github/fppt/jedismock/RedisServer.java
@@ -20,13 +20,15 @@ import java.util.concurrent.Future;
  */
 public class RedisServer {
 
+    private static final String NOT_RUNNING = "JedisMock is not running";
+
     private final int bindPort;
     private final InetAddress bindAddress;
     private final Map<Integer, RedisBase> redisBases;
-    private final ExecutorService threadPool;
-    private RedisService service;
-    private ServiceOptions options = ServiceOptions.defaultOptions();
-    private Future<Void> serviceFinalization;
+    private volatile ExecutorService threadPool;
+    private volatile RedisService service;
+    private volatile ServiceOptions options = ServiceOptions.defaultOptions();
+    private volatile Future<Void> serviceFinalization;
 
     public RedisServer() {
         this(0);
@@ -41,7 +43,6 @@ public class RedisServer {
         this.bindPort = port;
         this.bindAddress = address;
         this.redisBases = new HashMap<>();
-        this.threadPool = Executors.newSingleThreadExecutor();
         CommandFactory.initialize();
     }
 
@@ -68,12 +69,13 @@ public class RedisServer {
             throw new IllegalStateException();
         }
         this.service = new RedisService(bindPort, bindAddress, redisBases, options);
+        threadPool = Executors.newSingleThreadExecutor();
         serviceFinalization = threadPool.submit(service);
         return this;
     }
 
     public void stop() throws IOException {
-        Objects.requireNonNull(service);
+        Objects.requireNonNull(service, NOT_RUNNING);
         service.stop();
         service = null;
         try {
@@ -83,16 +85,22 @@ public class RedisServer {
         } catch (InterruptedException e) {
             System.err.println("Jedis-mock interrupted while stopping");
             Thread.currentThread().interrupt();
+        } finally {
+            threadPool.shutdownNow();
         }
     }
 
+    public boolean isRunning() {
+        return service != null;
+    }
+
     public String getHost() {
-        Objects.requireNonNull(service);
+        Objects.requireNonNull(service, NOT_RUNNING);
         return service.getServer().getInetAddress().getHostAddress();
     }
 
     public int getBindPort() {
-        Objects.requireNonNull(service);
+        Objects.requireNonNull(service, NOT_RUNNING);
         return service.getServer().getLocalPort();
     }
 }


Test patch:
diff --git a/src/test/java/com/github/fppt/jedismock/TestStartStop.java b/src/test/java/com/github/fppt/jedismock/TestStartStop.java
new file mode 100644
index 0000000..e0a9d12
--- /dev/null
+++ b/src/test/java/com/github/fppt/jedismock/TestStartStop.java
@@ -0,0 +1,73 @@
+package com.github.fppt.jedismock;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import redis.clients.jedis.Jedis;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class TestStartStop {
+    @Test
+    void testMultipleServerReuse() throws IOException {
+        Set<String> oldThreads = threadNames();
+        RedisServer server = RedisServer.newRedisServer();
+        for (int i = 0; i < 2000; i++) {
+            startCheckStop(server);
+        }
+        Set<String> newThreads = threadNames();
+        newThreads.removeAll(oldThreads);
+        Assertions.assertThat(newThreads).hasSizeLessThan(10);
+    }
+
+    @Test
+    void testMultipleServerCreation() throws IOException {
+        Set<String> oldThreads = threadNames();
+        for (int i = 0; i < 2000; i++) {
+            RedisServer server = RedisServer.newRedisServer();
+            startCheckStop(server);
+        }
+        Set<String> newThreads = threadNames();
+        newThreads.removeAll(oldThreads);
+        Assertions.assertThat(newThreads).hasSizeLessThan(10);
+    }
+
+    @Test
+    void testStartAlreadyStarted() throws IOException {
+        RedisServer server = RedisServer.newRedisServer();
+        server.start();
+        try {
+            Assertions.assertThatThrownBy(server::start).isInstanceOf(IllegalStateException.class);
+        } finally {
+            server.stop();
+        }
+    }
+
+    @Test
+    void testStopNotStarted() {
+        RedisServer server = RedisServer.newRedisServer();
+        Assertions.assertThatThrownBy(server::stop).hasMessageContaining("is not running");
+    }
+
+    private static Set<String> threadNames() {
+        return Thread.getAllStackTraces()
+                .keySet()
+                .stream()
+                .map(Thread::getName)
+                .collect(Collectors.toCollection(HashSet::new));
+    }
+
+    private static void startCheckStop(RedisServer server) throws IOException {
+        Assertions.assertThat(server.isRunning()).isFalse();
+        server.start();
+        Assertions.assertThat(server.isRunning()).isTrue();
+        try (Jedis jedis = new Jedis(server.getHost(), server.getBindPort())) {
+            Assertions.assertThat(jedis.ping()).isEqualTo("PONG");
+        }
+        server.stop();
+        Assertions.assertThat(server.isRunning()).isFalse();
+    }
+
+}