pulpcore 3.89.1__py3-none-any.whl → 3.90.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pulpcore might be problematic. Click here for more details.

Files changed (42) hide show
  1. pulp_certguard/app/__init__.py +1 -1
  2. pulp_file/app/__init__.py +1 -1
  3. pulp_file/tests/functional/api/test_filesystem_export.py +220 -0
  4. pulp_file/tests/functional/api/test_pulp_export.py +103 -3
  5. pulpcore/app/apps.py +1 -1
  6. pulpcore/app/importexport.py +18 -2
  7. pulpcore/app/management/commands/shell.py +8 -0
  8. pulpcore/app/migrations/0144_delete_old_appstatus.py +28 -0
  9. pulpcore/app/migrations/0145_domainize_import_export.py +53 -0
  10. pulpcore/app/modelresource.py +61 -21
  11. pulpcore/app/models/__init__.py +2 -5
  12. pulpcore/app/models/exporter.py +7 -1
  13. pulpcore/app/models/fields.py +0 -1
  14. pulpcore/app/models/importer.py +8 -1
  15. pulpcore/app/models/repository.py +16 -0
  16. pulpcore/app/models/status.py +8 -138
  17. pulpcore/app/models/task.py +15 -25
  18. pulpcore/app/serializers/domain.py +1 -1
  19. pulpcore/app/serializers/exporter.py +4 -4
  20. pulpcore/app/serializers/importer.py +2 -2
  21. pulpcore/app/serializers/task.py +11 -8
  22. pulpcore/app/tasks/importer.py +44 -10
  23. pulpcore/app/tasks/repository.py +27 -0
  24. pulpcore/app/viewsets/base.py +18 -14
  25. pulpcore/app/viewsets/domain.py +1 -1
  26. pulpcore/app/viewsets/exporter.py +1 -8
  27. pulpcore/app/viewsets/importer.py +1 -6
  28. pulpcore/app/viewsets/task.py +0 -1
  29. pulpcore/openapi/__init__.py +16 -2
  30. pulpcore/plugin/tasking.py +4 -2
  31. pulpcore/tasking/tasks.py +245 -127
  32. pulpcore/tasking/worker.py +6 -17
  33. pulpcore/tests/functional/api/test_crud_domains.py +7 -0
  34. pulpcore/tests/functional/api/test_tasking.py +2 -2
  35. pulpcore/tests/functional/api/using_plugin/test_crud_repos.py +9 -2
  36. pulpcore/tests/unit/content/test_handler.py +43 -0
  37. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/METADATA +7 -7
  38. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/RECORD +42 -38
  39. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/WHEEL +0 -0
  40. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/entry_points.txt +0 -0
  41. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/licenses/LICENSE +0 -0
  42. {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,7 @@ from rest_framework import viewsets
13
13
  from rest_framework.decorators import action
14
14
  from rest_framework.generics import get_object_or_404
15
15
  from rest_framework.response import Response
16
- from pulpcore.openapi import PulpAutoSchema
16
+ from pulpcore.openapi import PulpAutoSchema, InheritSerializer
17
17
  from rest_framework.serializers import ValidationError as DRFValidationError, ListField, CharField
18
18
 
19
19
  from pulpcore.app import tasks
@@ -482,27 +482,31 @@ class AsyncUpdateMixin(AsyncReservedObjectMixin):
482
482
  ALLOW_NON_BLOCKING_UPDATE = True
483
483
 
484
484
  @extend_schema(
485
- description="Trigger an asynchronous update task",
486
- responses={202: AsyncOperationResponseSerializer},
485
+ description="Update the entity and trigger an asynchronous task if necessary",
486
+ responses={200: InheritSerializer, 202: AsyncOperationResponseSerializer},
487
487
  )
488
488
  def update(self, request, pk, **kwargs):
489
489
  partial = kwargs.pop("partial", False)
490
490
  instance = self.get_object()
491
491
  serializer = self.get_serializer(instance, data=request.data, partial=partial)
492
492
  serializer.is_valid(raise_exception=True)
493
- app_label = instance._meta.app_label
494
- task = dispatch(
495
- tasks.base.ageneral_update,
496
- exclusive_resources=self.async_reserved_resources(instance),
497
- args=(pk, app_label, serializer.__class__.__name__),
498
- kwargs={"data": request.data, "partial": partial},
499
- immediate=self.ALLOW_NON_BLOCKING_UPDATE,
500
- )
501
- return OperationPostponedResponse(task, request)
493
+
494
+ if all(getattr(instance, key) == value for key, value in serializer.validated_data.items()):
495
+ return Response(serializer.data)
496
+ else:
497
+ app_label = instance._meta.app_label
498
+ task = dispatch(
499
+ tasks.base.ageneral_update,
500
+ exclusive_resources=self.async_reserved_resources(instance),
501
+ args=(pk, app_label, serializer.__class__.__name__),
502
+ kwargs={"data": request.data, "partial": partial},
503
+ immediate=self.ALLOW_NON_BLOCKING_UPDATE,
504
+ )
505
+ return OperationPostponedResponse(task, request)
502
506
 
503
507
  @extend_schema(
504
- description="Trigger an asynchronous partial update task",
505
- responses={202: AsyncOperationResponseSerializer},
508
+ description="Update the entity partially and trigger an asynchronous task if necessary",
509
+ responses={200: InheritSerializer, 202: AsyncOperationResponseSerializer},
506
510
  )
507
511
  def partial_update(self, request, *args, **kwargs):
508
512
  kwargs["partial"] = True
@@ -109,7 +109,7 @@ class DomainViewSet(
109
109
 
110
110
  @extend_schema(
111
111
  description="Trigger an asynchronous update task",
112
- responses={202: AsyncOperationResponseSerializer},
112
+ responses={200: DomainSerializer, 202: AsyncOperationResponseSerializer},
113
113
  )
114
114
  def update(self, request, pk, **kwargs):
115
115
  """Prevent trying to update the default domain."""
@@ -1,6 +1,5 @@
1
- from django.conf import settings
2
1
  from drf_spectacular.utils import extend_schema
3
- from rest_framework import mixins, exceptions
2
+ from rest_framework import mixins
4
3
 
5
4
  from pulpcore.app.models import (
6
5
  Export,
@@ -34,8 +33,6 @@ from pulpcore.app.viewsets.base import NAME_FILTER_OPTIONS
34
33
  from pulpcore.plugin.tasking import dispatch
35
34
  from pulpcore.app.response import OperationPostponedResponse
36
35
 
37
- from gettext import gettext as _
38
-
39
36
 
40
37
  class ExporterViewSet(
41
38
  NamedModelViewSet,
@@ -120,8 +117,6 @@ class PulpExportViewSet(ExportViewSet):
120
117
  """
121
118
  Generates a Task to export the set of repositories assigned to a specific PulpExporter.
122
119
  """
123
- if settings.DOMAIN_ENABLED:
124
- raise exceptions.ValidationError(_("Export not supported with Domains enabled."))
125
120
  # Validate Exporter
126
121
  exporter = PulpExporter.objects.get(pk=exporter_pk).cast()
127
122
  ExporterSerializer.validate_path(exporter.path, check_is_dir=True)
@@ -159,8 +154,6 @@ class FilesystemExportViewSet(ExportViewSet):
159
154
  """
160
155
  Generates a Task to export files to the filesystem.
161
156
  """
162
- if settings.DOMAIN_ENABLED:
163
- raise exceptions.ValidationError(_("Export not supported with Domains enabled."))
164
157
  # Validate Exporter
165
158
  exporter = FilesystemExporter.objects.get(pk=exporter_pk).cast()
166
159
  ExporterSerializer.validate_path(exporter.path, check_is_dir=True)
@@ -1,7 +1,6 @@
1
- from django.conf import settings
2
1
  from django.http import Http404
3
2
  from drf_spectacular.utils import extend_schema
4
- from rest_framework import mixins, exceptions
3
+ from rest_framework import mixins
5
4
 
6
5
  from pulpcore.app.models import (
7
6
  Import,
@@ -25,8 +24,6 @@ from pulpcore.app.viewsets import (
25
24
  from pulpcore.app.viewsets.base import NAME_FILTER_OPTIONS
26
25
  from pulpcore.tasking.tasks import dispatch
27
26
 
28
- from gettext import gettext as _
29
-
30
27
 
31
28
  class ImporterViewSet(
32
29
  NamedModelViewSet,
@@ -87,8 +84,6 @@ class PulpImportViewSet(ImportViewSet):
87
84
  )
88
85
  def create(self, request, importer_pk):
89
86
  """Import a Pulp export into Pulp."""
90
- if settings.DOMAIN_ENABLED:
91
- raise exceptions.ValidationError(_("Import not supported with Domains enabled."))
92
87
  try:
93
88
  importer = PulpImporter.objects.get(pk=importer_pk)
94
89
  except PulpImporter.DoesNotExist:
@@ -68,7 +68,6 @@ class TaskFilter(BaseFilterSet):
68
68
  model = Task
69
69
  fields = {
70
70
  "state": ["exact", "in", "ne"],
71
- "worker": ["exact", "in", "isnull"],
72
71
  "name": ["exact", "contains", "in", "ne"],
73
72
  "logging_cid": ["exact", "contains"],
74
73
  "pulp_created": DATETIME_FILTER_OPTIONS,
@@ -23,8 +23,8 @@ from drf_spectacular.plumbing import (
23
23
  )
24
24
  from drf_spectacular.settings import spectacular_settings
25
25
  from drf_spectacular.types import OpenApiTypes
26
- from drf_spectacular.utils import OpenApiParameter, extend_schema_field
27
- from drf_spectacular.extensions import OpenApiAuthenticationExtension
26
+ from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field
27
+ from drf_spectacular.extensions import OpenApiViewExtension, OpenApiAuthenticationExtension
28
28
  from rest_framework import mixins, serializers
29
29
  from rest_framework.exceptions import ParseError
30
30
  from rest_framework.request import Request
@@ -48,6 +48,10 @@ API_ROOT_NO_FRONT_SLASH = API_ROOT_NO_FRONT_SLASH.replace("<", "{").replace(">",
48
48
  extend_schema_field(OpenApiTypes.INT64)(serializers.IntegerField)
49
49
 
50
50
 
51
+ class InheritSerializer(serializers.Serializer):
52
+ """This is a dummy tracer to allow mixins to refer to the natural serializer of a viewset."""
53
+
54
+
51
55
  class PulpAutoSchema(AutoSchema):
52
56
  """Pulp Auto Schema."""
53
57
 
@@ -238,6 +242,7 @@ class PulpAutoSchema(AutoSchema):
238
242
  """
239
243
  Handle response status code.
240
244
  """
245
+ # DRF handles this most of the time. But it seems set_label still needs it.
241
246
  response = super()._get_response_bodies()
242
247
  if (
243
248
  self.method == "POST"
@@ -248,6 +253,15 @@ class PulpAutoSchema(AutoSchema):
248
253
 
249
254
  return response
250
255
 
256
+ def _get_response_for_code(
257
+ self, serializer, status_code, media_types=None, direction="response"
258
+ ):
259
+ """Hack to replace the InheritSerializer with the real deal."""
260
+ if serializer == InheritSerializer:
261
+ serializer = self._get_serializer()
262
+
263
+ return super()._get_response_for_code(serializer, status_code, media_types, direction)
264
+
251
265
 
252
266
  class PulpSchemaGenerator(SchemaGenerator):
253
267
  """Pulp Schema Generator."""
@@ -1,5 +1,5 @@
1
1
  # Support plugins dispatching tasks
2
- from pulpcore.tasking.tasks import dispatch
2
+ from pulpcore.tasking.tasks import dispatch, adispatch
3
3
 
4
4
  from pulpcore.app.tasks import (
5
5
  ageneral_update,
@@ -13,13 +13,14 @@ from pulpcore.app.tasks import (
13
13
  reclaim_space,
14
14
  )
15
15
  from pulpcore.app.tasks.vulnerability_report import check_content
16
- from pulpcore.app.tasks.repository import add_and_remove
16
+ from pulpcore.app.tasks.repository import add_and_remove, aadd_and_remove
17
17
 
18
18
 
19
19
  __all__ = [
20
20
  "ageneral_update",
21
21
  "check_content",
22
22
  "dispatch",
23
+ "adispatch",
23
24
  "fs_publication_export",
24
25
  "fs_repo_version_export",
25
26
  "general_create",
@@ -29,4 +30,5 @@ __all__ = [
29
30
  "orphan_cleanup",
30
31
  "reclaim_space",
31
32
  "add_and_remove",
33
+ "aadd_and_remove",
32
34
  ]
pulpcore/tasking/tasks.py CHANGED
@@ -6,8 +6,9 @@ import os
6
6
  import sys
7
7
  import traceback
8
8
  import tempfile
9
- import threading
10
9
  from gettext import gettext as _
10
+ from contextlib import contextmanager
11
+ from asgiref.sync import sync_to_async, async_to_sync
11
12
 
12
13
  from django.conf import settings
13
14
  from django.db import connection
@@ -60,91 +61,129 @@ def execute_task(task):
60
61
  contextvars.copy_context().run(_execute_task, task)
61
62
 
62
63
 
64
+ async def aexecute_task(task):
65
+ # This extra stack is needed to isolate the current_task ContextVar
66
+ await contextvars.copy_context().run(_aexecute_task, task)
67
+
68
+
63
69
  def _execute_task(task):
64
70
  # Store the task id in the context for `Task.current()`.
65
71
  current_task.set(task)
66
72
  task.set_running()
67
73
  domain = get_domain()
68
74
  try:
69
- _logger.info(
70
- "Starting task id: %s in domain: %s, task_type: %s, immediate: %s, deferred: %s",
71
- task.pk,
72
- domain.name,
73
- task.name,
74
- str(task.immediate),
75
- str(task.deferred),
76
- )
77
-
78
- # Execute task
79
- module_name, function_name = task.name.rsplit(".", 1)
80
- module = importlib.import_module(module_name)
81
- func = getattr(module, function_name)
82
- args = task.enc_args or ()
83
- kwargs = task.enc_kwargs or {}
84
- immediate = task.immediate
85
- is_coroutine_fn = asyncio.iscoroutinefunction(func)
86
-
87
- if immediate and not is_coroutine_fn:
88
- raise ValueError("Immediate tasks must be async functions.")
89
-
90
- if is_coroutine_fn:
91
- # both regular and immediate tasks can be coroutines, but only immediate must timeout
92
- _logger.debug("Task is coroutine %s", task.pk)
93
- coro = func(*args, **kwargs)
94
- if immediate:
95
- coro = asyncio.wait_for(coro, timeout=IMMEDIATE_TIMEOUT)
96
- loop = asyncio.get_event_loop()
97
- try:
98
- result = loop.run_until_complete(coro)
99
- except asyncio.TimeoutError:
100
- _logger.info(
101
- "Immediate task %s timed out after %s seconds.", task.pk, IMMEDIATE_TIMEOUT
102
- )
103
- raise RuntimeError(
104
- "Immediate task timed out after {timeout} seconds.".format(
105
- timeout=IMMEDIATE_TIMEOUT,
106
- )
107
- )
108
- else:
109
- result = func(*args, **kwargs)
110
-
75
+ log_task_start(task, domain)
76
+ task_function = get_task_function(task)
77
+ result = task_function()
111
78
  except Exception:
112
79
  exc_type, exc, tb = sys.exc_info()
113
80
  task.set_failed(exc, tb)
114
- _logger.info(
115
- "Task[{task_type}] {task_pk} failed ({exc_type}: {exc}) in domain: {domain}".format(
116
- task_type=task.name,
117
- task_pk=task.pk,
118
- exc_type=exc_type.__name__,
119
- exc=exc,
120
- domain=domain.name,
121
- )
122
- )
123
- _logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb))))
81
+ log_task_failed(task, exc_type, exc, tb, domain)
124
82
  send_task_notification(task)
125
83
  else:
126
84
  task.set_completed(result)
127
- execution_time = task.finished_at - task.started_at
128
- execution_time_us = int(execution_time.total_seconds() * 1_000_000) # μs
129
- _logger.info(
130
- "Task completed %s in domain:"
131
- " %s, task_type: %s, immediate: %s, deferred: %s, execution_time: %s μs",
132
- task.pk,
133
- domain.name,
134
- task.name,
135
- str(task.immediate),
136
- str(task.deferred),
137
- execution_time_us,
138
- )
85
+ log_task_completed(task, domain)
139
86
  send_task_notification(task)
87
+ return result
88
+ return None
140
89
 
141
90
 
142
- def running_from_thread_pool() -> bool:
143
- # TODO: this needs an alternative approach ASAP!
144
- # Currently we rely on the weak fact that ThreadPoolExecutor names threads like:
145
- # "ThreadPoolExecutor-0_0"
146
- thread_name = threading.current_thread().name
147
- return "ThreadPoolExecutor" in thread_name
91
+ async def _aexecute_task(task):
92
+ # Store the task id in the context for `Task.current()`.
93
+ current_task.set(task)
94
+ await sync_to_async(task.set_running)()
95
+ domain = get_domain()
96
+ try:
97
+ coroutine = get_task_function(task, ensure_coroutine=True)
98
+ result = await coroutine
99
+ except Exception:
100
+ exc_type, exc, tb = sys.exc_info()
101
+ await sync_to_async(task.set_failed)(exc, tb)
102
+ log_task_failed(task, exc_type, exc, tb, domain)
103
+ send_task_notification(task)
104
+ else:
105
+ await sync_to_async(task.set_completed)(result)
106
+ send_task_notification(task)
107
+ log_task_completed(task, domain)
108
+ return result
109
+ return None
110
+
111
+
112
+ def log_task_start(task, domain):
113
+ _logger.info(
114
+ "Starting task id: %s in domain: %s, task_type: %s, immediate: %s, deferred: %s",
115
+ task.pk,
116
+ domain.name,
117
+ task.name,
118
+ str(task.immediate),
119
+ str(task.deferred),
120
+ )
121
+
122
+
123
+ def log_task_completed(task, domain):
124
+ execution_time = task.finished_at - task.started_at
125
+ execution_time_us = int(execution_time.total_seconds() * 1_000_000) # μs
126
+ _logger.info(
127
+ "Task completed %s in domain:"
128
+ " %s, task_type: %s, immediate: %s, deferred: %s, execution_time: %s μs",
129
+ task.pk,
130
+ domain.name,
131
+ task.name,
132
+ str(task.immediate),
133
+ str(task.deferred),
134
+ execution_time_us,
135
+ )
136
+
137
+
138
+ def log_task_failed(task, exc_type, exc, tb, domain):
139
+ _logger.info(
140
+ "Task[{task_type}] {task_pk} failed ({exc_type}: {exc}) in domain: {domain}".format(
141
+ task_type=task.name,
142
+ task_pk=task.pk,
143
+ exc_type=exc_type.__name__,
144
+ exc=exc,
145
+ domain=domain.name,
146
+ )
147
+ )
148
+ _logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb))))
149
+
150
+
151
+ def get_task_function(task, ensure_coroutine=False):
152
+ module_name, function_name = task.name.rsplit(".", 1)
153
+ module = importlib.import_module(module_name)
154
+ func = getattr(module, function_name)
155
+ args = task.enc_args or ()
156
+ kwargs = task.enc_kwargs or {}
157
+ immediate = task.immediate
158
+ is_coroutine_fn = asyncio.iscoroutinefunction(func)
159
+
160
+ if immediate and not is_coroutine_fn:
161
+ raise ValueError("Immediate tasks must be async functions.")
162
+
163
+ if ensure_coroutine:
164
+ if not is_coroutine_fn:
165
+ return sync_to_async(func)(*args, **kwargs)
166
+ coro = func(*args, **kwargs)
167
+ if immediate:
168
+ coro = asyncio.wait_for(coro, timeout=IMMEDIATE_TIMEOUT)
169
+ return coro
170
+ else: # ensure normal function
171
+ if not is_coroutine_fn:
172
+ return lambda: func(*args, **kwargs)
173
+
174
+ async def task_wrapper(): # asyncio.wait_for + async_to_sync requires wrapping
175
+ coro = func(*args, **kwargs)
176
+ if immediate:
177
+ coro = asyncio.wait_for(coro, timeout=IMMEDIATE_TIMEOUT)
178
+ try:
179
+ return await coro
180
+ except asyncio.TimeoutError:
181
+ msg_template = "Immediate task %s timed out after %s seconds."
182
+ error_msg = msg_template % (task.pk, IMMEDIATE_TIMEOUT)
183
+ _logger.info(error_msg)
184
+ raise RuntimeError(error_msg)
185
+
186
+ return async_to_sync(task_wrapper)
148
187
 
149
188
 
150
189
  def dispatch(
@@ -194,18 +233,148 @@ def dispatch(
194
233
  ValueError: When `resources` is an unsupported type.
195
234
  """
196
235
 
197
- # Can't run short tasks immediately if running from thread pool
198
- immediate = immediate and not running_from_thread_pool()
236
+ execute_now = immediate and not called_from_content_app()
199
237
  assert deferred or immediate, "A task must be at least `deferred` or `immediate`."
238
+ send_wakeup_signal = True if not immediate else False
239
+ function_name = get_function_name(func)
240
+ versions = get_version(versions, function_name)
241
+ colliding_resources, resources = get_resources(exclusive_resources, shared_resources, immediate)
242
+ task_payload = get_task_payload(
243
+ function_name, task_group, args, kwargs, resources, versions, immediate, deferred
244
+ )
245
+ task = Task.objects.create(**task_payload)
246
+ task.refresh_from_db() # The database will have assigned a timestamp for us.
247
+ if execute_now:
248
+ if are_resources_available(colliding_resources, task):
249
+ send_wakeup_signal = True if resources else False
250
+ task.unblock()
251
+ with using_workdir():
252
+ execute_task(task)
253
+ elif deferred: # Resources are blocked and can be deferred
254
+ task.app_lock = None
255
+ task.save()
256
+ else: # Can't be deferred
257
+ task.set_canceling()
258
+ task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
259
+ if send_wakeup_signal:
260
+ wakeup_worker(TASK_WAKEUP_UNBLOCK)
261
+ return task
262
+
263
+
264
+ async def adispatch(
265
+ func,
266
+ args=None,
267
+ kwargs=None,
268
+ task_group=None,
269
+ exclusive_resources=None,
270
+ shared_resources=None,
271
+ immediate=False,
272
+ deferred=True,
273
+ versions=None,
274
+ ):
275
+ """Async version of dispatch."""
276
+ execute_now = immediate and not called_from_content_app()
277
+ assert deferred or immediate, "A task must be at least `deferred` or `immediate`."
278
+ function_name = get_function_name(func)
279
+ versions = get_version(versions, function_name)
280
+ colliding_resources, resources = get_resources(exclusive_resources, shared_resources, immediate)
281
+ send_wakeup_signal = False
282
+ task_payload = get_task_payload(
283
+ function_name, task_group, args, kwargs, resources, versions, immediate, deferred
284
+ )
285
+ task = await Task.objects.acreate(**task_payload)
286
+ await task.arefresh_from_db() # The database will have assigned a timestamp for us.
287
+ if execute_now:
288
+ if await async_are_resources_available(colliding_resources, task):
289
+ send_wakeup_signal = True if resources else False
290
+ await task.aunblock()
291
+ with using_workdir():
292
+ await aexecute_task(task)
293
+ elif deferred: # Resources are blocked and can be deferred
294
+ task.app_lock = None
295
+ await task.asave()
296
+ else: # Can't be deferred
297
+ task.set_canceling()
298
+ task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
299
+ if send_wakeup_signal:
300
+ wakeup_worker(TASK_WAKEUP_UNBLOCK)
301
+ return task
302
+
303
+
304
+ def get_task_payload(
305
+ function_name, task_group, args, kwargs, resources, versions, immediate, deferred
306
+ ):
307
+ payload = {
308
+ "state": TASK_STATES.WAITING,
309
+ "logging_cid": (get_guid()),
310
+ "task_group": task_group,
311
+ "name": function_name,
312
+ "enc_args": args,
313
+ "enc_kwargs": kwargs,
314
+ "parent_task": Task.current(),
315
+ "reserved_resources_record": resources,
316
+ "versions": versions,
317
+ "immediate": immediate,
318
+ "deferred": deferred,
319
+ "profile_options": x_task_diagnostics_var.get(None),
320
+ "app_lock": None if not immediate else AppStatus.objects.current(), # Lazy evaluation...
321
+ }
322
+ return payload
323
+
324
+
325
+ @contextmanager
326
+ def using_workdir():
327
+ cur_dir = os.getcwd()
328
+ with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as working_dir:
329
+ os.chdir(working_dir)
330
+ try:
331
+ yield
332
+ finally:
333
+ # Whether the task fails or not, we should always restore the workdir.
334
+ os.chdir(cur_dir)
335
+
200
336
 
337
+ async def async_are_resources_available(colliding_resources, task: Task) -> bool:
338
+ prior_tasks = Task.objects.filter(
339
+ state__in=TASK_INCOMPLETE_STATES, pulp_created__lt=task.pulp_created
340
+ )
341
+ colliding_resources_taken = await prior_tasks.filter(
342
+ reserved_resources_record__overlap=colliding_resources
343
+ ).aexists()
344
+ return not colliding_resources or not colliding_resources_taken
345
+
346
+
347
+ def are_resources_available(colliding_resources, task: Task) -> bool:
348
+ prior_tasks = Task.objects.filter(
349
+ state__in=TASK_INCOMPLETE_STATES, pulp_created__lt=task.pulp_created
350
+ )
351
+ colliding_resources_taken = prior_tasks.filter(
352
+ reserved_resources_record__overlap=colliding_resources
353
+ ).exists()
354
+ return not colliding_resources or not colliding_resources_taken
355
+
356
+
357
+ def called_from_content_app() -> bool:
358
+ current_app = AppStatus.objects.current()
359
+ return current_app is not None and current_app.app_type == "content"
360
+
361
+
362
+ def get_function_name(func):
201
363
  if callable(func):
202
364
  function_name = f"{func.__module__}.{func.__name__}"
203
365
  else:
204
366
  function_name = func
367
+ return function_name
368
+
205
369
 
370
+ def get_version(versions, function_name):
206
371
  if versions is None:
207
372
  versions = MODULE_PLUGIN_VERSIONS[function_name.split(".", maxsplit=1)[0]]
373
+ return versions
374
+
208
375
 
376
+ def get_resources(exclusive_resources, shared_resources, immediate):
377
+ domain_prn = get_prn(get_domain())
209
378
  if exclusive_resources is None:
210
379
  exclusive_resources = []
211
380
  else:
@@ -216,70 +385,19 @@ def dispatch(
216
385
  shared_resources = _validate_and_get_resources(shared_resources)
217
386
 
218
387
  # A task that is exclusive on a domain will block all tasks within that domain
219
- domain_prn = get_prn(get_domain())
220
388
  if domain_prn not in exclusive_resources:
221
389
  shared_resources.append(domain_prn)
222
390
  resources = exclusive_resources + [f"shared:{resource}" for resource in shared_resources]
223
391
 
224
- notify_workers = False
225
- task = Task.objects.create(
226
- state=TASK_STATES.WAITING,
227
- logging_cid=(get_guid()),
228
- task_group=task_group,
229
- name=function_name,
230
- enc_args=args,
231
- enc_kwargs=kwargs,
232
- parent_task=Task.current(),
233
- reserved_resources_record=resources,
234
- versions=versions,
235
- immediate=immediate,
236
- deferred=deferred,
237
- profile_options=x_task_diagnostics_var.get(None),
238
- app_lock=None if not immediate else AppStatus.objects.current(), # Lazy evaluation...
239
- )
240
- task.refresh_from_db() # The database will have assigned a timestamp for us.
392
+ # Compile a list of resources that must not be taken by other tasks.
393
+ colliding_resources = []
241
394
  if immediate:
242
- prior_tasks = Task.objects.filter(
243
- state__in=TASK_INCOMPLETE_STATES, pulp_created__lt=task.pulp_created
244
- )
245
- # Compile a list of resources that must not be taken by other tasks.
246
395
  colliding_resources = (
247
396
  shared_resources
248
397
  + exclusive_resources
249
398
  + [f"shared:{resource}" for resource in exclusive_resources]
250
399
  )
251
- # Can we execute this task immediately?
252
- if (
253
- not colliding_resources
254
- or not prior_tasks.filter(
255
- reserved_resources_record__overlap=colliding_resources
256
- ).exists()
257
- ):
258
- task.unblock()
259
-
260
- cur_dir = os.getcwd()
261
- with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as working_dir:
262
- os.chdir(working_dir)
263
- try:
264
- execute_task(task)
265
- finally:
266
- # Whether the task fails or not, we should always restore the workdir.
267
- os.chdir(cur_dir)
268
-
269
- if resources:
270
- notify_workers = True
271
- elif deferred:
272
- # Resources are blocked. Let the others handle it.
273
- task.app_lock = None
274
- task.save()
275
- else:
276
- task.set_canceling()
277
- task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
278
- else:
279
- notify_workers = True
280
- if notify_workers:
281
- wakeup_worker(TASK_WAKEUP_UNBLOCK)
282
- return task
400
+ return colliding_resources, resources
283
401
 
284
402
 
285
403
  def cancel_task(task_id):