Opened 3 years ago
Closed 3 years ago
#33161 closed New feature (fixed)
Do not ignore transaction durability errors within TestCase
Reported by: | Krzysztof Jagiełło | Owned by: | Krzysztof Jagiełło |
---|---|---|---|
Component: | Testing framework | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | David Seddon, Adam Johnson, Ian Foote, Hannes Ljungberg | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
Currently there is a discrepancy in how durable atomic blocks are handled in TransactionTestCase
vs TestCase
. Using the former, nested durable atomic blocks will, as expected, result in a RuntimeError
. Using the latter however, the error will go unnoticed as the durability check is turned off.
I have faced some issues with this behaviour in a codebase where we heavily utilize TestCase
and where we recently started to introduce durable atomic blocks – the durability errors do not surface until the code hits staging/production. The solution could be to switch over to TransactionTestCase
for the test classes that hit code paths with durable atomic blocks, but having to identify which tests could be affected by this issue is a bit inconvenient. And then there is the performance penalty of using TransactionTestCase
.
So, to the issue at hand. The durability check is disabled for TestCase
because otherwise durable atomic blocks would fail immediately as TestCase
wraps its tests in transactions. We could however add a marker to the transactions created by TestCase
, keep a stack of active transactions and make the durability check take the stack of transactions with their respective markers into account. This way we could easily detect when a durable atomic block is directly within a transaction created by TestCase
and skip the durability check only for this specific scenario.
To better illustrate what I am proposing here, I have prepared a PoC patch. Let me know what you think!
Change History (18)
comment:1 by , 3 years ago
Description: | modified (diff) |
---|
comment:2 by , 3 years ago
Owner: | changed from | to
---|---|
Status: | new → assigned |
comment:3 by , 3 years ago
Version: | 3.2 → dev |
---|
comment:4 by , 3 years ago
Cc: | added |
---|
comment:5 by , 3 years ago
Could you elaborate on what would cause the slowdown? I have noticed the mention about it in the docs, but couldn't really figure out why this would incur any significant performance penalty.
comment:6 by , 3 years ago
Cc: | added |
---|
comment:7 by , 3 years ago
I think it's reasonable for tests to still detect such problematic durable atomic blocks, without requiring TransactionTestCase
.
I've reviewed the initial PR and I came up with an idea for a simpler implementation: https://github.com/django/django/pull/14922
My implementation removes ensure_durability
and adds another boolean flag on BaseDatabaseWrapper
. This leaves us with about the same level of complexity as before the PR, so hopefully that alleviates any concerns.
(I also don't believe there's any potential for notable slowdown from the initial implementation - it only adds one stack.)
comment:8 by , 3 years ago
Triage Stage: | Unreviewed → Accepted |
---|
(I also don't believe there's any potential for notable slowdown from the initial implementation - it only adds one stack.)
Persuaded.
comment:9 by , 3 years ago
Description: | modified (diff) |
---|
comment:10 by , 3 years ago
Thanks Adam! Your approach seems more straightforward to me than the initial implementation. Keeping around the stack of atomic blocks just to handle this is specific scenario was a bit unnecessary.
comment:11 by , 3 years ago
Description: | modified (diff) |
---|
comment:12 by , 3 years ago
It took me a while to realize that a boolean flag on BaseDatabaseWrapper
might not be enough for cases like these:
def test_sequence_of_durables(self): with transaction.atomic(durable=True): ... with transaction.atomic(durable=True): ...
Keeping the atomic block stack around makes it easier to handle it correctly.
comment:13 by , 3 years ago
Description: | modified (diff) |
---|
comment:14 by , 3 years ago
Thanks for looking into this!
We've been using a hand-rolled equivalent of durable=True
extensively for a couple of years on a large code base. Our implementation consults a setting which we typically override along the lines of @override_settings(DISABLE_DURABILITY_CHECKS=True)
. Two years in, I don't think there's been much value in requiring the override for each relevant test, and I imagine we'll switch to using a global setting (at least for the transaction-wrapped test cases which, conveniently, use a different configuration to the unwrapped ones).
So on balance I think it makes more sense to disable the behaviour than require developers to turn it off on a test-by-test basis, though it might be worthwhile providing a setting so that people can choose the default behaviour.
Having said that, there is a downside which reminds me of the difficulties of testing code that uses select_for_update
. I have seen many occasions where wrapped test cases are the only coverage of such code, and we don't see the issue until it hits production. If we just turn off the checking wholesale, it's more likely that unrunnable code will make its way onto production.
An even better solution would be for wrapped TestCases to be able to tell the difference between the transaction begun by the TestCase itself and any subsequent atomic blocks entered. They would then allow durable blocks to be entered providing the only block we're in is that initial block (possibly this is what has been implemented, I wasn't able to tell at first glance).
comment:15 by , 3 years ago
Replying to David Seddon:
It reminds me of the difficulties of testing code that uses
select_for_update
. I have seen many occasions where wrapped test cases are the only coverage of such code, and we don't see the issue until it hits production.
Got hit by that one to in the past too! I think that once this patch gets merged we should be able to apply a similar fix to select_for_update
too.
Regarding the patch, I have decided to keep working on my PR (https://github.com/django/django/pull/14919) as the one proposed by Adam did not work for sequential durable transactions (see the repro case in my previous comment). Let me know if there is anything you would like me to adjust in my PR before its merge-ready.
comment:16 by , 3 years ago
Description: | modified (diff) |
---|
comment:17 by , 3 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
I don't think it's worth additional complexity and potential slowdown of
TestCase
for all users. I will wait for a second opinion before making a decision.