-
Notifications
You must be signed in to change notification settings - Fork 905
GODRIVER-3172 Read response in the background after an op timeout. #1589
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
Conversation
22b758d
to
cc0711c
Compare
API Change Report./x/mongo/driverincompatible changes##ExtractErrorFromServerResponse: changed from func(./x/bsonx/bsoncore.Document) error to func(context.Context, ./x/bsonx/bsoncore.Document) error compatible changesOperation.OmitCSOTMaxTimeMS: added ./x/mongo/driver/operationcompatible changes(*Aggregate).OmitCSOTMaxTimeMS: added ./x/mongo/driver/topologycompatible changesBGReadCallback: added |
Was this problem in the other PR addressed? |
x/mongo/driver/errors.go
Outdated
func ExtractErrorFromServerResponse(doc bsoncore.Document) error { | ||
// | ||
// Set isCSOT to true if "timeoutMS" is set on the Client. | ||
func ExtractErrorFromServerResponse(doc bsoncore.Document, isCSOT bool) 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.
Suggest not using a bool here and instead pass in the context and use csot.IsTimeoutContext(ctx) within decodeResult. This ensures that the source of CSOT-ness is derived from a context.
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.
Sounds good, will do.
@ShaneHarvey I'm currently testing the tradeoffs between doing the read in the background vs doing it in-line with a subsequent operation. I'll document or link to those results here. |
x/mongo/driver/topology/pool.go
Outdated
errs := make([]error, 0) | ||
connClosed := false | ||
|
||
err := conn.nc.SetReadDeadline(time.Now().Add(1 * time.Second)) |
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 would like to understand the reasoning behind the 1s deadline for reading any remaining server response. I'm wondering if this needs to be tuneable or whether it should be something dynamic like some multiple of P90 RTT.
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 1-second deadline is the failsafe timeout if the server doesn't respond in a reasonable period of time after the operation timed out. I picked 1 second because, in our testing, that value covered >99% of the server responses that came after the operation timed out when maxTimeMS
is set (the "overshoot duration").
The "overshoot duration" is a combination of timing variability in the client application, database, and network, so it's not clear if there is a practical measurement-based algorithm for picking that value. In the updated PR, that failsafe value is configurable using a global setting in an experimental package (topology.BGReadTimeout
). For now the goal is to make that a "safe" but not configurable value, but if we observe that different use cases need different values, we may make it generally configurable.
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.
LGTM!
@@ -683,8 +688,7 @@ func (op Operation) Execute(ctx context.Context) error { | |||
first = false | |||
} | |||
|
|||
// Calculate maxTimeMS value to potentially be appended to the wire message. | |||
maxTimeMS, err := op.calculateMaxTimeMS(ctx, srvr.RTTMonitor().P90(), srvr.RTTMonitor().Stats()) | |||
maxTimeMS, err := op.calculateMaxTimeMS(ctx, srvr.RTTMonitor()) |
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.
@matthewdale We just discovered an edge case here. We apparently have a background job that sets a context timeout of ~25 days, more specifically 2211839988
milliseconds which is greater than 32-bit max int so they started getting BadValue) 2211839988 value for maxTimeMS is out of range [0, 2147483647]
Obviously this is a somewhat far off edge case, I'm asking the team what's the reason of having a context with a deadline this long.
But wanted to flag this to you since this PR is going to start erroring out on those edge cases. The patch we applied internally is to just take min(math.MaxInt32 - (2 * time.Millisecond), time.Duration(float64(timeLeft)))
to avoid triggering that validation error. Not sure if the logic in calculateMaxTimeMS
should just say "That timeout is too long, I'm not going to set maxTimeMS at all" or if we'd want to apply the same min
logic.
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.
Ah the explanation of why the timeout is so long makes sense. tl;dr - Processes for things like refunds/chargebacks can have month long time frames, and various workflow orchestration systems will set a golang context to timeout when that window ends even if the current step of the workflow won't take nearly that long.
Our team is going to look at tuning DB calls to have more granular contexts/timeouts, but this still might be a broader issue y'all might be concerned about since others could run into it
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.
Wow, nice find! I've updated the driver to omit maxTimeMS
if the calculated value is greater than max int32 so that there's no timeout you can set that will cause the "BadValue" error.
That means if an application starts an op with a timeout greater than 2147483647ms, there will be no database-side timeout automatically set. However, the client-side timeout logic will still work and will cause the driver to close the connection if that timeout is ever reached, cancelling the op on the server-side (unless the command returns within the new 1-second "grace period" after the timeout).
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.
Awesome, that approach makes sense to me and the code looks good. Nice job on the quick fix!
@ShaneHarvey I did a bunch of testing between "background reads" during check-in and "foreground reads" during check-out after an operation times out. The results unfortunately didn't show a clear winner as both behaved remarkably similarly, but each approach seemed to have one major downside:
I had to test both approaches at very high operation and timeout volume before I observed the above differences. In most other circumstances that I tested, they behaved almost identically. For now I'm going to stick with the "background reads" approach to speed up delivering this fix. For the CSOT spec, I think we should allow driver teams to pick an approach suited to their language. |
Summary
Experimental change to minimize connection churn during lots-of-timeout events caused by increased database latency.
Changes:
maxTimeMS
from being added to commands when a context with deadline is used on an operation, even iftimeoutMS
is set on the client.maxTimeMS
from operations that return a cursor (Find
,Aggregate
) because the default behavior ofmaxTimeMS
is to limit the lifetime of the cursor as well as the initial command, which may be surprising (see DRIVERS-2722). The existing automaticmaxTimeMS
behavior remains the same (i.e. whentimeoutMS
is set and no Context deadline is provided,Find
andAggregate
still havemaxTimeMS
set).Background & Motivation
The Go Driver currently closes the in-use connection when a command times out on the client side. MongoDB processes commands sent on a single connection sequentially, so sending another command on the same connection after the previous one timed out could lead to the later command taking an unpredictable extra amount of time to complete (because it had to wait for the previous response to be read). Closing the connection is the simplest way to both resolve the unknown connection state and signal MongoDB to stop working on the command.
However, closing the connection after an operation timeout happens can cause problems when connection are closed and created at a very high rate. We've observed that setting
maxTimeMS
as described in the CSOT spec can cause MongoDB to reliably respond with a server-side timeout error 25-100ms after a client-side timeout occurs. That consistent response timing makes it feasible to await the response in the background after a client-side timeout instead of closing the connection.