Fix Apache HttpClient 5 reactor recovery#2034
Conversation
27d4ae0 to
3004731
Compare
| * Releases the (potentially very large) response buffer and converts a fatal {@link OutOfMemoryError} into a | ||
| * recoverable {@link IOException}. |
There was a problem hiding this comment.
I'm not sure it is safe to do this. The contract of java.lang.Error generally is that it "indicates serious problems that a reasonable application should not try to catch". I don't think you can assume the JVM is in a workable state after observing an error and the right thing to do is let the process crash. In this case, you don't actually know if the response buffer in question here caused the OOM, versus something else in the application unrelated to this client that consumed all the heap.
|
Would it make sense to break this into 2 PRs? One with the fix to allow recovery from reactor shutdowns, and another PR with the changes to reduce the chance of reactor-killing memory failures. |
Signed-off-by: Cédric Pelvet <cedric.pelvet@gmail.com>
Signed-off-by: Cédric Pelvet <cedric.pelvet@gmail.com>
Signed-off-by: Cédric Pelvet <cedric.pelvet@gmail.com>
3004731 to
a6d50d0
Compare
andrross
left a comment
There was a problem hiding this comment.
Looks good to me. @reta can you take a look?
@sharp-pixel The PR description should be updated since this no longer has all the original changes.
| if (isRecoverableReactorFailure(runtimeFailure, client) == false) { | ||
| throw runtimeFailure; | ||
| } | ||
| // The I/O reactor has been shut down. Try to recover by rebuilding the client, then retry this request once | ||
| // on the fresh client. | ||
| retryAfterReactorFailure(nodeTuple, options, request, warningsHandler, listener, node, client, runtimeFailure, allowRecovery); | ||
| return; |
There was a problem hiding this comment.
Nitpick, but easier to follow if you invert the logic:
| if (isRecoverableReactorFailure(runtimeFailure, client) == false) { | |
| throw runtimeFailure; | |
| } | |
| // The I/O reactor has been shut down. Try to recover by rebuilding the client, then retry this request once | |
| // on the fresh client. | |
| retryAfterReactorFailure(nodeTuple, options, request, warningsHandler, listener, node, client, runtimeFailure, allowRecovery); | |
| return; | |
| if (isRecoverableReactorFailure(runtimeFailure, client)) { | |
| // The I/O reactor has been shut down. Try to recover by rebuilding the client, then retry this request once | |
| // on the fresh client. | |
| retryAfterReactorFailure(nodeTuple, options, request, warningsHandler, listener, node, client, runtimeFailure, allowRecovery); | |
| } else { | |
| throw runtimeFailure; | |
| } |
Yeah I will take a look in a few days, thanks @andrross and @sharp-pixel |
|
@sharp-pixel few things from my side (thanks for working on it first of all):
In general, I think there is nothing fundamentally wrong to ask the caller to recreate the client in case of fatal failures (like I/O reactor one). The current fix is changing a lot of internals of the ApacheHttpClient5Transport, but we should not forget that we also have AwsSdk2Transport (that internally uses Apache5HttpClient) and it would be reasonable to expect the same client recovery there (since it is the same AHC5 client under the hood, with all the same issues). May be we should take a step back and:
Thank you. |
@reta That change causes certain code paths to convert OutOfMemoryErrors into RuntimeExceptions and allow the JVM to continue operating. Maybe I'm paranoid but that seems like a bad idea? If I'm wrong about that then it's probably fine to take the smaller-scoped change. |
@andrross It does indeed, but it has been applied to widely used software (elasticsearch-java), so may be we both are overly paranoid? |
@reta Without the fix, does the OOM get swallowed anyway, just with the side effect that the netty reactor is shut down? |
I am not 100% sure, I will look closely over the weekend, I believe that either a) reactor is shutdown cleanly b) reactor is not shutdown, but the thread(s) dies? |
|
@reta If the existing behavior is that Netty prevents the OOM exception from propagating to the application, then the fix to prevent OOMs from shutting down the reactor seems much less risky. |
@andrross so I did check the XxxError handling that the patch applies (from elastic/elasticsearch-java#1049) prevents the reactor shutdown - basically, there is no need to recreate the client |
Fixes #1969
Problem
ApacheHttpClient5Transportcould enter an unrecoverable state after Apache HttpClient 5 reportedIOReactorShutdownException. Once the underlying I/O reactor was shut down, the transport continued using the same deadCloseableHttpAsyncClient, so every later request failed until the application recreated the transport.One common trigger is memory pressure while buffering large or highly concurrent responses. The existing per-response buffer limit capped a single response, but it did not cap aggregate heap usage across concurrent responses, so many in-flight responses could still exhaust the heap and kill the reactor.
Solution
This change makes
ApacheHttpClient5Transportrecover from reactor shutdowns when it owns the client lifecycle:IOReactorShutdownExceptionis detected.execute(...)failures and callback-delivered reactor shutdown failures.It also reduces the chance of reactor-killing memory failures:
OutOfMemoryErrorduring response buffering into a request-levelIOExceptionso the reactor thread is not killed.Testing
Ran:
Also verified the locally published modified client in a standalone sample app using the normal existing transport builder path against OpenSearch 3.7.0 in Docker: