Skip to content

Commit 10cfa8d

Browse files
committed
Add Redis server, pub/sub, and multi-client test utilities
Introduce comprehensive unit tests for Redis interactions, including embedded server setup, pub/sub messaging, and multi-client operations. Update dependencies to support testing and logging improvements, and enhance `ThreadUtils` with better thread management capabilities.
1 parent 3ce4a02 commit 10cfa8d

6 files changed

Lines changed: 345 additions & 7 deletions

File tree

build.gradle

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ repositories {
2121
}
2222

2323
dependencies {
24-
githubImplementation "intisy:simple-logger:2.1.3.2"
24+
githubImplementation "intisy:simple-logger:2.1.5"
2525
implementation "org.apache.httpcomponents:httpclient:4.5.14"
2626
implementation "org.kohsuke:github-api:1.99"
2727
implementation "com.google.code.gson:gson:2.11.0"
2828
implementation "org.eclipse.jgit:org.eclipse.jgit:5.13.3.202401111512-r"
2929
implementation "org.kohsuke:github-api:1.324"
3030
implementation "com.github.codemonstur:embedded-redis:1.4.3"
31+
implementation "org.slf4j:slf4j-api:1.7.36"
32+
implementation "org.slf4j:slf4j-simple:1.7.36"
33+
testImplementation "junit:junit:4.12"
3134
}

src/main/java/io/github/intisy/utils/utils/ThreadUtils.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@ public static void sleep(int milliseconds) {
1515
}
1616
}
1717

18-
public static void newThread(Runnable runnable) {
19-
new Thread(runnable).start();
18+
public static Thread newThread(Runnable runnable) {
19+
return newThread(runnable, null);
2020
}
21-
public static void newThread(Runnable runnable, String name) {
22-
new Thread(runnable, name).start();
21+
public static Thread newThread(Runnable runnable, String name) {
22+
return newThread(runnable, name, false);
2323
}
24-
public static void newThread(Runnable runnable, String name, boolean daemon) {
24+
public static Thread newThread(Runnable runnable, String name, boolean daemon) {
2525
Thread thread = new Thread(runnable, name);
2626
thread.setDaemon(daemon);
2727
thread.start();
28+
return thread;
2829
}
2930

3031
public static String getThreadName() {

src/test/java/io/github/intisy/utils/custom/external/TestRedis.java renamed to src/test/java/io/github/intisy/utils/custom/external/RedisConnectionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* @author Finn Birich
99
*/
10-
public class TestRedis {
10+
public class RedisConnectionTest {
1111
public static void main(String[] args) {
1212
System.out.println("Redis Client Event System Demonstration");
1313
System.out.println("======================================\n");
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package io.github.intisy.utils.custom.external;
2+
3+
import io.github.intisy.simple.logger.SimpleLogger; // Assuming you have a logger implementation
4+
import io.github.intisy.utils.custom.external.Redis;
5+
6+
import java.io.IOException;
7+
8+
public class RedisMultiClientTest {
9+
10+
public static void main(String[] args) {
11+
SimpleLogger logger = new SimpleLogger(); // Use your actual logger
12+
Redis serverInstance = null;
13+
Redis client1 = null;
14+
Redis client2 = null;
15+
int actualPort = -1;
16+
17+
try {
18+
// 1. Start an embedded Redis server instance
19+
// We use one instance to manage the embedded server lifecycle.
20+
// Enable allowPortSearch to find a free port if the default (6379) is busy.
21+
logger.info("Attempting to start embedded Redis server...");
22+
serverInstance = new Redis("localhost", 6379, true, true, false); // host, port, useEmbedded, allowPortSearch, allowMockFallback
23+
serverInstance.setLogger(logger);
24+
serverInstance.connect(); // This will start the embedded server
25+
26+
if (!serverInstance.isConnected() || !serverInstance.ping()) {
27+
throw new RuntimeException("Failed to start or connect to the embedded Redis server.");
28+
}
29+
actualPort = serverInstance.getPort(); // Get the actual port used (might differ if port search was needed)
30+
logger.success("Embedded Redis server started successfully on port: " + actualPort);
31+
32+
// 2. Create two client instances connecting to the embedded server
33+
// Note: useEmbedded is false for these clients as they connect externally.
34+
logger.info("Creating Client 1...");
35+
client1 = new Redis(serverInstance.getHost(), actualPort, false, false, false);
36+
client1.setLogger(logger);
37+
client1.connect();
38+
if (!client1.isConnected() || !client1.ping()) {
39+
throw new RuntimeException("Client 1 failed to connect.");
40+
}
41+
logger.success("Client 1 connected.");
42+
43+
44+
logger.info("Creating Client 2...");
45+
client2 = new Redis(serverInstance.getHost(), actualPort, false, false, false);
46+
client2.setLogger(logger);
47+
client2.connect();
48+
if (!client2.isConnected() || !client2.ping()) {
49+
throw new RuntimeException("Client 2 failed to connect.");
50+
}
51+
logger.success("Client 2 connected.");
52+
53+
// 3. Test Interaction: Client 1 sets data, Client 2 gets data
54+
String testKey = "multiClientTestKey";
55+
String testValue = "Hello from Client 1!";
56+
57+
logger.info("Client 1 setting data: Key='" + testKey + "', Value='" + testValue + "'");
58+
client1.setData(testKey, testValue);
59+
60+
logger.info("Client 2 getting data for Key='" + testKey + "'");
61+
String retrievedValue = client2.getData(testKey);
62+
63+
if (testValue.equals(retrievedValue)) {
64+
logger.success("Success! Client 2 retrieved the value set by Client 1: '" + retrievedValue + "'");
65+
} else {
66+
logger.error("Failure! Client 2 retrieved unexpected value: '" + retrievedValue + "'");
67+
}
68+
69+
// 4. (Optional) Check client list
70+
// Note: Parsing this string can be brittle. This just prints it.
71+
logger.info("Checking client list (via Client 1):");
72+
String clientList = client1.getClientList();
73+
logger.info(clientList);
74+
// You could add logic here to check if the list contains >1 client entry.
75+
76+
} catch (IOException e) {
77+
logger.error("IOException during Redis operations", e);
78+
} catch (RuntimeException e) {
79+
logger.error("Runtime exception during test", e);
80+
} finally {
81+
// 5. Clean up: Disconnect clients and stop the server
82+
logger.info("Cleaning up resources...");
83+
if (client1 != null && client1.isConnected()) {
84+
client1.disconnect();
85+
logger.info("Client 1 disconnected.");
86+
}
87+
if (client2 != null && client2.isConnected()) {
88+
client2.disconnect();
89+
logger.info("Client 2 disconnected.");
90+
}
91+
if (serverInstance != null && serverInstance.isConnected()) {
92+
// Disconnecting the instance that manages the embedded server will stop it.
93+
serverInstance.disconnect();
94+
logger.info("Embedded Redis server instance stopped.");
95+
}
96+
}
97+
}
98+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package io.github.intisy.utils.custom.external;
2+
3+
import io.github.intisy.simple.logger.SimpleLogger; // Assuming you have a logger implementation
4+
import io.github.intisy.utils.custom.external.Redis;
5+
import io.github.intisy.utils.utils.ThreadUtils; // Using your ThreadUtils for convenience
6+
7+
import java.io.IOException;
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.TimeUnit;
10+
import java.util.concurrent.atomic.AtomicInteger;
11+
12+
public class RedisPubSubTest {
13+
14+
private static final String TEST_CHANNEL = "test-notifications";
15+
16+
public static void main(String[] args) {
17+
SimpleLogger logger = new SimpleLogger(); // Use your actual logger
18+
Redis serverClient = null; // This instance will manage the embedded server AND publish
19+
Redis listenerClient = null; // This instance will subscribe and listen
20+
21+
// Use CountDownLatch to wait for messages in the test
22+
final int expectedMessages = 3;
23+
CountDownLatch messageLatch = new CountDownLatch(expectedMessages);
24+
AtomicInteger receivedMessages = new AtomicInteger(0);
25+
26+
27+
// Define the listener logic
28+
Redis.MessageListener messageListener = (channel, message) -> {
29+
logger.info("[Listener] Received message on channel '" + channel + "': " + message);
30+
receivedMessages.incrementAndGet();
31+
messageLatch.countDown(); // Signal that a message was received
32+
};
33+
34+
Thread listenerThread = null;
35+
36+
try {
37+
// 1. Start the embedded server using serverClient
38+
logger.info("Creating server+client instance and starting embedded Redis...");
39+
serverClient = new Redis("localhost", 6379, true, true, false);
40+
serverClient.setLogger(logger);
41+
serverClient.connect();
42+
43+
if (!serverClient.isConnected() || !serverClient.ping()) {
44+
throw new RuntimeException("Failed to start or connect the embedded Redis server instance.");
45+
}
46+
logger.success("Embedded Redis server started by serverClient on host: " + serverClient.getHost() + ", port: " + serverClient.getPort());
47+
48+
// 2. Create and connect the listener client
49+
logger.info("Creating Listener client...");
50+
listenerClient = new Redis(serverClient.getHost(), serverClient.getPort(), false, false, false);
51+
listenerClient.setLogger(logger);
52+
listenerClient.connect();
53+
54+
if (!listenerClient.isConnected() || !listenerClient.ping()) {
55+
throw new RuntimeException("Listener client failed to connect.");
56+
}
57+
logger.success("Listener client connected successfully.");
58+
59+
// 3. Start subscribing in a separate thread
60+
logger.info("Starting listener thread to subscribe to channel: " + TEST_CHANNEL);
61+
final Redis finalListenerClient = listenerClient; // Need final variable for lambda
62+
listenerThread = ThreadUtils.newThread(() -> {
63+
try {
64+
// This call will likely block until the listenerClient is disconnected
65+
finalListenerClient.subscribe(TEST_CHANNEL, messageListener);
66+
logger.info("[Listener Thread] Subscription ended.");
67+
} catch (Exception e) {
68+
// Catch redis.clients.jedis.exceptions.JedisConnectionException if disconnected externally
69+
if (e.getCause() instanceof java.net.SocketException && e.getCause().getMessage().contains("Socket closed")) {
70+
logger.warn("[Listener Thread] Subscription interrupted by disconnection, which is expected on shutdown.");
71+
} else {
72+
logger.error("[Listener Thread] Error during subscription", e);
73+
}
74+
}
75+
}, "redis-listener-thread");
76+
77+
// Give the listener thread a moment to establish the subscription
78+
logger.info("Waiting briefly for listener to initialize...");
79+
ThreadUtils.sleep(1000); // Allow 1 second for subscription setup
80+
81+
// 4. Publish messages from the serverClient
82+
logger.info("ServerClient publishing messages to channel: " + TEST_CHANNEL);
83+
for (int i = 1; i <= expectedMessages; i++) {
84+
String message = "Message " + i + " from ServerClient";
85+
logger.info("[Publisher] Sending: " + message);
86+
serverClient.publish(TEST_CHANNEL, message);
87+
ThreadUtils.sleep(200); // Small delay between messages
88+
}
89+
90+
// 5. Wait for messages to be received or timeout
91+
logger.info("Waiting for listener to receive " + expectedMessages + " messages...");
92+
boolean messagesReceived = messageLatch.await(5, TimeUnit.SECONDS); // Wait up to 5 seconds
93+
94+
if (messagesReceived) {
95+
logger.success("Success! Listener received all " + expectedMessages + " expected messages.");
96+
} else {
97+
logger.error("Failure! Listener only received " + receivedMessages.get() + " out of " + expectedMessages + " messages within the timeout.");
98+
}
99+
100+
101+
} catch (IOException e) {
102+
logger.error("IOException during Redis operations", e);
103+
} catch (InterruptedException e) {
104+
logger.error("Main thread interrupted", e);
105+
Thread.currentThread().interrupt(); // Restore interrupted status
106+
} catch (RuntimeException e) {
107+
logger.error("Runtime exception during test", e);
108+
} finally {
109+
// 6. Clean up
110+
logger.info("Cleaning up resources...");
111+
112+
// Disconnect the listener first. This should interrupt the blocking subscribe() call.
113+
if (listenerClient != null && listenerClient.isConnected()) {
114+
listenerClient.disconnect();
115+
logger.info("Listener client disconnected.");
116+
}
117+
118+
// Wait for the listener thread to finish (optional but good practice)
119+
if (listenerThread != null) {
120+
try {
121+
logger.info("Waiting for listener thread to terminate...");
122+
listenerThread.join(2000); // Wait up to 2 seconds
123+
if (listenerThread.isAlive()) {
124+
logger.warn("Listener thread did not terminate gracefully.");
125+
} else {
126+
logger.info("Listener thread terminated.");
127+
}
128+
} catch (InterruptedException e) {
129+
logger.error("Interrupted while waiting for listener thread to join", e);
130+
Thread.currentThread().interrupt();
131+
}
132+
}
133+
134+
// Disconnect the serverClient (which also stops the embedded server)
135+
if (serverClient != null && serverClient.isConnected()) {
136+
serverClient.disconnect();
137+
logger.info("ServerClient disconnected and embedded server stopped.");
138+
}
139+
}
140+
}
141+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.github.intisy.utils.custom.external;
2+
3+
import io.github.intisy.simple.logger.SimpleLogger; // Assuming you have a logger implementation
4+
import io.github.intisy.utils.custom.external.Redis;
5+
6+
import java.io.IOException;
7+
8+
public class RedisServerClientTest {
9+
10+
public static void main(String[] args) {
11+
SimpleLogger logger = new SimpleLogger(); // Use your actual logger
12+
Redis serverClient = null; // This instance will manage the embedded server AND act as a client
13+
Redis client2 = null; // This instance will be a pure client
14+
15+
try {
16+
// 1. Create the first Redis instance to act as the server + client
17+
// useEmbedded=true tells it to start the embedded server on connect()
18+
// allowPortSearch=true allows it to find a free port if needed
19+
logger.info("Creating server+client instance and starting embedded Redis...");
20+
serverClient = new Redis("localhost", 6379, true, true, false); // host, port, useEmbedded=true, allowPortSearch=true, allowMockFallback=false
21+
serverClient.setLogger(logger);
22+
serverClient.connect(); // Starts the embedded server and connects this instance to it
23+
24+
if (!serverClient.isConnected() || !serverClient.ping()) {
25+
throw new RuntimeException("Failed to start or connect the embedded Redis server instance.");
26+
}
27+
logger.success("Embedded Redis server started by serverClient on host: " + serverClient.getHost() + ", port: " + serverClient.getPort());
28+
29+
// 2. Create the second client instance connecting to the first one's server
30+
// useEmbedded=false as it's just connecting externally
31+
// Use the actual host and port from the serverClient instance
32+
logger.info("Creating Client 2 to connect to serverClient's embedded server...");
33+
client2 = new Redis(serverClient.getHost(), serverClient.getPort(), false, false, false); // useEmbedded=false
34+
client2.setLogger(logger);
35+
client2.connect(); // Connects to the existing embedded server
36+
37+
if (!client2.isConnected() || !client2.ping()) {
38+
throw new RuntimeException("Client 2 failed to connect to the embedded server.");
39+
}
40+
logger.success("Client 2 connected successfully.");
41+
42+
// 3. Test Interaction - Bidirectional
43+
String key1 = "serverClientKey";
44+
String value1 = "Data from server+client";
45+
String key2 = "client2Key";
46+
String value2 = "Data from pure client";
47+
48+
// serverClient sets data, client2 retrieves
49+
logger.info("ServerClient setting data: Key='" + key1 + "'");
50+
serverClient.setData(key1, value1);
51+
logger.info("Client 2 getting data for Key='" + key1 + "'");
52+
String retrievedValue1 = client2.getData(key1);
53+
if (value1.equals(retrievedValue1)) {
54+
logger.success("Success! Client 2 retrieved value from ServerClient: '" + retrievedValue1 + "'");
55+
} else {
56+
logger.error("Failure! Client 2 retrieved unexpected value: '" + retrievedValue1 + "'");
57+
}
58+
59+
// client2 sets data, serverClient retrieves
60+
logger.info("Client 2 setting data: Key='" + key2 + "'");
61+
client2.setData(key2, value2);
62+
logger.info("ServerClient getting data for Key='" + key2 + "'");
63+
String retrievedValue2 = serverClient.getData(key2);
64+
if (value2.equals(retrievedValue2)) {
65+
logger.success("Success! ServerClient retrieved value from Client 2: '" + retrievedValue2 + "'");
66+
} else {
67+
logger.error("Failure! ServerClient retrieved unexpected value: '" + retrievedValue2 + "'");
68+
}
69+
70+
71+
// 4. (Optional) Check client list
72+
logger.info("Checking client list (via Client 2):");
73+
String clientList = client2.getClientList();
74+
logger.info(clientList);
75+
// Expecting at least two clients here (serverClient's internal connection + client2)
76+
77+
} catch (IOException e) {
78+
logger.error("IOException during Redis operations", e);
79+
} catch (RuntimeException e) {
80+
logger.error("Runtime exception during test", e);
81+
} finally {
82+
// 5. Clean up: Disconnect clients and stop the server
83+
logger.info("Cleaning up resources...");
84+
if (client2 != null && client2.isConnected()) {
85+
client2.disconnect();
86+
logger.info("Client 2 disconnected.");
87+
}
88+
if (serverClient != null && serverClient.isConnected()) {
89+
// Disconnecting the instance that manages the embedded server will stop it.
90+
serverClient.disconnect();
91+
logger.info("ServerClient disconnected and embedded server stopped.");
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)