Skip to content

Commit 98ffe0c

Browse files
committed
Use async task for ensure_versioned_database_exists
1 parent 2f6e2af commit 98ffe0c

File tree

6 files changed

+105
-50
lines changed

6 files changed

+105
-50
lines changed

contentcuration/contentcuration/models.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2602,7 +2602,7 @@ class CommunityLibrarySubmission(models.Model):
26022602

26032603
def save(self, *args, **kwargs):
26042604
# Not a top-level import to avoid circular import issues
2605-
from contentcuration.utils.publish import ensure_versioned_database_exists
2605+
from contentcuration.tasks import ensure_versioned_database_exists_task
26062606

26072607
# Validate on save that the submission author is an editor of the channel
26082608
# and that the version is not greater than the current channel version.
@@ -2630,7 +2630,11 @@ def save(self, *args, **kwargs):
26302630
# When creating a new submission, ensure the channel has a versioned database
26312631
# (it might not have if the channel was published before versioned databases
26322632
# were introduced).
2633-
ensure_versioned_database_exists(self.channel)
2633+
ensure_versioned_database_exists_task.fetch_or_enqueue(
2634+
user=self.author,
2635+
channel_id=self.channel.id,
2636+
channel_version=self.channel.version,
2637+
)
26342638

26352639
super().save(*args, **kwargs)
26362640

contentcuration/contentcuration/tasks.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from contentcuration.utils.csv_writer import write_user_csv
2020
from contentcuration.utils.nodes import calculate_resource_size
2121
from contentcuration.utils.nodes import generate_diff
22+
from contentcuration.utils.publish import ensure_versioned_database_exists
2223
from contentcuration.viewsets.user import AdminUserFilter
2324

2425

@@ -159,3 +160,8 @@ def sendcustomemails_task(subject, message, query):
159160
text,
160161
settings.DEFAULT_FROM_EMAIL,
161162
)
163+
164+
165+
@app.task(name="ensure_versioned_database_exists_task")
166+
def ensure_versioned_database_exists_task(channel_id, channel_version):
167+
ensure_versioned_database_exists(channel_id, channel_version)

contentcuration/contentcuration/tests/helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django_celery_results.models import TaskResult
88
from search.models import ContentNodeFullTextSearch
99

10+
from contentcuration.celery import app
1011
from contentcuration.models import ContentNode
1112

1213

@@ -71,3 +72,27 @@ def reverse_with_query(
7172
if query:
7273
return f"{url}?{urlencode(query)}"
7374
return url
75+
76+
77+
class EagerTasksTestMixin(object):
78+
"""
79+
Mixin to make Celery tasks run synchronously during the tests.
80+
"""
81+
82+
celery_task_always_eager = None
83+
84+
@classmethod
85+
def setUpClass(cls):
86+
super().setUpClass()
87+
# update celery so tasks are always eager for this test, meaning they'll execute synchronously
88+
cls.celery_task_always_eager = app.conf.task_always_eager
89+
app.conf.update(task_always_eager=True)
90+
91+
def setUp(self):
92+
super().setUp()
93+
clear_tasks()
94+
95+
@classmethod
96+
def tearDownClass(cls):
97+
super().tearDownClass()
98+
app.conf.update(task_always_eager=cls.celery_task_always_eager)

contentcuration/contentcuration/tests/test_models.py

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from contentcuration.models import UserHistory
3535
from contentcuration.tests import testdata
3636
from contentcuration.tests.base import StudioTestCase
37+
from contentcuration.tests.helpers import EagerTasksTestMixin
3738
from contentcuration.viewsets.sync.constants import DELETED
3839

3940

@@ -550,14 +551,17 @@ def test_make_content_id_unique(self):
550551

551552

552553
@mock.patch(
553-
"contentcuration.utils.publish.ensure_versioned_database_exists", return_value=None
554+
"contentcuration.tasks.ensure_versioned_database_exists_task.fetch_or_enqueue",
555+
return_value=None,
554556
)
555-
class CommunityLibrarySubmissionTestCase(PermissionQuerysetTestCase):
557+
class CommunityLibrarySubmissionTestCase(
558+
EagerTasksTestMixin, PermissionQuerysetTestCase
559+
):
556560
@property
557561
def base_queryset(self):
558562
return CommunityLibrarySubmission.objects.all()
559563

560-
def test_create_submission(self, mock_ensure_db_exists):
564+
def test_create_submission(self, mock_ensure_db_exists_task_fetch_or_enqueue):
561565
# Smoke test
562566
channel = testdata.channel()
563567
author = testdata.user()
@@ -587,20 +591,26 @@ def test_save__author_not_editor(self, mock_ensure_db_exists):
587591
with self.assertRaises(ValidationError):
588592
submission.save()
589593

590-
def test_save__nonpositive_channel_version(self, mock_ensure_db_exists):
594+
def test_save__nonpositive_channel_version(
595+
self, mock_ensure_db_exists_task_fetch_or_enqueue
596+
):
591597
submission = testdata.community_library_submission()
592598
submission.channel_version = 0
593599
with self.assertRaises(ValidationError):
594600
submission.save()
595601

596-
def test_save__matching_channel_version(self, mock_ensure_db_exists):
602+
def test_save__matching_channel_version(
603+
self, mock_ensure_db_exists_task_fetch_or_enqueue
604+
):
597605
submission = testdata.community_library_submission()
598606
submission.channel.version = 5
599607
submission.channel.save()
600608
submission.channel_version = 5
601609
submission.save()
602610

603-
def test_save__impossibly_high_channel_version(self, mock_ensure_db_exists):
611+
def test_save__impossibly_high_channel_version(
612+
self, mock_ensure_db_exists_task_fetch_or_enqueue
613+
):
604614
submission = testdata.community_library_submission()
605615
submission.channel.version = 5
606616
submission.channel.save()
@@ -609,40 +619,50 @@ def test_save__impossibly_high_channel_version(self, mock_ensure_db_exists):
609619
submission.save()
610620

611621
def test_save__ensure_versioned_database_exists_on_create(
612-
self, mock_ensure_db_exists
622+
self, mock_ensure_db_exists_task_fetch_or_enqueue
613623
):
614624
submission = testdata.community_library_submission()
615625

616-
mock_ensure_db_exists.assert_called_once_with(submission.channel)
626+
mock_ensure_db_exists_task_fetch_or_enqueue.assert_called_once_with(
627+
user=submission.author,
628+
channel_id=submission.channel.id,
629+
channel_version=submission.channel.version,
630+
)
617631

618632
def test_save__dont_ensure_versioned_database_exists_on_update(
619-
self, mock_ensure_db_exists
633+
self, mock_ensure_db_exists_task_fetch_or_enqueue
620634
):
621635
submission = testdata.community_library_submission()
622-
mock_ensure_db_exists.reset_mock()
636+
mock_ensure_db_exists_task_fetch_or_enqueue.reset_mock()
623637

624638
submission.description = "Updated description"
625639
submission.save()
626640

627-
mock_ensure_db_exists.assert_not_called()
641+
mock_ensure_db_exists_task_fetch_or_enqueue.assert_not_called()
628642

629-
def test_filter_view_queryset__anonymous(self, mock_ensure_db_exists):
643+
def test_filter_view_queryset__anonymous(
644+
self, mock_ensure_db_exists_task_fetch_or_enqueue
645+
):
630646
_ = testdata.community_library_submission()
631647

632648
queryset = CommunityLibrarySubmission.filter_view_queryset(
633649
self.base_queryset, user=self.anonymous_user
634650
)
635651
self.assertFalse(queryset.exists())
636652

637-
def test_filter_view_queryset__forbidden_user(self, mock_ensure_db_exists):
653+
def test_filter_view_queryset__forbidden_user(
654+
self, mock_ensure_db_exists_task_fetch_or_enqueue
655+
):
638656
_ = testdata.community_library_submission()
639657

640658
queryset = CommunityLibrarySubmission.filter_view_queryset(
641659
self.base_queryset, user=self.forbidden_user
642660
)
643661
self.assertFalse(queryset.exists())
644662

645-
def test_filter_view_queryset__channel_editor(self, mock_ensure_db_exists):
663+
def test_filter_view_queryset__channel_editor(
664+
self, mock_ensure_db_exists_task_fetch_or_enqueue
665+
):
646666
submission_a = testdata.community_library_submission()
647667
submission_b = testdata.community_library_submission()
648668

@@ -656,31 +676,39 @@ def test_filter_view_queryset__channel_editor(self, mock_ensure_db_exists):
656676
self.assertQuerysetContains(queryset, pk=submission_a.id)
657677
self.assertQuerysetDoesNotContain(queryset, pk=submission_b.id)
658678

659-
def test_filter_view_queryset__admin(self, mock_ensure_db_exists):
679+
def test_filter_view_queryset__admin(
680+
self, mock_ensure_db_exists_task_fetch_or_enqueue
681+
):
660682
submission_a = testdata.community_library_submission()
661683

662684
queryset = CommunityLibrarySubmission.filter_view_queryset(
663685
self.base_queryset, user=self.admin_user
664686
)
665687
self.assertQuerysetContains(queryset, pk=submission_a.id)
666688

667-
def test_filter_edit_queryset__anonymous(self, mock_ensure_db_exists):
689+
def test_filter_edit_queryset__anonymous(
690+
self, mock_ensure_db_exists_task_fetch_or_enqueue
691+
):
668692
_ = testdata.community_library_submission()
669693

670694
queryset = CommunityLibrarySubmission.filter_edit_queryset(
671695
self.base_queryset, user=self.anonymous_user
672696
)
673697
self.assertFalse(queryset.exists())
674698

675-
def test_filter_edit_queryset__forbidden_user(self, mock_ensure_db_exists):
699+
def test_filter_edit_queryset__forbidden_user(
700+
self, mock_ensure_db_exists_task_fetch_or_enqueue
701+
):
676702
_ = testdata.community_library_submission()
677703

678704
queryset = CommunityLibrarySubmission.filter_edit_queryset(
679705
self.base_queryset, user=self.forbidden_user
680706
)
681707
self.assertFalse(queryset.exists())
682708

683-
def test_filter_edit_queryset__channel_editor(self, mock_ensure_db_exists):
709+
def test_filter_edit_queryset__channel_editor(
710+
self, mock_ensure_db_exists_task_fetch_or_enqueue
711+
):
684712
submission = testdata.community_library_submission()
685713

686714
user = testdata.user()
@@ -692,7 +720,9 @@ def test_filter_edit_queryset__channel_editor(self, mock_ensure_db_exists):
692720
)
693721
self.assertFalse(queryset.exists())
694722

