-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Improvements for connection desynchronization and hardening of error handling with connections #1998
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
base: main
Are you sure you want to change the base?
Improvements for connection desynchronization and hardening of error handling with connections #1998
Changes from 7 commits
19e5ae1
8695e97
1ddd06b
78a73e4
8619393
b6974f7
ac7df01
cace84d
0ecb0fe
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 |
|---|---|---|
|
|
@@ -56,6 +56,7 @@ | |
| import com.mongodb.observability.micrometer.MongodbObservationContext; | ||
| import org.bson.BsonBinaryReader; | ||
| import org.bson.BsonDocument; | ||
| import org.bson.BsonSerializationException; | ||
| import org.bson.ByteBuf; | ||
| import org.bson.codecs.BsonDocumentCodec; | ||
| import org.bson.codecs.Decoder; | ||
|
|
@@ -571,8 +572,8 @@ private <T> T receiveCommandMessageResponse(final Decoder<T> decoder, final Comm | |
| new BsonDocumentCodec()), description.getServerAddress(), operationContext.getTimeoutContext()); | ||
| } | ||
|
|
||
| commandSuccessful = true; | ||
| commandEventSender.sendSucceededEvent(responseBuffers); | ||
| commandSuccessful = true; | ||
|
Member
Author
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. Why
|
||
|
|
||
| T commandResult = getCommandResult(decoder, responseBuffers, responseTo, operationContext.getTimeoutContext()); | ||
| hasMoreToCome = responseBuffers.getReplyHeader().hasMoreToCome(); | ||
|
|
@@ -583,20 +584,18 @@ private <T> T receiveCommandMessageResponse(final Decoder<T> decoder, final Comm | |
| } | ||
|
|
||
| return commandResult; | ||
| } catch (Exception e) { | ||
| if (!commandSuccessful) { | ||
| commandEventSender.sendFailedEvent(e); | ||
| } | ||
| } catch (Throwable t) { | ||
| onCommandFailure(t, commandSuccessful, commandEventSender); | ||
| if (tracingSpan != null) { | ||
| if (e instanceof MongoCommandException) { | ||
| if (t instanceof MongoCommandException) { | ||
| MongodbObservationContext ctx = tracingSpan.getMongodbObservationContext(); | ||
| if (ctx != null) { | ||
| ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) e).getErrorCode())); | ||
| ctx.setResponseStatusCode(String.valueOf(((MongoCommandException) t).getErrorCode())); | ||
| } | ||
| } | ||
| tracingSpan.error(e); | ||
| tracingSpan.error(t); | ||
| } | ||
| throw e; | ||
| throw t; | ||
|
rozza marked this conversation as resolved.
|
||
| } finally { | ||
| if (tracingSpan != null) { | ||
| tracingSpan.closeScope(); | ||
|
|
@@ -605,6 +604,42 @@ private <T> T receiveCommandMessageResponse(final Decoder<T> decoder, final Comm | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether a failure raised while processing a command response leaves the connection | ||
| * reusable. These exception types are only raised after the full framed response has been read off | ||
| * the wire, so the stream remains synchronized and the connection can be returned to the pool: | ||
| * <ul> | ||
| * <li>{@link MongoCommandException} — an {@code ok: 0} error response (and subclasses)</li> | ||
| * <li>{@link MongoWriteConcernWithResponseException} — a write concern error carrying the response</li> | ||
| * <li>{@link MongoOperationTimeoutException} — a write concern timeout, or a server-side | ||
| * {@code MaxTimeMSExpired} ({@code ok: 0}) timeout; both are derived from the response body</li> | ||
| * <li>{@link BsonSerializationException} — a corrupt BSON body whose exact byte count was still consumed</li> | ||
| * </ul> | ||
| * Any other failure (e.g. a responseTo mismatch or an unexpected error) may have left the stream | ||
| * desynchronized, so the connection must be closed to prevent pool reuse. | ||
| */ | ||
| private static boolean connectionIsReusable(final Throwable failure) { | ||
| return failure instanceof MongoCommandException | ||
| || failure instanceof MongoWriteConcernWithResponseException | ||
| || failure instanceof MongoOperationTimeoutException | ||
| || failure instanceof BsonSerializationException; | ||
|
Collaborator
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. Can we expand |
||
| } | ||
|
|
||
| /** | ||
| * Handles a failure raised while sending or processing a command, identically on the sync and async paths: | ||
| * closes the connection unless the failure leaves it {@linkplain #connectionIsReusable reusable}, and emits a | ||
| * command-failed event unless a succeeded event has already been sent. | ||
| */ | ||
| private void onCommandFailure(final Throwable failure, final boolean commandSuccessful, | ||
| final CommandEventSender commandEventSender) { | ||
| if (!connectionIsReusable(failure)) { | ||
| close(); | ||
|
Collaborator
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. If try {
if (!connectionIsReusable(failure)) {
close();
}
} catch (Throwable closeFailure) {
failure.addSuppressed(closeFailure)
}
if (!commandSuccessful) {
sender.sendFailedEvent(failure);
} |
||
| } | ||
| if (!commandSuccessful) { | ||
| commandEventSender.sendFailedEvent(failure); | ||
| } | ||
| } | ||
|
|
||
| private <T> void sendAndReceiveAsyncInternal(final CommandMessage message, final Decoder<T> decoder, | ||
| final OperationContext operationContext, final SingleResultCallback<T> callback) { | ||
| if (isClosed()) { | ||
|
|
@@ -730,28 +765,33 @@ private <T> void sendCommandMessageAsync(final int messageId, final Decoder<T> d | |
| return; | ||
| } | ||
| assertNotNull(responseBuffers); | ||
| T commandResult; | ||
| T commandResult = null; | ||
| boolean commandSuccessful = false; | ||
| Throwable failure = null; | ||
| try { | ||
| updateSessionContext(operationContext.getSessionContext(), responseBuffers); | ||
| boolean commandOk = | ||
| isCommandOk(new BsonBinaryReader(new ByteBufferBsonInput(responseBuffers.getBodyByteBuffer()))); | ||
| responseBuffers.reset(); | ||
| if (!commandOk) { | ||
| MongoException commandFailureException = getCommandFailureException( | ||
| throw getCommandFailureException( | ||
| responseBuffers.getResponseDocument(messageId, new BsonDocumentCodec()), | ||
| description.getServerAddress(), operationContext.getTimeoutContext()); | ||
| commandEventSender.sendFailedEvent(commandFailureException); | ||
| throw commandFailureException; | ||
| } | ||
| commandEventSender.sendSucceededEvent(responseBuffers); | ||
| commandSuccessful = true; | ||
|
|
||
| commandResult = getCommandResult(decoder, responseBuffers, messageId, operationContext.getTimeoutContext()); | ||
| } catch (Throwable localThrowable) { | ||
| callback.onResult(null, localThrowable); | ||
| return; | ||
| failure = localThrowable; | ||
| } finally { | ||
| responseBuffers.close(); | ||
| } | ||
| if (failure != null) { | ||
| onCommandFailure(failure, commandSuccessful, commandEventSender); | ||
| callback.onResult(null, failure); | ||
| return; | ||
| } | ||
| callback.onResult(commandResult, null); | ||
| })); | ||
| } | ||
|
|
@@ -783,9 +823,9 @@ public void sendMessage(final List<ByteBuf> byteBuffers, final int lastRequestId | |
| } | ||
| try { | ||
| stream.write(byteBuffers, operationContext); | ||
| } catch (Exception e) { | ||
| } catch (Throwable t) { | ||
| close(); | ||
| throwTranslatedWriteException(e, operationContext); | ||
| throwTranslatedWriteException(t, operationContext); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -803,10 +843,10 @@ public void sendMessageAsync( | |
| c.complete(c); | ||
| }).thenRunTryCatchAsyncBlocks(c -> { | ||
| stream.writeAsync(byteBuffers, operationContext, c.asHandler()); | ||
| }, Exception.class, (e, c) -> { | ||
| }, Throwable.class, (t, c) -> { | ||
| try { | ||
| close(); | ||
| throwTranslatedWriteException(e, operationContext); | ||
| throwTranslatedWriteException(t, operationContext); | ||
| } catch (Throwable translatedException) { | ||
| c.completeExceptionally(translatedException); | ||
| } | ||
|
|
@@ -859,12 +899,12 @@ public void completed(@Nullable final ByteBuf buffer) { | |
| @Override | ||
| public void failed(final Throwable t) { | ||
| close(); | ||
| callback.onResult(null, translateReadException(t, operationContext)); | ||
| callback.onResult(null, translateReadFailure(t, operationContext)); | ||
| } | ||
| }); | ||
| } catch (Exception e) { | ||
| } catch (Throwable t) { | ||
| close(); | ||
| callback.onResult(null, translateReadException(e, operationContext)); | ||
| callback.onResult(null, translateReadFailure(t, operationContext)); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -888,7 +928,19 @@ private void updateSessionContext(final SessionContext sessionContext, final Res | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Rethrows a fatal JVM {@link Error} (e.g. {@link OutOfMemoryError}) unchanged, so it is never downgraded to a | ||
| * catchable {@link MongoException}. Used by the paths that propagate a failure by throwing; the async read path | ||
| * delivers the failure as a callback value instead and uses {@link #translateReadFailure} for the same purpose. | ||
| */ | ||
| private static void rethrowIfError(final Throwable t) { | ||
| if (t instanceof Error) { | ||
| throw (Error) t; | ||
| } | ||
| } | ||
|
|
||
| private void throwTranslatedWriteException(final Throwable e, final OperationContext operationContext) { | ||
| rethrowIfError(e); | ||
| if (e instanceof MongoSocketWriteTimeoutException && operationContext.getTimeoutContext().hasTimeoutMS()) { | ||
| throw createMongoTimeoutException(e); | ||
| } | ||
|
|
@@ -906,6 +958,18 @@ private void throwTranslatedWriteException(final Throwable e, final OperationCon | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Translates a read failure for delivery to an async callback. {@link Error}s are passed through unchanged | ||
| * rather than wrapped in a {@link MongoException}, so a fatal JVM error (e.g. {@link OutOfMemoryError}) is not | ||
| * downgraded to a catchable exception. The sync read path uses {@link #rethrowIfError} for the same purpose. | ||
| */ | ||
| private Throwable translateReadFailure(final Throwable e, final OperationContext operationContext) { | ||
| if (e instanceof Error) { | ||
| return e; | ||
| } | ||
| return translateReadException(e, operationContext); | ||
| } | ||
|
|
||
| private MongoException translateReadException(final Throwable e, final OperationContext operationContext) { | ||
| if (operationContext.getTimeoutContext().hasTimeoutMS()) { | ||
| if (e instanceof SocketTimeoutException) { | ||
|
|
@@ -938,6 +1002,9 @@ private MongoSocketReadTimeoutException createReadTimeoutException(final Socket | |
| } | ||
|
|
||
| private ResponseBuffers receiveResponseBuffers(final OperationContext operationContext) { | ||
| // The uncompressed buffer is allocated by us (not handed to ResponseBuffers until the last | ||
| // statement of the compressed branch), so it must be released if anything fails beforehand. | ||
| ByteBuf uncompressedBuffer = null; | ||
| try { | ||
| ByteBuf messageHeaderBuffer = stream.read(MESSAGE_HEADER_LENGTH, operationContext); | ||
| MessageHeader messageHeader; | ||
|
|
@@ -955,11 +1022,11 @@ private ResponseBuffers receiveResponseBuffers(final OperationContext operationC | |
|
|
||
| Compressor compressor = getCompressor(compressedHeader); | ||
|
|
||
| ByteBuf buffer = getBuffer(compressedHeader.getUncompressedSize()); | ||
| compressor.uncompress(messageBuffer, buffer); | ||
| uncompressedBuffer = getBuffer(compressedHeader.getUncompressedSize()); | ||
| compressor.uncompress(messageBuffer, uncompressedBuffer); | ||
|
|
||
| buffer.flip(); | ||
| return new ResponseBuffers(new ReplyHeader(buffer, compressedHeader), buffer); | ||
| uncompressedBuffer.flip(); | ||
| return new ResponseBuffers(new ReplyHeader(uncompressedBuffer, compressedHeader), uncompressedBuffer); | ||
| } else { | ||
| ResponseBuffers responseBuffers = new ResponseBuffers(new ReplyHeader(messageBuffer, messageHeader), messageBuffer); | ||
| releaseMessageBuffer = false; | ||
|
|
@@ -971,7 +1038,11 @@ private ResponseBuffers receiveResponseBuffers(final OperationContext operationC | |
| } | ||
| } | ||
| } catch (Throwable t) { | ||
| if (uncompressedBuffer != null) { | ||
| uncompressedBuffer.release(); | ||
| } | ||
| close(); | ||
| rethrowIfError(t); | ||
| throw translateReadException(t, operationContext); | ||
| } | ||
| } | ||
|
|
@@ -1005,18 +1076,25 @@ public void onResult(@Nullable final ByteBuf result, @Nullable final Throwable t | |
| callback.onResult(null, t); | ||
| return; | ||
| } | ||
| MessageHeader messageHeader = null; | ||
| Throwable headerParsingFailure = null; | ||
| try { | ||
| assertNotNull(result); | ||
| MessageHeader messageHeader = new MessageHeader(result, description.getMaxMessageSize()); | ||
| readAsync(messageHeader.getMessageLength() - MESSAGE_HEADER_LENGTH, operationContext, | ||
| new MessageCallback(messageHeader)); | ||
| messageHeader = new MessageHeader(result, description.getMaxMessageSize()); | ||
| } catch (Throwable localThrowable) { | ||
| callback.onResult(null, localThrowable); | ||
| headerParsingFailure = localThrowable; | ||
| } finally { | ||
| if (result != null) { | ||
| result.release(); | ||
| } | ||
| } | ||
| if (headerParsingFailure != null) { | ||
| close(); | ||
| callback.onResult(null, headerParsingFailure); | ||
| return; | ||
| } | ||
| readAsync(messageHeader.getMessageLength() - MESSAGE_HEADER_LENGTH, operationContext, | ||
| new MessageCallback(messageHeader)); | ||
| } | ||
|
|
||
| private class MessageCallback implements SingleResultCallback<ByteBuf> { | ||
|
|
@@ -1034,19 +1112,24 @@ public void onResult(@Nullable final ByteBuf result, @Nullable final Throwable t | |
| } | ||
| boolean releaseResult = true; | ||
| assertNotNull(result); | ||
| ResponseBuffers responseBuffers = null; | ||
| Throwable bodyParsingFailure = null; | ||
| // The uncompressed buffer is allocated by us and is not handed to ResponseBuffers until the | ||
| // last statement of the try, so it must be released if anything fails beforehand. | ||
| ByteBuf uncompressedBuffer = null; | ||
| try { | ||
| ReplyHeader replyHeader; | ||
| ByteBuf responseBuffer; | ||
| if (messageHeader.getOpCode() == OP_COMPRESSED.getValue()) { | ||
| try { | ||
| CompressedHeader compressedHeader = new CompressedHeader(result, messageHeader); | ||
| Compressor compressor = getCompressor(compressedHeader); | ||
| ByteBuf buffer = getBuffer(compressedHeader.getUncompressedSize()); | ||
| compressor.uncompress(result, buffer); | ||
| uncompressedBuffer = getBuffer(compressedHeader.getUncompressedSize()); | ||
| compressor.uncompress(result, uncompressedBuffer); | ||
|
|
||
| buffer.flip(); | ||
| replyHeader = new ReplyHeader(buffer, compressedHeader); | ||
| responseBuffer = buffer; | ||
| uncompressedBuffer.flip(); | ||
| replyHeader = new ReplyHeader(uncompressedBuffer, compressedHeader); | ||
| responseBuffer = uncompressedBuffer; | ||
| } finally { | ||
| releaseResult = false; | ||
| result.release(); | ||
|
|
@@ -1056,14 +1139,25 @@ public void onResult(@Nullable final ByteBuf result, @Nullable final Throwable t | |
| responseBuffer = result; | ||
| releaseResult = false; | ||
| } | ||
| callback.onResult(new ResponseBuffers(replyHeader, responseBuffer), null); | ||
| // Must be the last statement in the try: ResponseBuffers now owns responseBuffer, so | ||
| // nothing that could throw may follow it, or the buffer would leak (it is not released below). | ||
| responseBuffers = new ResponseBuffers(replyHeader, responseBuffer); | ||
| } catch (Throwable localThrowable) { | ||
|
rozza marked this conversation as resolved.
|
||
| callback.onResult(null, localThrowable); | ||
| if (uncompressedBuffer != null) { | ||
| uncompressedBuffer.release(); | ||
| } | ||
| bodyParsingFailure = localThrowable; | ||
| } finally { | ||
| if (releaseResult) { | ||
| result.release(); | ||
| } | ||
| } | ||
| if (bodyParsingFailure != null) { | ||
| close(); | ||
| callback.onResult(null, bodyParsingFailure); | ||
| return; | ||
| } | ||
| callback.onResult(responseBuffers, null); | ||
| } | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.