-
Notifications
You must be signed in to change notification settings - Fork 0
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
Add UDS read/write error handling and tests with glide core mock. #76
Changes from all commits
59b379f
edfc45e
28b5f8a
f3abdfd
5b70748
d08bec0
4cdfd33
f4699ad
e717c5e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ | ||
package glide.connection; | ||
|
||
import static java.util.concurrent.TimeUnit.SECONDS; | ||
import static org.junit.jupiter.api.Assertions.assertAll; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertFalse; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import connection_request.ConnectionRequestOuterClass.ConnectionRequest; | ||
import connection_request.ConnectionRequestOuterClass.NodeAddress; | ||
import glide.api.RedisClient; | ||
import glide.api.models.exceptions.ClosingException; | ||
import glide.connectors.handlers.CallbackDispatcher; | ||
import glide.connectors.handlers.ChannelHandler; | ||
import glide.connectors.resources.Platform; | ||
import glide.managers.CommandManager; | ||
import glide.managers.ConnectionManager; | ||
import glide.utils.RustCoreLibMockTestBase; | ||
import glide.utils.RustCoreMock; | ||
import java.util.concurrent.ExecutionException; | ||
import java.util.concurrent.Future; | ||
import java.util.concurrent.TimeoutException; | ||
import lombok.SneakyThrows; | ||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import redis_request.RedisRequestOuterClass.RedisRequest; | ||
import response.ResponseOuterClass.Response; | ||
|
||
public class ConnectionWithGlideMockTests extends RustCoreLibMockTestBase { | ||
|
||
private ChannelHandler channelHandler = null; | ||
|
||
@BeforeEach | ||
@SneakyThrows | ||
public void createTestClient() { | ||
channelHandler = | ||
new ChannelHandler( | ||
new CallbackDispatcher(), socketPath, Platform.getThreadPoolResourceSupplier().get()); | ||
} | ||
|
||
@AfterEach | ||
public void closeTestClient() { | ||
channelHandler.close(); | ||
} | ||
|
||
private Future<Response> testConnection() { | ||
return channelHandler.connect(createConnectionRequest()); | ||
} | ||
|
||
private static ConnectionRequest createConnectionRequest() { | ||
return ConnectionRequest.newBuilder() | ||
.addAddresses(NodeAddress.newBuilder().setHost("dummyhost").setPort(42).build()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add a comment about why fake values are used. |
||
.build(); | ||
} | ||
|
||
@BeforeAll | ||
public static void init() { | ||
startRustCoreLibMock(null); | ||
} | ||
|
||
@Test | ||
@SneakyThrows | ||
// as of #710 https://github.com/aws/babushka/pull/710 - connection response is empty | ||
public void can_connect_with_empty_response() { | ||
RustCoreMock.updateGlideMock( | ||
new RustCoreMock.GlideMockProtobuf() { | ||
@Override | ||
public Response connection(ConnectionRequest request) { | ||
return Response.newBuilder().build(); | ||
} | ||
|
||
@Override | ||
public Response.Builder redisRequest(RedisRequest request) { | ||
return null; | ||
} | ||
}); | ||
|
||
var connectionResponse = testConnection().get(); | ||
assertAll( | ||
() -> assertFalse(connectionResponse.hasClosingError()), | ||
() -> assertFalse(connectionResponse.hasRequestError()), | ||
() -> assertFalse(connectionResponse.hasRespPointer())); | ||
} | ||
|
||
@Test | ||
@SneakyThrows | ||
public void can_connect_with_ok_response() { | ||
RustCoreMock.updateGlideMock( | ||
new RustCoreMock.GlideMockProtobuf() { | ||
@Override | ||
public Response connection(ConnectionRequest request) { | ||
return OK().build(); | ||
} | ||
|
||
@Override | ||
public Response.Builder redisRequest(RedisRequest request) { | ||
return null; | ||
} | ||
}); | ||
|
||
var connectionResponse = testConnection().get(); | ||
assertAll( | ||
() -> assertTrue(connectionResponse.hasConstantResponse()), | ||
() -> assertFalse(connectionResponse.hasClosingError()), | ||
() -> assertFalse(connectionResponse.hasRequestError()), | ||
() -> assertFalse(connectionResponse.hasRespPointer())); | ||
} | ||
|
||
@Test | ||
public void cant_connect_when_no_response() { | ||
RustCoreMock.updateGlideMock( | ||
new RustCoreMock.GlideMockProtobuf() { | ||
@Override | ||
public Response connection(ConnectionRequest request) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public Response.Builder redisRequest(RedisRequest request) { | ||
return null; | ||
} | ||
}); | ||
|
||
assertThrows(TimeoutException.class, () -> testConnection().get(1, SECONDS)); | ||
} | ||
|
||
@Test | ||
@SneakyThrows | ||
public void cant_connect_when_negative_response() { | ||
RustCoreMock.updateGlideMock( | ||
new RustCoreMock.GlideMockProtobuf() { | ||
@Override | ||
public Response connection(ConnectionRequest request) { | ||
return Response.newBuilder().setClosingError("You shall not pass!").build(); | ||
} | ||
|
||
@Override | ||
public Response.Builder redisRequest(RedisRequest request) { | ||
return null; | ||
} | ||
}); | ||
|
||
var exception = assertThrows(ExecutionException.class, () -> testConnection().get(1, SECONDS)); | ||
assertAll( | ||
() -> assertTrue(exception.getCause() instanceof ClosingException), | ||
() -> assertEquals("You shall not pass!", exception.getCause().getMessage())); | ||
} | ||
|
||
@Test | ||
@SneakyThrows | ||
public void rethrow_error_on_read_when_malformed_packet_received() { | ||
RustCoreMock.updateGlideMock(request -> new byte[] {-1}); | ||
|
||
var exception = assertThrows(ExecutionException.class, () -> testConnection().get(1, SECONDS)); | ||
assertAll( | ||
() -> assertTrue(exception.getCause() instanceof ClosingException), | ||
() -> | ||
assertTrue( | ||
exception | ||
.getCause() | ||
.getMessage() | ||
.contains("An unhandled error while reading from UDS channel"))); | ||
} | ||
|
||
@Test | ||
@SneakyThrows | ||
public void rethrow_error_if_UDS_channel_closed() { | ||
var client = new TestClient(channelHandler); | ||
stopRustCoreLibMock(); | ||
try { | ||
var exception = | ||
assertThrows( | ||
ExecutionException.class, () -> client.customCommand(new String[0]).get(1, SECONDS)); | ||
assertTrue(exception.getCause() instanceof RuntimeException); | ||
|
||
// Not a public class, can't import | ||
assertEquals( | ||
"io.netty.channel.StacklessClosedChannelException", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems brittle if Netty's version changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hopefully they make it public when they update? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hope it won't happen without CI verification to catch failures. |
||
exception.getCause().getCause().getClass().getName()); | ||
} finally { | ||
// restart mock to let other tests pass if this one failed | ||
startRustCoreLibMock(null); | ||
} | ||
} | ||
|
||
private static class TestClient extends RedisClient { | ||
|
||
public TestClient(ChannelHandler channelHandler) { | ||
super(new ConnectionManager(channelHandler), new CommandManager(channelHandler)); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ | ||
package glide.utils; | ||
|
||
import glide.connectors.handlers.ChannelHandler; | ||
import glide.ffi.resolvers.SocketListenerResolver; | ||
import lombok.SneakyThrows; | ||
import org.junit.jupiter.api.AfterAll; | ||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.BeforeEach; | ||
|
||
public class RustCoreLibMockTestBase { | ||
|
||
/** | ||
* Pass this socket path to {@link ChannelHandler} or mock {@link | ||
* SocketListenerResolver#getSocket()} to return it. | ||
*/ | ||
protected static String socketPath = null; | ||
|
||
@SneakyThrows | ||
public static void startRustCoreLibMock(RustCoreMock.GlideMock rustCoreLibMock) { | ||
assert socketPath == null | ||
: "Previous `RustCoreMock` wasn't stopped. Ensure that your test class inherits" | ||
+ " `RustCoreLibMockTestBase`."; | ||
|
||
socketPath = RustCoreMock.start(rustCoreLibMock); | ||
} | ||
|
||
@BeforeEach | ||
public void preTestCheck() { | ||
assert socketPath != null | ||
: "You missed to call `startRustCoreLibMock` in a `@BeforeAll` method of your test class" | ||
+ " inherited from `RustCoreLibMockTestBase`."; | ||
} | ||
|
||
@AfterEach | ||
public void afterTestCheck() { | ||
assert !RustCoreMock.failed() : "Error occurred in `RustCoreMock`"; | ||
} | ||
|
||
@AfterAll | ||
@SneakyThrows | ||
public static void stopRustCoreLibMock() { | ||
assert socketPath != null | ||
: "You missed to call `startRustCoreLibMock` in a `@AfterAll` method of your test class" | ||
+ " inherited from `RustCoreLibMockTestBase`."; | ||
RustCoreMock.stop(); | ||
socketPath = null; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add tests for InterruptedExceptions on the thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should I do to get one? Kill a thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes?