-
Notifications
You must be signed in to change notification settings - Fork 246
DRIVERS-1934: withTransaction API retries too frequently #1851
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: master
Are you sure you want to change the base?
Conversation
| callbacks. | ||
|
|
||
| A previous design had no limits for retrying commits or entire transactions. The callback is always able indicate that | ||
| A previous design had no limits for retrying commits or entire transactions. The callback is always able to indicate that |
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.
Maybe I read it wrong, or maybe its a typo? Not sure.
| [abortTransaction](../transactions/transactions.md#aborttransaction) on the session. | ||
| 2. If the callback's error includes a "TransientTransactionError" label and the elapsed time of `withTransaction` is | ||
| less than 120 seconds, jump back to step two. | ||
| less than 120 seconds, sleep for `jitter * min(BACKOFF_INITIAL * (1.25**retry), BACKOFF_MAX)` where: |
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 know python uses ** as the exponential operator. I don't believe that is a standard across other programming languages. Is it clear that its exponent? Is there a different preferred symbol for exponent? (I know math commonly uses ^ but it typically also means bitwise XOR in code so I felt like that could be confusing.)
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.
** seems fine to me, but I might also be biased because that's what Node does 🙂. But I don't think people will have trouble understanding what this means.
| ### Retry Backoff is Enforced | ||
|
|
||
| Drivers should test that retries within `withTransaction` do not occur immediately. Ideally, set BACKOFF_INITIAL 500ms | ||
| and configure a failpoint that forces one retry. Ensure that the operation took more than 500ms so succeed. |
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.
This test would not work as described because jitter is non-deterministic.
An alternative test would be to use a failpoint to fail the transaction X times and then assert the overall time is larger than some threshold.
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.
Yeah, I realized that when implementing it. I set the fail point to fail 3 times and that seems to be consistently working (and without jitter failing 3 times would still cause the success to happen within 500ms)
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.
update: it was consistent locally but super flakey on atlas. I modified the with transaction code to append backoff values to make this easier to test and the test is now more stable.
|
|
||
| ### Retry Backoff is Enforced | ||
|
|
||
| Drivers should test that retries within `withTransaction` do not occur immediately. Configure a fail point that forces 3 |
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.
3 here was a bit of an arbitrary number. The most important part of this value is that its greater than 1
3 just felt like a small enough to be a quick test but big enough to conclude backoff is consistently happening.
If folks have more opinions on this number, I'm not attached to 3.
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'm not sure the _transaction_retry_backoffs concept is viable for testing here since:
- many languages don't have a way to implement it without making the attribute public
- It's an unbounded list that can grow forever.
- there's no prior art for something like this in the driver as far as I know.
Instead I'd suggest my test from earlier where we fail the transaction X times and assert the run time is greater than some threshold T. X should be large enough to reduce false positives where the test fails due to jitter resulting in a small delay for every retry.
We can calculate T by recording the command failed+succeeded events, summing their duration, and adding a fixed constant.
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.
Makes sense, it took me a bit but i figured out what X and T are for python -- I don't know if they'll be the same for the other languages tho? Should the test description leave it as X and T? or should I put in the numbers that I have and see what others have to say about 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.
An alternative I'm considering in Node is to configure the random number generator to be deterministic for testing purposes. Because this would make the tests both simpler to reason about and deterministic. ex: make random() always return 1, then we can make assertions on the timing of retries deterministically
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.
Ohhh! I like that idea! Would you want that to replace the current proposed test idea, or is this in addition to the previously defined test?
I'm imaging this as a second test where we fail the transactions a handful of times and calculate the backoff times assuming random() always returns 1 and ensure that the transaction takes longer than the sum of the backoff times.
The main reason to keep the first test (where we don't configure random) is to just ensure that jitter is applied? If so, then should that first test be modified to assert that the transaction succeeds in < T where T is the sum of the maximum backoff times?
Then together we're effectively checking a minimum and maximum threshold on the backoff? Did that make sense?
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.
It depends exactly what we think we need to test here, I think. If the goal is only to test that retry backoff is enforced, I think your current implementation works.
Although I'd be worried about flakiness, because anything that relies on random values is inherently non-determinstic and just due to the distribution of the delays it seems like flakes might be likely:
- assuming jitter = 1, the max delay for the test is 3064ms
- the last 4 retries account for almost 1750ms of delay in the test (330.8ms, 413.5ms, 500ms, 500ms)
So, short delays in the last few retries could make make the test run a lot shorter than expected. A determinsitic jitter would solve this problem. That, and the test is a bit simpler to reason about imo (I spent a bit of time trying to calculate the probability that this test fails assuming random() returns an equal distribution of values in [0,1], which goes away if we random() is deterministic).
I like the idea of keeping both tests if we do want to make sure jitter is applied, but a determinsitic alternative would be to make random() return a non-1 value (like .5) and assert that the test takes between [total sleep time with jitter = .5, total sleep time with jitter = 1 (or some other value)].
I also know this is feasible in Node, I'm not sure how feasible configuring the random() function would be in other drivers.
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 think theoretically the test described by shane works but I agree that flakiness is likely an issue. I tried to account for that with the "optionally change initial_backoff to a higher value" but I don't know how feasible that is across various drivers (hence the optional)
As you've stated, the effect of jitter on the later attempt have a higher impact than the earlier ones, but I generally hope that over time the avg of random should be ~0.5 which sanity checked my initial 1.25 second timeout (discovered through guess and check for better or worse) with random jitter -- but clearly we've been observing that it's not consistent enough across languages
Noting my journey in trying to get values that were consistent in python and seeing that they don't carry over to Node seems to imply that if this test were to actually be implemented, each driver may have to find their own values of X and T? which feels silly imo?
All of that is to say, I think at this point i'm convinced a deterministic approach is the way to go for tests. Just to be clear, you are suggesting that the two test be:
- random always returns 1 and assert that the test takes more than total sleep time (with jitter = 1)
- random always returns non-1 value,
aand assert that the test takes between [total sleep time with jitter =b, total sleep time with jitter =c(or some other value)].
doesa=bor can it be such thatb<=a? obviouslyc>a, correct?
how do we want to decide ona,b, andc? There is a part of me that worries if the time difference between total sleep time withcvs total sleep time withaisn't big enough, the rest of the driver operations could total test time > total sleep time with jitter =c? idk i think i'm rambling now.
|
|
||
| ### Retry Backoff is Enforced | ||
|
|
||
| Drivers should test that retries within `withTransaction` do not occur immediately. Configure a fail point that forces 3 |
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'm not sure the _transaction_retry_backoffs concept is viable for testing here since:
- many languages don't have a way to implement it without making the attribute public
- It's an unbounded list that can grow forever.
- there's no prior art for something like this in the driver as far as I know.
Instead I'd suggest my test from earlier where we fail the transaction X times and assert the run time is greater than some threshold T. X should be large enough to reduce false positives where the test fails due to jitter resulting in a small delay for every retry.
We can calculate T by recording the command failed+succeeded events, summing their duration, and adding a fixed constant.
source/transactions-convenient-api/transactions-convenient-api.md
Outdated
Show resolved
Hide resolved
| }, | ||
| "data": { | ||
| "failCommands": ["commitTransaction"], | ||
| "errorCode": 24, |
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.
Can we use this error code instead for these tests?:
code: 251,
name: NoSuchTransaction
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.
Changed in f40529f
| ``` | ||
|
|
||
| Let the callback for the transaction be a simple `insertOne` command. Check that the total time for all retries exceeded | ||
| 3.5 seconds. |
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.
These two tests are identical. What if we run this test twice, first with backoff disabled (eg jitter set to always be 0%) and second with it enabled and jitter set to always be 100%. Then we can assert the second attempt takes 3 seconds longer?
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.
Ooo i like this idea! Changed in f40529f
I decreased the number of failures to 13 so the test wouldn't take as long (resulting in total backing time to be 2.2 seconds) and added a +/- 1 second window for the difference between the two attempts.
| ``` | ||
|
|
||
| Let the callback for the transaction be a simple `insertOne` command. Let `no_backoff_time` be the time it took for the | ||
| command to succeed. |
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.
"command" -> "withTransaction call"
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.
changed in 56e88f0
| return `1`. Set the fail point to force 13 retries using the same command as before. Using the same callback as before, | ||
| check that the total time for the withTransaction command is within +/-1 second of `no_backoff_time` plus 2.2 seconds. | ||
| Note that 2.2 seconds is the sum of backoff 13 consecutive backoff values and the 1-second window is just to account for | ||
| potential networking differences between the two runs. |
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.
minor nit for tone:
"the 1-second window is just to account for
potential networking differences between the two runs."
->
"the 1-second window accounts for potential variance between the two runs.
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.
changed in 56e88f0
| This method can be expressed by the following pseudo-code: | ||
|
|
||
| ```typescript | ||
| var BACKOFF_INITIAL = 1 // 1ms initial backoff |
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.
Let's update this to use 5ms as described in the tech doc. And the growth rate as well.
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.
changed in 56e88f0
source/transactions-convenient-api/transactions-convenient-api.md
Outdated
Show resolved
Hide resolved
source/transactions-convenient-api/transactions-convenient-api.md
Outdated
Show resolved
Hide resolved
source/transactions-convenient-api/transactions-convenient-api.md
Outdated
Show resolved
Hide resolved
| withTransaction(callback, options) { | ||
| // Note: drivers SHOULD use a monotonic clock to determine elapsed time | ||
| var startTime = Date.now(); // milliseconds since Unix epoch | ||
| var TIMEOUT_MS = timeoutMS is None ? 120000 : TIMEOUT_MS |
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 didn't actually realize this pseudocode is my language 😅. None is not valid TS. I also forgot that it's slightly more complicated; a timeoutMS of 0 = no timeout. So it's actually gotta look something like:
| var TIMEOUT_MS = timeoutMS is None ? 120000 : TIMEOUT_MS | |
| var TIMEOUT_MS = typeof timeoutMS === 'number' | |
| ? timeoutMS === 0 | |
| ? Infinity | |
| : timeoutMS | |
| : 120; |
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.
aand i'm now just remembering that it's additionally complicated, because we can inherit the timeoutMS from the session.
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'll need to give this dedicated focus tomorrow just to make sure its right lol. But if you have a chance, want to give it a shot? It probably requires cross referencing the CSOT spec for timeout rules for withTransaction with the pseudocode here and the convenient API spec.
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.
LOL i did find it a bit odd that "pseudocode" seemed to basically just be TS in this doc but i'll take a closer look -- i'm not super familiar with CSOT spec so i'll have to give that a closer read. I'll drop anything I find here tho
| > [!NOTE] | ||
| > errorCode 251 is NoSuchTransaction. |
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.
This isn't formatting for me? Maybe it's something to do with the indentation?
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.
hmm wacky? it seemed to render in my preview in pycharm when I was editing this so i assumed it worked. I'll play around with the indentation after looking into CSOT. worse case i'll be boring and just have this note be normal text lol
Please complete the following before merging:
clusters).