Repository: readthedocs/readthedocs.org

Issue:
move_files tasks depends on the Version from the db and fails
Similar to #4789, we pass the `Version.pk` to the `move_files` task. If the version is removed before the task is executed, the task fails because the row is not in the database anymore.

The problem is at this line,

https://github.com/rtfd/readthedocs.org/blob/3fbb0c1a7c94bcc979f54d67fad8dc94f74854c8/readthedocs/projects/tasks.py#L838

We should handle this exception properly and do not fail the task. We need to think this a little more, but I suppose that if there is no version in the database, there is nothing to move or do at that point. So, I think that just adding a `try/except` block with an `log.warning` will do the trick.

This happens frequently: https://sentry.io/read-the-docs/readthedocs-org/issues/235379529/?query=is:unresolved


Discussion:
@humitos Would it be okay if i take this issue?

I think you want something like this

```python3
try:
    version = Version.objects.get(pk=version_pk)
except Version.DoesNotExist:
    log.warning(f'Version with pk {version_pk} does not exist')
```
I just realize that this also happens at 

https://github.com/rtfd/readthedocs.org/blob/dfc8fc9eba8dc9caae171ca0b3e8f6a71594e088/readthedocs/projects/tasks.py#L1085-L1087

probably for the same reason that the Version/Project is deleted manually by the user.

We should think about a _generic way_ of handling this from all the tasks that depend on objects being on the database.
@humitos Probably we can implement custom `get()` method in the model manager of `Version`. I found it here
https://github.com/rtfd/readthedocs.org/blob/9b2b17c0fc603267b4b20f5923862c2db82602da/readthedocs/builds/managers.py#L17

in this, we can write our own `get()` method which may probably look something like this

```python3
from django.core.exceptions import ObjectDoesNotExist

def get(self, **kwargs):
    try:
        return super().get(**kwargs)
    except ObjectDoesNotExist:
        # warning or something else
```
Sounds good to me.

We may have to name it different, though. I'd like to be more explicit on the name for the use case: `get_object_from_task` or `get_object_or_log`. I'm not sure, but something like that.

We will also need to handle when the query returns `None` from the task, so we stop the task there without failing.
`get_object_or_log` sounds good to me.
I am not sure about what you meant from **stop the task**, do you mean to `return None` from the task like this: 
```python3
@app.task(queue='web')
def task_name(version_pk, *args, **kwargs):
    # code
    version = Version.objects.get_object_or_log(pk=version_pk)
    if not version:
        return None
    # code
```

Resolving patch:
diff --git a/readthedocs/builds/managers.py b/readthedocs/builds/managers.py
--- a/readthedocs/builds/managers.py
+++ b/readthedocs/builds/managers.py
@@ -2,7 +2,10 @@
 
 """Build and Version class model Managers."""
 
+import logging
+
 from django.db import models
+from django.core.exceptions import ObjectDoesNotExist
 
 from readthedocs.core.utils.extend import (
     SettingsOverrideObject,
@@ -19,6 +22,8 @@
 )
 from .querysets import VersionQuerySet
 
+log = logging.getLogger(__name__)
+
 
 __all__ = ['VersionManager']
 
@@ -67,6 +72,18 @@ def create_latest(self, **kwargs):
         defaults.update(kwargs)
         return self.create(**defaults)
 
+    def get_object_or_log(self, **kwargs):
+        """
+        Returns Version object or log.
+
+        It will return the Version object if found for the given kwargs,
+        otherwise it will log a warning along with all provided kwargs.
+        """
+        try:
+            return super().get(**kwargs)
+        except ObjectDoesNotExist:
+            log.warning('Version not found for given kwargs. %s' % kwargs)
+
 
 class VersionManager(SettingsOverrideObject):
     _default_class = VersionManagerBase
diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py
--- a/readthedocs/projects/tasks.py
+++ b/readthedocs/projects/tasks.py
@@ -899,7 +899,9 @@ def sync_files(
     synchronization of build artifacts on each application instance.
     """
     # Clean up unused artifacts
-    version = Version.objects.get(pk=version_pk)
+    version = Version.objects.get_object_or_log(pk=version_pk)
+    if not version:
+        return
     if not pdf:
         remove_dirs([
             version.project.get_production_media_path(
@@ -959,7 +961,9 @@ def move_files(
     :param epub: Sync ePub files
     :type epub: bool
     """
-    version = Version.objects.get(pk=version_pk)
+    version = Version.objects.get_object_or_log(pk=version_pk)
+    if not version:
+        return
     log.debug(
         LOG_TEMPLATE.format(
             project=version.project.slug,
@@ -1035,7 +1039,9 @@ def update_search(version_pk, commit, delete_non_commit_files=True):
     :param commit: Commit that updated index
     :param delete_non_commit_files: Delete files not in commit from index
     """
-    version = Version.objects.get(pk=version_pk)
+    version = Version.objects.get_object_or_log(pk=version_pk)
+    if not version:
+        return
 
     page_list = process_all_json_files(version, build_dir=False)
 
@@ -1130,7 +1136,9 @@ def fileify(version_pk, commit):
 
     This is so we have an idea of what files we have in the database.
     """
-    version = Version.objects.get(pk=version_pk)
+    version = Version.objects.get_object_or_log(pk=version_pk)
+    if not version:
+        return
     project = version.project
 
     if not commit:
@@ -1220,7 +1228,9 @@ def _manage_imported_files(version, path, commit):
 
 @app.task(queue='web')
 def send_notifications(version_pk, build_pk):
-    version = Version.objects.get(pk=version_pk)
+    version = Version.objects.get_object_or_log(pk=version_pk)
+    if not version:
+        return
     build = Build.objects.get(pk=build_pk)
 
     for hook in version.project.webhook_notifications.all():


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_build_notifications.py b/readthedocs/rtd_tests/tests/test_build_notifications.py
--- a/readthedocs/rtd_tests/tests/test_build_notifications.py
+++ b/readthedocs/rtd_tests/tests/test_build_notifications.py
@@ -17,6 +17,13 @@ def setUp(self):
         self.project = fixture.get(Project)
         self.version = fixture.get(Version, project=self.project)
         self.build = fixture.get(Build, version=self.version)
+    
+    @patch('readthedocs.builds.managers.log')
+    def test_send_notification_none_if_wrong_version_pk(self, mock_logger):
+        self.assertFalse(Version.objects.filter(pk=345343).exists())
+        send_notifications(version_pk=345343, build_pk=None)
+        mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")
+
 
     def test_send_notification_none(self):
         send_notifications(self.version.pk, self.build.pk)
diff --git a/readthedocs/rtd_tests/tests/test_celery.py b/readthedocs/rtd_tests/tests/test_celery.py
--- a/readthedocs/rtd_tests/tests/test_celery.py
+++ b/readthedocs/rtd_tests/tests/test_celery.py
@@ -12,6 +12,7 @@
 from readthedocs.builds.models import Build
 from readthedocs.doc_builder.exceptions import VersionLockedError
 from readthedocs.projects import tasks
+from readthedocs.builds.models import Version
 from readthedocs.projects.exceptions import RepositoryError
 from readthedocs.projects.models import Project
 from readthedocs.rtd_tests.base import RTDTestCase
@@ -250,3 +251,27 @@ def public_task_exception():
                 'error': 'Something bad happened',
             },
         )
+
+    @patch('readthedocs.builds.managers.log')
+    def test_sync_files_logging_when_wrong_version_pk(self, mock_logger):
+        self.assertFalse(Version.objects.filter(pk=345343).exists())
+        tasks.sync_files(project_pk=None, version_pk=345343)
+        mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")
+
+    @patch('readthedocs.builds.managers.log')
+    def test_move_files_logging_when_wrong_version_pk(self, mock_logger):
+        self.assertFalse(Version.objects.filter(pk=345343).exists())
+        tasks.move_files(version_pk=345343, hostname=None)
+        mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")
+
+    @patch('readthedocs.builds.managers.log')
+    def test_update_search_logging_when_wrong_version_pk(self, mock_logger):
+        self.assertFalse(Version.objects.filter(pk=345343).exists())
+        tasks.update_search(version_pk=345343, commit=None)
+        mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")
+
+    @patch('readthedocs.builds.managers.log')
+    def test_fileify_logging_when_wrong_version_pk(self, mock_logger):
+        self.assertFalse(Version.objects.filter(pk=345343).exists())
+        tasks.fileify(version_pk=345343, commit=None)
+        mock_logger.warning.assert_called_with("Version not found for given kwargs. {'pk': 345343}")