How to properly deal with exceptions coming from L

2019-02-26 23:39发布

问题:

I have a library in which I have provided two methods, sync and async for our customer. They can call whichever method they feel is right for their purpose.

  • executeSynchronous() - waits until I have a result, returns the result.
  • executeAsynchronous() - returns a Future immediately which can be processed after other things are done, if needed.

They will pass DataKey object which has the user id in it. And we will figure out which machine to call basis on the user id. So we will make http call to the url using AsyncRestTemplate and then send the response back to them basis on whether it is successful or not.

Below is my interface:

public interface Client {
    // for synchronous
    public DataResponse executeSync(final DataKey key);

    // for asynchronous
    public Future<DataResponse> executeAsync(final DataKey key);
}

And below is my implementation:

public class DataClient implements IClient {

    // does this have to be final?
    private final AsyncRestTemplate restTemplate = new AsyncRestTemplate();

    @Override
    public DataResponse executeSync(final DataKey keys) {
        Future<DataResponse> responseFuture = executeAsync(keys);
        DataResponse response = null;
        try {
            response = responseFuture.get(keys.getTimeout(), TimeUnit.Milliseconds);
        } catch (CancellationException e) {
            // what to do here?
        }  catch (InterruptedException e) {
            // is this right way to deal with InterruptedException?
            throw new RuntimeException("Interrupted", e);
        } catch (ExecutionException e) {
            // what do you mean by ExecutionException? And how should we deal with this?
            DataLogging.logErrors(e.getCause(), DataErrorEnum.ERROR_CLIENT, keys);
            response = new DataResponse(null, DataErrorEnum.ERROR_CLIENT, DataStatusEnum.ERROR);
        } catch (TimeoutException e) {
            DataLogging.logErrors(e.getCause(), DataErrorEnum.TIMEOUT_ON_CLIENT, keys);
            response = new DataResponse(null, DataErrorEnum.TIMEOUT_ON_CLIENT, DataStatusEnum.ERROR);       
        }

        return response;
    }

    @Override
    public Future<DataResponse> executeAsync(final DataKey keys) {
        final SettableFuture<DataResponse> responseFuture = SettableFuture.create();
        restTemplate.exchange(createURL(keys), HttpMethod.GET, keys.getEntity(), String.class).addCallback(
                new ListenableFutureCallback<ResponseEntity<String>>() {
                    @Override
                    public void onSuccess(ResponseEntity<String> result) {
                        responseFuture.set(new DataResponse(result.getBody(), DataErrorEnum.OK,
                                DataStatusEnum.SUCCESS));
                    }

                    @Override
                    public void onFailure(Throwable ex) {
                        DataLogging.logErrors(ex, DataErrorEnum.ERROR_SERVER, keys);
                        responseFuture.set(new DataResponse(null, DataErrorEnum.ERROR_CLIENT,
                                DataStatusEnum.ERROR));
                    }
                });

        return responseFuture;

    }
}

Now my question is:

  • How do I properly deal with exceptions in the catch block of executeSync? Is there any difference between CancellationException and TimeoutException? Also what we should do with ExecutionException in general?
  • Does my DataKey have to be final in my interface? If I remove final variable in my executeAsync implementation, then I get compilation error as Cannot refer to a non-final variable keys inside an inner class defined in a different method.
  • Is this the right way to use ListenableFutureCallback in my executeAsync method? Or is there any better way to use that?

Any inputs/suggestions are also welcome on my design for having sync and async implementations.

回答1:

I assume you're using Spring 4 (AsyncRestTemplate). In this case ListenableFuture that you get is not really Guava's ListenableFuture, but it's clone in Spring. Anyway you should deal with exceptions the same way as you deal with exceptions from the standard Future.

Answers to your questions:

// does this have to be final? private final AsyncRestTemplate
restTemplate = new AsyncRestTemplate();

It doesn't (in this case), but this is a good practice as in general it makes object less mutable simplifying reasoning about it's behaviour.

catch (CancellationException e) {
    // what to do here?
}

CancellationException will be thrown if task is cancelled (either via Future#cancel or ExecutorService#shutdownNow). It cannot happen in your case as only you have references to Future and (implicitly via private AsyncRestTemplate) ExecutorService used by execute queries. So

throw new AssertionError("executeAsync task couldn't be cancelled", e);

Is there any difference between CancellationException and TimeoutException?

In Future#get call you've specified timeout. TimeoutException will be thrown if result is still not available after keys.getTimeout() milliseconds.

catch (InterruptedException e) {
   // is this right way to deal with InterruptedException?
   throw new RuntimeException("Interrupted", e);
}

In this case no. InterruptedException will be thrown when client's thread is interrupted. You don't own that thread so you should propagate InterruptedException (i.e. declare executeSync(DataKey keys) throws InterruptedException). If for some reason you cannot change signature of the method then at least restore interrupted flag (Thread.currentThread().interrupt()) before throwing RuntimeException.

catch (ExecutionException e) {
   // what do you mean by ExecutionException? And how should we deal with this?
   DataLogging.logErrors(e.getCause(), DataErrorEnum.ERROR_CLIENT, keys);
   response = new DataResponse(null, DataErrorEnum.ERROR_CLIENT, DataStatusEnum.ERROR);
}

ExecutionException means that code submitted to ExecutorService as Callable/Runnable threw exception during execution. In your case ExecutionException will never be thrown because you return SettableFuture with value set in both onSuccess and onFailure callbacks, so you can throw AssertionError in the catch block. There is no general strategy of response to ExecutionException.

Does my DataKey have to be final in my interface?

It must be final in executeAsync implementation because you reference it from anonymous class (onFailure callback);

Is this the right way to use ListenableFutureCallback in my executeAsync method? Or is there any better way to use that?

Don't see anything wrong with it.

Some advices:

  1. Consider making thread pool for async client configurable.

By default AsyncRestTemplate uses SimpleAsyncTaskExecutor which creates new thread for each request. This may be not suitable for all your clients. Note that if you follow this advice response to CancellationException must be different as client now can have reference to ExecutorService: throwing RuntimeException should be fine.

  1. Describe in (java)doc thread pool used by default!

  2. I would split sync and async versions.

  3. I think that using sync RestTemplate and implementing async version by means of sync version would simplify implementation.

  4. Consider returning more flexible ListenableFuture instead of plain Future (using SettableListenableFuture instead of SettableFuture).