695-
def test_filter_edit_queryset__author(self, mock_ensure_db_exists):
723+
def test_filter_edit_queryset__author(
724+
self, mock_ensure_db_exists_task_fetch_or_enqueue
725+
):
696726
submission_a = testdata.community_library_submission()
697727
submission_b = testdata.community_library_submission()
698728

@@ -702,15 +732,17 @@ def test_filter_edit_queryset__author(self, mock_ensure_db_exists):
702732
self.assertQuerysetContains(queryset, pk=submission_a.id)
703733
self.assertQuerysetDoesNotContain(queryset, pk=submission_b.id)
704734

705-
def test_filter_edit_queryset__admin(self, mock_ensure_db_exists):
735+
def test_filter_edit_queryset__admin(
736+
self, mock_ensure_db_exists_task_fetch_or_enqueue
737+
):
706738
submission_a = testdata.community_library_submission()
707739

708740
queryset = CommunityLibrarySubmission.filter_edit_queryset(
709741
self.base_queryset, user=self.admin_user
710742
)
711743
self.assertQuerysetContains(queryset, pk=submission_a.id)
712744

713-
def test_mark_live(self, mock_ensure_db_exists):
745+
def test_mark_live(self, mock_ensure_db_exists_task_fetch_or_enqueue):
714746
submission_a = testdata.community_library_submission()
715747
submission_b = testdata.community_library_submission()
716748

contentcuration/contentcuration/tests/viewsets/base.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22

33
from django.urls import reverse
44

5-
from contentcuration.celery import app
65
from contentcuration.models import Change
7-
from contentcuration.tests.helpers import clear_tasks
6+
from contentcuration.tests.helpers import EagerTasksTestMixin
87
from contentcuration.viewsets.sync.constants import CHANNEL
98
from contentcuration.viewsets.sync.constants import SYNCED
109
from contentcuration.viewsets.sync.utils import _generate_event as base_generate_event
@@ -102,25 +101,7 @@ def generate_publish_next_event(channel_id, use_staging_tree=False):
102101
return event
103102

104103

105-
class SyncTestMixin(object):
106-
celery_task_always_eager = None
107-
108-
@classmethod
109-
def setUpClass(cls):
110-
super(SyncTestMixin, cls).setUpClass()
111-
# update celery so tasks are always eager for this test, meaning they'll execute synchronously
112-
cls.celery_task_always_eager = app.conf.task_always_eager
113-
app.conf.update(task_always_eager=True)
114-
115-
def setUp(self):
116-
super(SyncTestMixin, self).setUp()
117-
clear_tasks()
118-
119-
@classmethod
120-
def tearDownClass(cls):
121-
super(SyncTestMixin, cls).tearDownClass()
122-
app.conf.update(task_always_eager=cls.celery_task_always_eager)
123-
104+
class SyncTestMixin(EagerTasksTestMixin):
124105
@property
125106
def sync_url(self):
126107
return reverse("sync")

contentcuration/contentcuration/utils/publish.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,32 +1094,39 @@ def publish_channel( # noqa: C901
10941094
return channel
10951095

10961096

1097-
def ensure_versioned_database_exists(channel):
1097+
def ensure_versioned_database_exists(channel_id, channel_version):
10981098
"""
10991099
Ensures that the versioned database exists, and if not, copies the unversioned database to the versioned path.
11001100
This happens if the channel was published back when versioned databases were not used.
11011101
"""
1102-
if channel.version == 0:
1102+
if channel_version == 0:
11031103
raise ValueError("An unpublished channel cannot have a versioned database.")
11041104

11051105
unversioned_db_storage_path = os.path.join(
1106-
settings.DB_ROOT, "{id}.sqlite3".format(id=channel.id)
1106+
settings.DB_ROOT, "{id}.sqlite3".format(id=channel_id)
11071107
)
11081108
versioned_db_storage_path = os.path.join(
11091109
settings.DB_ROOT,
1110-
"{id}-{version}.sqlite3".format(id=channel.id, version=channel.version),
1110+
"{id}-{version}.sqlite3".format(id=channel_id, version=channel_version),
11111111
)
11121112

11131113
if not storage.exists(versioned_db_storage_path):
11141114
if not storage.exists(unversioned_db_storage_path):
11151115
# This should never happen, a published channel should always have an unversioned database
11161116
raise FileNotFoundError(
1117-
f"Neither unversioned nor versioned database found for channel {channel.id}."
1117+
f"Neither unversioned nor versioned database found for channel {channel_id}."
11181118
)
11191119

1120+
# NOTE: This should not result in a race condition in the case that a newer
1121+
# version of the channel is published before the task running this function
1122+
# is executed. In that case, the publishing logic would have already created
1123+
# the versioned database. The only case where this could be problematic is
1124+
# if this happens between the check above this comment and the commands below
1125+
# it. However, this is EXTREMELY unlikely, and could probably only be solved
1126+
# by introducing a locking mechanism for the database storage objects.
11201127
with storage.open(unversioned_db_storage_path, "rb") as unversioned_db_file:
11211128
storage.save(versioned_db_storage_path, unversioned_db_file)
11221129

11231130
logging.info(
1124-
f"Versioned database for channel {channel.id} did not exist, copied the unversioned database to {versioned_db_storage_path}."
1131+
f"Versioned database for channel {channel_id} did not exist, copied the unversioned database to {versioned_db_storage_path}."
11251132
)

0 commit comments

Comments
 (0)