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.
- pulp_certguard/app/__init__.py +1 -1
- pulp_file/app/__init__.py +1 -1
- pulp_file/tests/functional/api/test_filesystem_export.py +220 -0
- pulp_file/tests/functional/api/test_pulp_export.py +103 -3
- pulpcore/app/apps.py +1 -1
- pulpcore/app/importexport.py +18 -2
- pulpcore/app/management/commands/shell.py +8 -0
- pulpcore/app/migrations/0144_delete_old_appstatus.py +28 -0
- pulpcore/app/migrations/0145_domainize_import_export.py +53 -0
- pulpcore/app/modelresource.py +61 -21
- pulpcore/app/models/__init__.py +2 -5
- pulpcore/app/models/exporter.py +7 -1
- pulpcore/app/models/fields.py +0 -1
- pulpcore/app/models/importer.py +8 -1
- pulpcore/app/models/repository.py +16 -0
- pulpcore/app/models/status.py +8 -138
- pulpcore/app/models/task.py +15 -25
- pulpcore/app/serializers/domain.py +1 -1
- pulpcore/app/serializers/exporter.py +4 -4
- pulpcore/app/serializers/importer.py +2 -2
- pulpcore/app/serializers/task.py +11 -8
- pulpcore/app/tasks/importer.py +44 -10
- pulpcore/app/tasks/repository.py +27 -0
- pulpcore/app/viewsets/base.py +18 -14
- pulpcore/app/viewsets/domain.py +1 -1
- pulpcore/app/viewsets/exporter.py +1 -8
- pulpcore/app/viewsets/importer.py +1 -6
- pulpcore/app/viewsets/task.py +0 -1
- pulpcore/openapi/__init__.py +16 -2
- pulpcore/plugin/tasking.py +4 -2
- pulpcore/tasking/tasks.py +245 -127
- pulpcore/tasking/worker.py +6 -17
- pulpcore/tests/functional/api/test_crud_domains.py +7 -0
- pulpcore/tests/functional/api/test_tasking.py +2 -2
- pulpcore/tests/functional/api/using_plugin/test_crud_repos.py +9 -2
- pulpcore/tests/unit/content/test_handler.py +43 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/METADATA +7 -7
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/RECORD +42 -38
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/WHEEL +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/entry_points.txt +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/licenses/LICENSE +0 -0
- {pulpcore-3.89.1.dist-info → pulpcore-3.90.0.dist-info}/top_level.txt +0 -0
pulpcore/app/viewsets/base.py
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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="
|
|
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
|
pulpcore/app/viewsets/domain.py
CHANGED
|
@@ -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
|
|
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
|
|
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:
|
pulpcore/app/viewsets/task.py
CHANGED
|
@@ -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,
|
pulpcore/openapi/__init__.py
CHANGED
|
@@ -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."""
|
pulpcore/plugin/tasking.py
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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):
|