|
| 1 | +from abc import ABCMeta, abstractmethod |
| 2 | +from inspect import iscoroutinefunction |
| 3 | + |
| 4 | +from asgiref.sync import sync_to_async |
| 5 | + |
| 6 | +from django.conf import settings |
| 7 | +from django.core import checks |
| 8 | +from django.db import connections |
| 9 | +from django.tasks import DEFAULT_TASK_QUEUE_NAME |
| 10 | +from django.tasks.base import ( |
| 11 | + DEFAULT_TASK_PRIORITY, |
| 12 | + TASK_MAX_PRIORITY, |
| 13 | + TASK_MIN_PRIORITY, |
| 14 | + Task, |
| 15 | +) |
| 16 | +from django.tasks.exceptions import InvalidTask |
| 17 | +from django.utils import timezone |
| 18 | +from django.utils.inspect import get_func_args, is_module_level_function |
| 19 | + |
| 20 | + |
| 21 | +class BaseTaskBackend(metaclass=ABCMeta): |
| 22 | + task_class = Task |
| 23 | + |
| 24 | + # Does the backend support Tasks to be enqueued with the run_after |
| 25 | + # attribute? |
| 26 | + supports_defer = False |
| 27 | + |
| 28 | + # Does the backend support coroutines to be enqueued? |
| 29 | + supports_async_task = False |
| 30 | + |
| 31 | + # Does the backend support results being retrieved (from any |
| 32 | + # thread/process)? |
| 33 | + supports_get_result = False |
| 34 | + |
| 35 | + # Does the backend support executing Tasks in a given |
| 36 | + # priority order? |
| 37 | + supports_priority = False |
| 38 | + |
| 39 | + def __init__(self, alias, params): |
| 40 | + self.alias = alias |
| 41 | + self.queues = set(params.get("QUEUES", [DEFAULT_TASK_QUEUE_NAME])) |
| 42 | + self.enqueue_on_commit = bool(params.get("ENQUEUE_ON_COMMIT", True)) |
| 43 | + self.options = params.get("OPTIONS", {}) |
| 44 | + |
| 45 | + def _get_enqueue_on_commit_for_task(self, task): |
| 46 | + return ( |
| 47 | + task.enqueue_on_commit |
| 48 | + if task.enqueue_on_commit is not None |
| 49 | + else self.enqueue_on_commit |
| 50 | + ) |
| 51 | + |
| 52 | + def validate_task(self, task): |
| 53 | + """ |
| 54 | + Determine whether the provided Task can be executed by the backend. |
| 55 | + """ |
| 56 | + if not is_module_level_function(task.func): |
| 57 | + raise InvalidTask("Task function must be defined at a module level.") |
| 58 | + |
| 59 | + if not self.supports_async_task and iscoroutinefunction(task.func): |
| 60 | + raise InvalidTask("Backend does not support async Tasks.") |
| 61 | + |
| 62 | + task_func_args = get_func_args(task.func) |
| 63 | + if task.takes_context and ( |
| 64 | + not task_func_args or task_func_args[0] != "context" |
| 65 | + ): |
| 66 | + raise InvalidTask( |
| 67 | + "Task takes context but does not have a first argument of 'context'." |
| 68 | + ) |
| 69 | + |
| 70 | + if not self.supports_priority and task.priority != DEFAULT_TASK_PRIORITY: |
| 71 | + raise InvalidTask("Backend does not support setting priority of tasks.") |
| 72 | + if ( |
| 73 | + task.priority < TASK_MIN_PRIORITY |
| 74 | + or task.priority > TASK_MAX_PRIORITY |
| 75 | + or int(task.priority) != task.priority |
| 76 | + ): |
| 77 | + raise InvalidTask( |
| 78 | + f"priority must be a whole number between {TASK_MIN_PRIORITY} and " |
| 79 | + f"{TASK_MAX_PRIORITY}." |
| 80 | + ) |
| 81 | + |
| 82 | + if not self.supports_defer and task.run_after is not None: |
| 83 | + raise InvalidTask("Backend does not support run_after.") |
| 84 | + |
| 85 | + if ( |
| 86 | + settings.USE_TZ |
| 87 | + and task.run_after is not None |
| 88 | + and not timezone.is_aware(task.run_after) |
| 89 | + ): |
| 90 | + raise InvalidTask("run_after must be an aware datetime.") |
| 91 | + |
| 92 | + if self.queues and task.queue_name not in self.queues: |
| 93 | + raise InvalidTask(f"Queue '{task.queue_name}' is not valid for backend.") |
| 94 | + |
| 95 | + @abstractmethod |
| 96 | + def enqueue(self, task, args, kwargs): |
| 97 | + """Queue up a task to be executed.""" |
| 98 | + |
| 99 | + async def aenqueue(self, task, args, kwargs): |
| 100 | + """Queue up a task function (or coroutine) to be executed.""" |
| 101 | + return await sync_to_async(self.enqueue, thread_sensitive=True)( |
| 102 | + task=task, args=args, kwargs=kwargs |
| 103 | + ) |
| 104 | + |
| 105 | + def get_result(self, result_id): |
| 106 | + """ |
| 107 | + Retrieve a task result by id. |
| 108 | +
|
| 109 | + Raise TaskResultDoesNotExist if such result does not exist. |
| 110 | + """ |
| 111 | + raise NotImplementedError( |
| 112 | + "This backend does not support retrieving or refreshing results." |
| 113 | + ) |
| 114 | + |
| 115 | + async def aget_result(self, result_id): |
| 116 | + """See get_result().""" |
| 117 | + return await sync_to_async(self.get_result, thread_sensitive=True)( |
| 118 | + result_id=result_id |
| 119 | + ) |
| 120 | + |
| 121 | + def check(self, **kwargs): |
| 122 | + if self.enqueue_on_commit and not connections._settings: |
| 123 | + yield checks.Error( |
| 124 | + "ENQUEUE_ON_COMMIT cannot be used when no databases are configured.", |
| 125 | + hint="Set ENQUEUE_ON_COMMIT to False", |
| 126 | + id="tasks.E001", |
| 127 | + ) |
| 128 | + |
| 129 | + elif ( |
| 130 | + self.enqueue_on_commit |
| 131 | + and not connections["default"].features.supports_transactions |
| 132 | + ): |
| 133 | + yield checks.Error( |
| 134 | + "ENQUEUE_ON_COMMIT cannot be used on a database which doesn't support " |
| 135 | + "transactions.", |
| 136 | + hint="Set ENQUEUE_ON_COMMIT to False", |
| 137 | + id="tasks.E002", |
| 138 | + ) |
0 commit comments