-
-
Notifications
You must be signed in to change notification settings - Fork 210
[change] Making batch imports atomic #551 #603
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
Used transaction.atomic() to ensure all operations inside succeed together. Incase of failure rollback will occure. Fixes openwisp#551
nemesifier
left a comment
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.
In #551 I wrote:
What if the import operation fails in the middle?
I believe we would end up with half users created.
Have you tried making the import fail on purpose in the middle to see what happens?
Can we recreate this in the tests?
Example from chatgpt to raise an error the second time the save method is called:
from unittest.mock import patch
from django.db import DatabaseError
from myapp.models import MyModel
def fail_on_second_call(real_method):
calls = [True, False] # First call real, second call raises
def wrapper(self, *args, **kwargs):
if calls.pop(0):
return real_method(self, *args, **kwargs)
else:
raise DatabaseError("Simulated DB failure on second save")
return wrapper
with patch.object(MyModel, 'save', autospec=True) as mocked_save:
# Wrap original method
mocked_save.side_effect = fail_on_second_call(MyModel.save)
obj = MyModel(field="test")
obj.save() # ✅ Real save
obj.name = "test2"
try:
obj.save() # ❌ Fails with simulated DatabaseError
except DatabaseError as e:
print(f"Caught simulated error: {e}")We could do something like this to cause a failure the second time the save_user() method is called. Without the patch, I expect that the save operations wouldn't be rolledback, and the first user created during the import would remain in the DB.
With the fix, that shouldn't happen and any imported data should be automatically rolledback.
Just adding the transaction.atomic() block could solve it, but it doesn't give us any confidence that it's really working, let's validate our assumptions.
|
Closing due to lack of follow up, feel free to follow up and reopen this. |
hey @nemesifier, sorry for the delay, I must've missed this while updating my all other PRs. I have had some previous uncomitted work done here so I'll follow up with a commit now after some refinements. |
Wrapped add() and csvfile_upload() in database transactions. Added TestBatchAtomicity suite using BaseTransactionTestCase to verify that both the batch metadata and user records are correctly rolled back on failure. Fixes openwisp#551
Fixed Unformatted documentation file causing QA to fail in failure using openwisp-qa-format. Fixes openwisp#551
I've implemented 2 tests that check if both the add() and csvfile_upload() functions are truly atomic and would fail without
Athough making a test for failure for this was complicated as standard test tools hide the very database behavior needed to observe, in the test I am triggering a database IntegrityError via duplicate email constraints. This verifies that a failure midway through the import loop correctly triggers a ROLLBACK of both the partially created users and the RadiusBatch metadata record. |
📝 WalkthroughWalkthroughThe changes introduce database transaction atomicity to batch user import operations in OpenWISP RADIUS. Both the CSV file upload and the internal Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
Comment |
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
openwisp_radius/tests/test_batch_add_users.py (1)
150-186: Well-designed tests for atomicity verification.The test class correctly inherits from
BaseTransactionTestCaseand uses duplicate email constraints to triggerIntegrityError, which validates rollback behavior. Both tests effectively verify that partial imports are prevented.Minor enhancement suggestion for test_add_method_internal_atomicity:
Consider also asserting that the batch itself still exists (since it was created outside the add() method's atomic block) but has no associated users. This would more comprehensively validate the atomic boundary:
🔎 Suggested enhancement
def test_add_method_internal_atomicity(self): User = get_user_model() org = self._get_org() data = [ ["user_one", "pass123", "duplicate@example.com", "John", "Doe"], ["user_two", "pass123", "duplicate@example.com", "Jane", "Doe"], ] batch = self._create_radius_batch( name="atomic-integrity-test", strategy="csv", organization=org, csvfile=self._get_csvfile(data), ) with self.assertRaises(IntegrityError): batch.add(data) self.assertFalse(User.objects.filter(username="user_one").exists()) + # Verify batch still exists but has no users (validates atomic boundary) + self.assertTrue(RadiusBatch.objects.filter(name="atomic-integrity-test").exists()) + self.assertEqual(batch.users.count(), 0)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
openwisp_radius/base/models.pyopenwisp_radius/tests/test_batch_add_users.py
🧰 Additional context used
🧬 Code graph analysis (1)
openwisp_radius/tests/test_batch_add_users.py (4)
openwisp_radius/tests/__init__.py (2)
FileMixin(174-200)_get_csvfile(179-188)openwisp_radius/tests/mixins.py (1)
BaseTransactionTestCase(234-237)tests/openwisp2/sample_users/models.py (1)
User(20-29)openwisp_radius/base/models.py (2)
csvfile_upload(1008-1019)add(985-1006)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
- GitHub Check: Python==3.13 | django~=5.2.0
- GitHub Check: Python==3.10 | django~=5.1.0
- GitHub Check: Python==3.13 | django~=5.1.0
- GitHub Check: Python==3.11 | django~=4.2.0
- GitHub Check: Python==3.12 | django~=5.1.0
- GitHub Check: Python==3.10 | django~=5.2.0
- GitHub Check: Python==3.12 | django~=5.2.0
- GitHub Check: Python==3.11 | django~=5.1.0
- GitHub Check: Python==3.10 | django~=4.2.0
- GitHub Check: Python==3.12 | django~=4.2.0
- GitHub Check: Python==3.11 | django~=5.2.0
🔇 Additional comments (4)
openwisp_radius/base/models.py (3)
20-20: LGTM! Import added for atomicity support.The
transactionimport is necessary for the atomic blocks introduced in this PR.
988-999: Excellent implementation of atomicity in batch user creation.The atomic block correctly ensures all user creation operations succeed or roll back together. Notably, email sending (lines 999-1006) is correctly placed outside the transaction block, which is important because:
- Email sending is a side effect that cannot be rolled back
- Emails should only be sent after successful database commit
- Email failures shouldn't trigger database rollback
1016-1019: Nested transaction handling is correct.The
csvfile_upload()method wrapsfull_clean(),save(), andadd()in atransaction.atomic()block. Sinceadd()also usestransaction.atomic()at line 988, Django handles this with savepoints: the inner atomic block becomes a savepoint within the outer transaction. If any exception is raised withinadd()and propagates uncaught, the entire outer transaction is rolled back, ensuring the RadiusBatch record and any user records are not partially committed. Thesend_mail()calls outside the inner transaction guarantee emails are sent only after successful commit.openwisp_radius/tests/test_batch_add_users.py (1)
3-5: LGTM! Imports added for atomicity tests.The imports of
get_user_modelandIntegrityErrorare necessary for the new atomicity test cases.
Used transaction.atomic() to ensure all operations inside succeed together. Incase of failure rollback will occure.
Fixes #551
Checklist
Reference to Existing Issue
Closes #551.
Description of Changes
Added usage of transaction.atomic() functions to introduce atomicity and rollback in case of transaction operation failure.
Summary by CodeRabbit
Bug Fixes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.