-
Notifications
You must be signed in to change notification settings - Fork 70
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
Java client: update error handling #865
Java client: update error handling #865
Conversation
* Add exception handling and tests. Signed-off-by: Yury-Fridlyand <[email protected]> * Update tests. Signed-off-by: Yury-Fridlyand <[email protected]> * spotless Signed-off-by: Yury-Fridlyand <[email protected]> * typo Signed-off-by: Yury-Fridlyand <[email protected]> * Address PR review comments. Signed-off-by: Yury-Fridlyand <[email protected]> --------- Signed-off-by: Yury-Fridlyand <[email protected]>
@@ -63,18 +64,17 @@ public void setUp() throws ExecutionException, InterruptedException { | |||
|
|||
@SneakyThrows | |||
@Test | |||
public void ConnectionRequestProtobufGeneration_DefaultRedisClientConfiguration_returns() { | |||
public void connection_request_protobuf_generation_default_standalone_configuration() { |
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.
why are you modifying the casing scheme of the tests, and will this affect other PRs? will the new scheme be enforced with a linter?
If it is, please spin large stylistic changes to another PR. If it isn't, why bother?
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.
why are you modifying the casing scheme
https://github.com/aws/glide-for-redis/blob/e39f4ae12d1583a1d62d6cbce4aba5966ff270b8/java/client/src/test/resources/junit-platform.properties#L2
This thing replaces underscores by spaces, which makes test report more readable. There is no similar feature for CaMel casing.
will the new scheme be enforced with a linter?
no, both function names are valid (at least for a test cases)
spin large stylistic changes to another PR
We have only couple of files with UT, so all changes are here. I still open to move it another PR.
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.
Reverted in 794833e
@@ -41,11 +41,11 @@ public ChannelHandler(CallbackDispatcher callbackDispatcher, String socketPath) | |||
/** | |||
* Open a new channel for a new client and running it on the provided EventLoopGroup | |||
* | |||
* @param eventLoopGroup - ELG to run handler on |
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.
if you have multiple lines of pure style changes, please spin them out to a separate PR.
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.
Reverted in 794833e
import response.ResponseOuterClass.Response; | ||
|
||
/** Holder for resources required to dispatch responses and used by {@link ReadHandler}. */ | ||
@RequiredArgsConstructor | ||
public class CallbackDispatcher { | ||
|
||
/** Unique request ID (callback ID). Thread-safe and overflow-safe. */ | ||
private final AtomicInteger nextAvailableRequestId = new AtomicInteger(0); | ||
protected final AtomicInteger nextAvailableRequestId = new AtomicInteger(0); |
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.
why the change?
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.
We create TestCallbackDispatcher
(which inherits CallbackDispatcher
) for tests. Access to protected fields is required to verify correctness of the data processing.
This is true for other classes too where access modifier was changed.
future.completeAsync(() -> response); | ||
} else { | ||
// TODO: log an error. | ||
// probably a response was received after shutdown or `registerRequest` call was missing | ||
|
||
System.err.printf( |
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.
if this happens, the client is in an erroneous state and should close.
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.
Added in 794833e
@@ -24,8 +24,8 @@ public class ChannelHandler { | |||
|
|||
private static final String THREAD_POOL_NAME = "glide-channel"; | |||
|
|||
private final Channel channel; | |||
private final CallbackDispatcher callbackDispatcher; | |||
protected final Channel channel; |
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.
why was this changed?
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.
For tests, ref #865 (comment)
|
||
var protobufErrorsToJavaClientErrors = | ||
Map.of( | ||
Unspecified, RequestException.class, |
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.
this list is nice, but what mechanism ensures that if there's a new exception type the list will update? Can't we iterate over the types using reflection?
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.
We can iterate with reflection, but mapping is still hardcoded. I'll try to make mapping as a class property to avoid duplicating it in tests.
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.
Reflection is ugly and requires additional error handling. For nice mapping we should update the enum, but it is managed by protobuf.
I keep this unchanged.
future.completeAsync(() -> response); | ||
} else { | ||
// TODO: log an error. | ||
// probably a response was received after shutdown or `registerRequest` call was missing | ||
|
||
System.err.printf( | ||
"Received a response for not registered callback id %d, request error = %s%n", |
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.
Is this supposed to be \n instead of %n?
"Received a response for not registered callback id %d, request error = %s%n", | |
"Received a response for not registered callback id %d, request error = %s\n", |
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.
Not a mistake - %n
is rendered as \n
. Thank you for proofreading though.
callbackDispatcher.completeRequest(response); | ||
|
||
var exception = assertThrows(ExecutionException.class, future::get); | ||
// a ClosingException thrown from CallbackDispatcher::completeRequest and then |
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.
// a ClosingException thrown from CallbackDispatcher::completeRequest and then | |
// a RedisException thrown from CallbackDispatcher::completeRequest and then |
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.
Thanks fixed in 794833e
Signed-off-by: Yury-Fridlyand <[email protected]>
future.completeAsync(() -> response); | ||
} else { | ||
// TODO: log an error. | ||
// probably a response was received after shutdown or `registerRequest` call was missing | ||
|
||
System.err.printf( |
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.
Added in 794833e
future.completeAsync(() -> response); | ||
} else { | ||
// TODO: log an error. | ||
// probably a response was received after shutdown or `registerRequest` call was missing | ||
|
||
System.err.printf( | ||
"Received a response for not registered callback id %d, request error = %s%n", |
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.
Not a mistake - %n
is rendered as \n
. Thank you for proofreading though.
@@ -41,11 +41,11 @@ public ChannelHandler(CallbackDispatcher callbackDispatcher, String socketPath) | |||
/** | |||
* Open a new channel for a new client and running it on the provided EventLoopGroup | |||
* | |||
* @param eventLoopGroup - ELG to run handler on |
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.
Reverted in 794833e
|
||
var protobufErrorsToJavaClientErrors = | ||
Map.of( | ||
Unspecified, RequestException.class, |
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.
Reflection is ugly and requires additional error handling. For nice mapping we should update the enum, but it is managed by protobuf.
I keep this unchanged.
callbackDispatcher.completeRequest(response); | ||
|
||
var exception = assertThrows(ExecutionException.class, future::get); | ||
// a ClosingException thrown from CallbackDispatcher::completeRequest and then |
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.
Thanks fixed in 794833e
@@ -71,7 +71,7 @@ public void createClient_withConfig_successfullyReturnsRedisClient() { | |||
|
|||
@SneakyThrows | |||
@Test | |||
public void createClient_errorOnConnectionThrowsExecutionException() { | |||
public void createClient_error_on_connection_throws_ExecutionException() { |
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.
Reverted in 794833e
public void submitNewCommand_returnObjectResult() | ||
throws ExecutionException, InterruptedException { | ||
@SneakyThrows | ||
public void submitNewCommand_return_Object_result() { |
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.
Reverted in 794833e
@@ -63,18 +64,17 @@ public void setUp() throws ExecutionException, InterruptedException { | |||
|
|||
@SneakyThrows | |||
@Test | |||
public void ConnectionRequestProtobufGeneration_DefaultRedisClientConfiguration_returns() { | |||
public void connection_request_protobuf_generation_default_standalone_configuration() { |
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.
Reverted in 794833e
Signed-off-by: Yury-Fridlyand <[email protected]>
// According to https://github.com/aws/glide-for-redis/issues/851 | ||
// a response with a closing error may arrive with any/random callback ID (usually -1) | ||
// CommandManager and ConnectionManager would close the UDS channel on ClosingException | ||
responses |
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.
minor: consider wrapping this repeated code in a function, and in that function also close the channel.
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.
or maybe I'm missing where the channel is closed?
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.
nvm, saw that closing the channel happens in the exception handler.
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.
I tried to make all error handling in one place, including channel closing, but CallbackDispatcher
does not (and can not) have refenrence to the channel.
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.
Updated in 0dbec7b. Will use new function to handle read error too in the next PR.
System.err.printf( | ||
"Received a response for not registered callback id %d, request error = %s%n", | ||
callbackId, response.getRequestError()); | ||
responses |
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.
you cancel all of the responses, but you should also close the channel.
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.
nvm, saw that it's happening in the exception handler.
@Test | ||
public void verify_future_pipeline_abortion() { | ||
var future = new CompletableFuture<Boolean>(); | ||
var future2 = future.exceptionally(e -> false).thenApplyAsync(r -> r ? 42 : 100500); |
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 are you testing here? that the Future.exceptionally
function works correctly?
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, I show the exception handling pipeline in one place. In client it is distributed over multiple classes.
|
||
@Test | ||
@SneakyThrows | ||
public void callback_dispatcher_handles_response_without_error_but_with_incorrect_callback_id() { |
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.
"handle" is an anti-pattern in test names, because it makes it hard to understand whether the test is correct or not. It's preferable that the test should say what happens -
callback_dispatcher_closes_connection_on_response_without_error_but_with_incorrect_callback_id
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.
Renamed in 0dbec7b + added a comment
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.
The fully correct description is too long, because callback dispatcher does not close the channel, it forces connection manager and command manager to do that.
var exception = assertThrows(ExecutionException.class, future1::get); | ||
// a ClosingException thrown from CallbackDispatcher::completeRequest and then | ||
// rethrown by CommandManager::exceptionHandler | ||
assertTrue(exception.getCause() instanceof ClosingException); |
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.
you might want to offload the checks for closed connections to a function, instead of repeating the checks in all closing tests.
|
||
@Test | ||
@SneakyThrows | ||
public void callback_dispatcher_handles_response_with_closing_error() { |
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.
same - instead of "handle", say what's expected to happen
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.
callback_dispatcher_closes_channel_and_throws_ClosingException
or
callback_dispatcher_with_ClosingError_closes_channel_and_throws_ClosingException
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.
Renamed in 0dbec7b + added a comment
@ParameterizedTest(name = "{0}") | ||
@MethodSource("getProtobufErrorsToJavaClientErrorsMapping") | ||
@SneakyThrows | ||
public void callback_dispatcher_handles_response_with_request_error( |
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.
same
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.
callback_dispatcher_does_not_close_channel_and_throws_RedisException
or
callback_dispatcher_with_RequestError_does_not_close_channel_and_throws_RedisException
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.
Renamed in 0dbec7b + added a comment
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
Signed-off-by: Yury-Fridlyand <[email protected]>
…osing_error Signed-off-by: Yury-Fridlyand <[email protected]>
CallbackDispatcher
ClosingError
(ref: Rust core returns incorrect callback ID on connection failure #851)ClosingError