Skip to content
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

Merged

Conversation

Yury-Fridlyand
Copy link
Collaborator

  1. Move error handling to CallbackDispatcher
  2. Handle case of incorrect callback ID with ClosingError (ref: Rust core returns incorrect callback ID on connection failure #851)
  3. Close channel on ClosingError
  4. Tests

* 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]>
@Yury-Fridlyand Yury-Fridlyand added the java issues and fixes related to the java client label Jan 27, 2024
@Yury-Fridlyand Yury-Fridlyand requested a review from a team as a code owner January 27, 2024 00:59
@@ -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() {
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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
Copy link
Contributor

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.

Copy link
Collaborator Author

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the change?

Copy link
Collaborator Author

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(
Copy link
Contributor

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.

Copy link
Collaborator Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this changed?

Copy link
Collaborator Author

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,
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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",
Copy link
Collaborator

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?

Suggested change
"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",

Copy link
Collaborator Author

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// a ClosingException thrown from CallbackDispatcher::completeRequest and then
// a RedisException thrown from CallbackDispatcher::completeRequest and then

Copy link
Collaborator Author

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(
Copy link
Collaborator Author

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",
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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,
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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() {
Copy link
Collaborator Author

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() {
Copy link
Collaborator Author

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() {
Copy link
Collaborator Author

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
Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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
Copy link
Contributor

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.

Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Collaborator Author

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() {
Copy link
Contributor

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

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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);
Copy link
Contributor

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() {
Copy link
Contributor

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

Copy link
Contributor

@acarbonetto acarbonetto Jan 31, 2024

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

Copy link
Collaborator Author

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Contributor

@acarbonetto acarbonetto Jan 31, 2024

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

Copy link
Collaborator Author

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

@Yury-Fridlyand Yury-Fridlyand merged commit 888109e into valkey-io:main Feb 1, 2024
11 checks passed
@Yury-Fridlyand Yury-Fridlyand deleted the java/integ_yuryf_closing_error branch February 1, 2024 01:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
java issues and fixes related to the java client
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants