-
-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5135 from kobotoolbox/fix-transfer-ownership-race…
…-condition Fix project transfer failure in some circumstances
- Loading branch information
Showing
10 changed files
with
172 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
96 changes: 96 additions & 0 deletions
96
kobo/apps/project_ownership/management/commands/resume_failed_transfers_2_024_25_fix.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
from django.core.management import call_command | ||
from django.core.management.base import BaseCommand | ||
|
||
from ...models import ( | ||
Transfer, | ||
TransferStatus, | ||
TransferStatusChoices, | ||
TransferStatusTypeChoices, | ||
) | ||
from ...utils import ( | ||
move_media_files, | ||
move_attachments, | ||
rewrite_mongo_userform_id, | ||
) | ||
|
||
|
||
class Command(BaseCommand): | ||
help = ( | ||
'Resume project ownership transfers done under `2.024.25` which failed ' | ||
'with error: "Project A : previous_owner -> new_owner is not in progress"' | ||
) | ||
|
||
def handle(self, *args, **options): | ||
|
||
usernames = set() | ||
verbosity = options['verbosity'] | ||
|
||
for transfer_status in TransferStatus.objects.filter( | ||
status=TransferStatusChoices.FAILED, | ||
status_type=TransferStatusTypeChoices.GLOBAL, | ||
error__icontains='is not in progress', | ||
).iterator(): | ||
transfer = transfer_status.transfer | ||
if transfer.asset.pending_delete: | ||
if verbosity: | ||
self.stdout.write( | ||
f'Project `{transfer.asset}` is in trash bin, skip it!' | ||
) | ||
continue | ||
|
||
if not self._validate_whether_transfer_can_be_fixed(transfer): | ||
if verbosity: | ||
self.stdout.write( | ||
f'Project `{transfer.asset}` transfer cannot be fixed' | ||
f' automatically' | ||
) | ||
continue | ||
|
||
if not transfer.asset.has_deployment: | ||
continue | ||
|
||
if verbosity: | ||
self.stdout.write( | ||
f'Resuming `{transfer.asset}` transfer…' | ||
) | ||
self._move_data(transfer) | ||
move_attachments(transfer) | ||
move_media_files(transfer) | ||
if verbosity: | ||
self.stdout.write('\tDone!') | ||
usernames.add(transfer.invite.recipient.username) | ||
|
||
# Update attachment storage bytes counters | ||
for username in usernames: | ||
call_command( | ||
'update_attachment_storage_bytes', | ||
verbosity=verbosity, | ||
force=True, | ||
username=username, | ||
) | ||
|
||
def _move_data(self, transfer: Transfer): | ||
|
||
# Sanity check | ||
asset = transfer.asset | ||
rewrite_mongo_userform_id(transfer) | ||
number_of_submissions = asset.deployment.xform.num_of_submissions | ||
submission_ids = [ | ||
s['_id'] | ||
for s in asset.deployment.get_submissions(asset.owner, fields=['_id']) | ||
] | ||
|
||
if number_of_submissions == (mongo_document_count := len(submission_ids)): | ||
self.stdout.write(f'\tSuccess: {number_of_submissions} submissions moved!') | ||
else: | ||
missing_count = number_of_submissions - mongo_document_count | ||
self.stdout.write( | ||
f'\t⚠️ Only {mongo_document_count} submissions moved, ' | ||
f'{missing_count} are missing!' | ||
) | ||
|
||
def _validate_whether_transfer_can_be_fixed(self, transfer: Transfer) -> bool: | ||
original_new_owner_id = transfer.invite.recipient_id | ||
current_owner_id = transfer.asset.owner_id | ||
|
||
return current_owner_id == original_new_owner_id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from contextlib import contextmanager | ||
from unittest import mock | ||
|
||
from django.contrib.auth.management import DEFAULT_DB_ALIAS | ||
|
||
|
||
@contextmanager | ||
def immediate_on_commit(using=None): | ||
""" | ||
Context manager executing transaction.on_commit() hooks immediately as | ||
if the connection was in auto-commit mode. This is required when | ||
using a subclass of django.test.TestCase as all tests are wrapped in | ||
a transaction that never gets committed. | ||
Source: https://code.djangoproject.com/ticket/30457#comment:1 | ||
""" | ||
immediate_using = DEFAULT_DB_ALIAS if using is None else using | ||
|
||
def on_commit(func, using=None): | ||
using = DEFAULT_DB_ALIAS if using is None else using | ||
if using == immediate_using: | ||
func() | ||
|
||
with mock.patch('django.db.transaction.on_commit', side_effect=on_commit) as patch: | ||
yield patch |