pulpcore 3.87.1__py3-none-any.whl → 3.89.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 (31) hide show
  1. pulp_certguard/app/__init__.py +1 -1
  2. pulp_file/app/__init__.py +1 -1
  3. pulp_file/app/tasks/publishing.py +5 -0
  4. pulp_file/app/tasks/synchronizing.py +4 -0
  5. pulpcore/app/apps.py +1 -1
  6. pulpcore/app/migrations/0142_task_result.py +21 -0
  7. pulpcore/app/migrations/0143_require_app_lock_zdu.py +15 -0
  8. pulpcore/app/models/task.py +43 -25
  9. pulpcore/app/serializers/task.py +5 -0
  10. pulpcore/app/settings.py +0 -2
  11. pulpcore/app/tasks/base.py +9 -2
  12. pulpcore/content/handler.py +7 -6
  13. pulpcore/plugin/serializers/__init__.py +2 -0
  14. pulpcore/plugin/serializers/content.py +1 -1
  15. pulpcore/tasking/tasks.py +67 -71
  16. pulpcore/tasking/worker.py +91 -89
  17. pulpcore/tests/functional/api/test_status.py +1 -1
  18. pulpcore/tests/functional/api/test_upload.py +3 -1
  19. pulpcore/tests/functional/api/using_plugin/test_distributions.py +6 -2
  20. pulpcore/tests/functional/api/using_plugin/test_tasks.py +20 -0
  21. pulpcore/tests/functional/utils.py +1 -1
  22. pulpcore/tests/unit/metrics/test_aiohttp_instrumentation.py +4 -1
  23. pulpcore/tests/unit/models/test_task.py +5 -3
  24. pulpcore/tests/unit/stages/test_artifactdownloader.py +2 -2
  25. pulpcore/tests/unit/viewsets/test_viewset_base.py +3 -3
  26. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/METADATA +2 -2
  27. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/RECORD +31 -29
  28. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/WHEEL +0 -0
  29. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/entry_points.txt +0 -0
  30. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/licenses/LICENSE +0 -0
  31. {pulpcore-3.87.1.dist-info → pulpcore-3.89.0.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,6 @@ class PulpCertGuardPluginAppConfig(PulpPluginAppConfig):
6
6
 
7
7
  name = "pulp_certguard.app"
8
8
  label = "certguard"
9
- version = "3.87.1"
9
+ version = "3.89.0"
10
10
  python_package_name = "pulpcore"
11
11
  domain_compatible = True
pulp_file/app/__init__.py CHANGED
@@ -8,6 +8,6 @@ class PulpFilePluginAppConfig(PulpPluginAppConfig):
8
8
 
9
9
  name = "pulp_file.app"
10
10
  label = "file"
11
- version = "3.87.1"
11
+ version = "3.89.0"
12
12
  python_package_name = "pulpcore"
13
13
  domain_compatible = True
@@ -13,6 +13,7 @@ from pulpcore.plugin.models import (
13
13
  )
14
14
 
15
15
  from pulp_file.app.models import FilePublication
16
+ from pulp_file.app.serializers import FilePublicationSerializer
16
17
  from pulp_file.manifest import Entry, Manifest
17
18
 
18
19
 
@@ -51,6 +52,10 @@ def publish(manifest, repository_version_pk, checkpoint=False):
51
52
 
52
53
  log.info(_("Publication: {publication} created").format(publication=publication.pk))
53
54
 
55
+ publication = FilePublicationSerializer(
56
+ instance=publication, context={"request": None}
57
+ ).data
58
+
54
59
  return publication
55
60
 
56
61
 
@@ -7,6 +7,7 @@ from urllib.parse import quote, urlparse, urlunparse
7
7
  from django.core.files import File
8
8
 
9
9
  from pulpcore.plugin.models import Artifact, ProgressReport, Remote, PublishedMetadata
10
+ from pulpcore.plugin.serializers import RepositoryVersionSerializer
10
11
  from pulpcore.plugin.stages import (
11
12
  DeclarativeArtifact,
12
13
  DeclarativeContent,
@@ -65,6 +66,9 @@ def synchronize(remote_pk, repository_pk, mirror, url=None):
65
66
 
66
67
  log.info(_("Publication: {publication} created").format(publication=publication.pk))
67
68
 
69
+ if rv:
70
+ rv = RepositoryVersionSerializer(instance=rv, context={"request": None}).data
71
+
68
72
  return rv
69
73
 
70
74
 
pulpcore/app/apps.py CHANGED
@@ -239,7 +239,7 @@ class PulpAppConfig(PulpPluginAppConfig):
239
239
  label = "core"
240
240
 
241
241
  # The version of this app
242
- version = "3.87.1"
242
+ version = "3.89.0"
243
243
 
244
244
  # The python package name providing this app
245
245
  python_package_name = "pulpcore"
@@ -0,0 +1,21 @@
1
+ # Generated by Django 4.2.23 on 2025-08-29 13:34
2
+
3
+ import django.core.serializers.json
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("core", "0141_alter_appstatus_name"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AddField(
15
+ model_name="task",
16
+ name="result",
17
+ field=models.JSONField(
18
+ default=None, encoder=django.core.serializers.json.DjangoJSONEncoder, null=True
19
+ ),
20
+ ),
21
+ ]
@@ -0,0 +1,15 @@
1
+ # Generated by Django 4.2.23 on 2025-09-04 08:58
2
+
3
+ from django.db import migrations
4
+ from pulpcore.migrations import RequireVersion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ("core", "0142_task_result"),
11
+ ]
12
+
13
+ operations = [
14
+ RequireVersion("core", "3.87"),
15
+ ]
@@ -2,6 +2,7 @@
2
2
  Django models related to the Tasking system
3
3
  """
4
4
 
5
+ import json
5
6
  import logging
6
7
  import traceback
7
8
  from datetime import timedelta
@@ -11,7 +12,7 @@ from django.conf import settings
11
12
  from django.contrib.postgres.fields import ArrayField, HStoreField
12
13
  from django.contrib.postgres.indexes import GinIndex
13
14
  from django.core.serializers.json import DjangoJSONEncoder
14
- from django.db import connection, models
15
+ from django.db import models
15
16
  from django.utils import timezone
16
17
  from django_lifecycle import hook, AFTER_CREATE
17
18
 
@@ -20,10 +21,10 @@ from pulpcore.app.models import (
20
21
  BaseModel,
21
22
  GenericRelationModel,
22
23
  )
23
- from pulpcore.app.models.status import BaseAppStatus
24
+ from pulpcore.app.models.status import AppStatus, BaseAppStatus
24
25
  from pulpcore.app.models.fields import EncryptedJSONField
25
26
  from pulpcore.constants import TASK_CHOICES, TASK_INCOMPLETE_STATES, TASK_STATES
26
- from pulpcore.exceptions import AdvisoryLockError, exception_to_dict
27
+ from pulpcore.exceptions import exception_to_dict
27
28
  from pulpcore.app.util import get_domain_pk, current_task
28
29
  from pulpcore.app.loggers import deprecation_logger
29
30
 
@@ -114,6 +115,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
114
115
  deferred (models.BooleanField): Whether to allow defer running the task to a
115
116
  pulpcore_worker. Both `immediate` and `deferred` cannot both be `False`.
116
117
  Defaults to `True`.
118
+ result (models.JSONField): The result of the task
117
119
 
118
120
  Relations:
119
121
  app_lock (AppStatus): The app holding the lock on this task.
@@ -139,7 +141,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
139
141
  enc_kwargs = EncryptedJSONField(null=True, encoder=DjangoJSONEncoder)
140
142
 
141
143
  worker = models.ForeignKey("Worker", null=True, related_name="tasks", on_delete=models.SET_NULL)
142
- # This field is supposed to replace the session advisory locks to protect tasks.
144
+ # This field is the lock to protect tasks.
143
145
  app_lock = models.ForeignKey(
144
146
  "AppStatus", null=True, related_name="tasks", on_delete=models.SET_NULL
145
147
  )
@@ -160,25 +162,11 @@ class Task(BaseModel, AutoAddObjPermsMixin):
160
162
  immediate = models.BooleanField(default=False, null=True)
161
163
  deferred = models.BooleanField(default=True, null=True)
162
164
 
165
+ result = models.JSONField(default=None, null=True, encoder=DjangoJSONEncoder)
166
+
163
167
  def __str__(self):
164
168
  return "Task: {name} [{state}]".format(name=self.name, state=self.state)
165
169
 
166
- def __enter__(self):
167
- self.lock = _uuid_to_advisory_lock(self.pk.int)
168
- with connection.cursor() as cursor:
169
- cursor.execute("SELECT pg_try_advisory_lock(%s)", [self.lock])
170
- acquired = cursor.fetchone()[0]
171
- if not acquired:
172
- raise AdvisoryLockError("Could not acquire lock.")
173
- return self
174
-
175
- def __exit__(self, exc_type, exc_value, traceback):
176
- with connection.cursor() as cursor:
177
- cursor.execute("SELECT pg_advisory_unlock(%s)", [self.lock])
178
- released = cursor.fetchone()[0]
179
- if not released:
180
- raise RuntimeError("Lock not held.")
181
-
182
170
  @staticmethod
183
171
  def current_id():
184
172
  """
@@ -214,7 +202,11 @@ class Task(BaseModel, AutoAddObjPermsMixin):
214
202
  This updates the :attr:`started_at` and sets the :attr:`state` to :attr:`RUNNING`.
215
203
  """
216
204
  started_at = timezone.now()
217
- rows = Task.objects.filter(pk=self.pk, state=TASK_STATES.WAITING).update(
205
+ rows = Task.objects.filter(
206
+ pk=self.pk,
207
+ state=TASK_STATES.WAITING,
208
+ app_lock=AppStatus.objects.current(),
209
+ ).update(
218
210
  state=TASK_STATES.RUNNING,
219
211
  started_at=started_at,
220
212
  )
@@ -229,22 +221,40 @@ class Task(BaseModel, AutoAddObjPermsMixin):
229
221
  )
230
222
  )
231
223
 
232
- def set_completed(self):
224
+ def set_completed(self, result=None):
233
225
  """
234
226
  Set this Task to the completed state, save it, and log output in warning cases.
235
227
 
236
228
  This updates the :attr:`finished_at` and sets the :attr:`state` to :attr:`COMPLETED`.
229
+ If `result` is provided, the :attr:`result` contains the result of the task.
237
230
  """
231
+ try:
232
+ json.dumps(result, cls=DjangoJSONEncoder)
233
+ except (TypeError, ValueError):
234
+ deprecation_logger.warning(
235
+ _(
236
+ "The result of the {} function is not JSON-serializable and will be "
237
+ "replaced with None: {}. This will raise an error in version 3.100."
238
+ ).format(self.name, result)
239
+ )
240
+ result = None
241
+
238
242
  # Only set the state to finished if it's running. This is important for when the task has
239
243
  # been canceled, so we don't move the task from canceled to finished.
240
244
  finished_at = timezone.now()
241
- rows = Task.objects.filter(pk=self.pk, state=TASK_STATES.RUNNING).update(
245
+ rows = Task.objects.filter(
246
+ pk=self.pk,
247
+ state=TASK_STATES.RUNNING,
248
+ app_lock=AppStatus.objects.current(),
249
+ ).update(
242
250
  state=TASK_STATES.COMPLETED,
243
251
  finished_at=finished_at,
252
+ result=result,
244
253
  )
245
254
  if rows == 1:
246
255
  self.state = TASK_STATES.COMPLETED
247
256
  self.finished_at = finished_at
257
+ self.result = result
248
258
  else:
249
259
  self.refresh_from_db()
250
260
  # If the user requested to cancel this task while the worker finished it, we leave it
@@ -271,7 +281,11 @@ class Task(BaseModel, AutoAddObjPermsMixin):
271
281
  finished_at = timezone.now()
272
282
  tb_str = "".join(traceback.format_tb(tb))
273
283
  error = exception_to_dict(exc, tb_str)
274
- rows = Task.objects.filter(pk=self.pk, state=TASK_STATES.RUNNING).update(
284
+ rows = Task.objects.filter(
285
+ pk=self.pk,
286
+ state=TASK_STATES.RUNNING,
287
+ app_lock=AppStatus.objects.current(),
288
+ ).update(
275
289
  state=TASK_STATES.FAILED,
276
290
  finished_at=finished_at,
277
291
  error=error,
@@ -318,7 +332,11 @@ class Task(BaseModel, AutoAddObjPermsMixin):
318
332
  task_data = {}
319
333
  if reason:
320
334
  task_data["error"] = {"reason": reason}
321
- rows = Task.objects.filter(pk=self.pk, state=TASK_STATES.CANCELING).update(
335
+ rows = Task.objects.filter(
336
+ pk=self.pk,
337
+ state=TASK_STATES.CANCELING,
338
+ app_lock=AppStatus.objects.current(),
339
+ ).update(
322
340
  state=final_state,
323
341
  finished_at=finished_at,
324
342
  **task_data,
@@ -88,6 +88,10 @@ class TaskSerializer(ModelSerializer):
88
88
  help_text=_("A list of resources required by that task."),
89
89
  read_only=True,
90
90
  )
91
+ result = serializers.JSONField(
92
+ read_only=True,
93
+ help_text=_("The result of this task."),
94
+ )
91
95
 
92
96
  def get_created_by(self, obj):
93
97
  if task_user_map := self.context.get("task_user_mapping"):
@@ -115,6 +119,7 @@ class TaskSerializer(ModelSerializer):
115
119
  "progress_reports",
116
120
  "created_resources",
117
121
  "reserved_resources_record",
122
+ "result",
118
123
  )
119
124
 
120
125
 
pulpcore/app/settings.py CHANGED
@@ -216,8 +216,6 @@ TIME_ZONE = "UTC"
216
216
 
217
217
  USE_I18N = "USE_I18N", True
218
218
 
219
- USE_L10N = True
220
-
221
219
  USE_TZ = True
222
220
 
223
221
 
@@ -19,7 +19,8 @@ def general_create_from_temp_file(app_label, serializer_name, temp_file_pk, *arg
19
19
  context = kwargs.pop("context", {})
20
20
  context["pulp_temp_file_pk"] = temp_file_pk
21
21
 
22
- general_create(app_label, serializer_name, data=data, context=context, *args, **kwargs)
22
+ data = general_create(app_label, serializer_name, data=data, context=context, *args, **kwargs)
23
+ return data
23
24
 
24
25
 
25
26
  def general_create(app_label, serializer_name, *args, **kwargs):
@@ -33,6 +34,7 @@ def general_create(app_label, serializer_name, *args, **kwargs):
33
34
  data = kwargs.pop("data", None)
34
35
 
35
36
  context = kwargs.pop("context", {})
37
+ context.setdefault("request", None)
36
38
  serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
37
39
  serializer = serializer_class(data=data, context=context)
38
40
 
@@ -42,6 +44,7 @@ def general_create(app_label, serializer_name, *args, **kwargs):
42
44
  instance = instance.cast()
43
45
  resource = CreatedResource(content_object=instance)
44
46
  resource.save()
47
+ return serializer.data
45
48
 
46
49
 
47
50
  def general_update(instance_id, app_label, serializer_name, *args, **kwargs):
@@ -130,13 +133,17 @@ async def ageneral_update(instance_id, app_label, serializer_name, *args, **kwar
130
133
  """
131
134
  data = kwargs.pop("data", None)
132
135
  partial = kwargs.pop("partial", False)
136
+ context = kwargs.pop("context", {})
137
+ context.setdefault("request", None)
138
+
133
139
  serializer_class = get_plugin_config(app_label).named_serializers[serializer_name]
134
140
  instance = await serializer_class.Meta.model.objects.aget(pk=instance_id)
135
141
  if isinstance(instance, MasterModel):
136
142
  instance = await instance.acast()
137
- serializer = serializer_class(instance, data=data, partial=partial)
143
+ serializer = serializer_class(instance, data=data, partial=partial, context=context)
138
144
  await sync_to_async(serializer.is_valid)(raise_exception=True)
139
145
  await sync_to_async(serializer.save)()
146
+ return await sync_to_async(lambda: serializer.data)()
140
147
 
141
148
 
142
149
  async def ageneral_delete(instance_id, app_label, serializer_name):
@@ -7,6 +7,7 @@ import socket
7
7
  import struct
8
8
  from gettext import gettext as _
9
9
  from datetime import datetime, timedelta
10
+ from datetime import timezone as dt_timezone
10
11
 
11
12
  from aiohttp.client_exceptions import ClientResponseError, ClientConnectionError
12
13
  from aiohttp.web import FileResponse, StreamResponse, HTTPOk
@@ -115,7 +116,7 @@ class DistroListings(HTTPOk):
115
116
  if path == "":
116
117
  path = settings.CONTENT_PATH_PREFIX
117
118
  html = Handler.render_html(directory_list, path=path)
118
- super().__init__(body=html, headers={"Content-Type": "text/html"})
119
+ super().__init__(text=html, headers={"Content-Type": "text/html"})
119
120
 
120
121
 
121
122
  class CheckpointListings(HTTPOk):
@@ -137,7 +138,7 @@ class CheckpointListings(HTTPOk):
137
138
  dates = {f"{Handler._format_checkpoint_timestamp(s)}/": s for s in checkpoints}
138
139
  directory_list = dates.keys()
139
140
  html = Handler.render_html(directory_list, dates=dates, path=path)
140
- super().__init__(body=html, headers={"Content-Type": "text/html"})
141
+ super().__init__(text=html, headers={"Content-Type": "text/html"})
141
142
 
142
143
 
143
144
  class ArtifactNotFound(Exception):
@@ -439,9 +440,9 @@ class Handler:
439
440
  else:
440
441
  raise PathNotResolved(path)
441
442
 
442
- request_timestamp = request_timestamp.replace(tzinfo=timezone.utc)
443
+ request_timestamp = request_timestamp.replace(tzinfo=dt_timezone.utc)
443
444
  # Future timestamps are not allowed for checkpoints
444
- if request_timestamp > datetime.now(tz=timezone.utc):
445
+ if request_timestamp > datetime.now(tz=dt_timezone.utc):
445
446
  raise PathNotResolved(path)
446
447
  # The timestamp is truncated to seconds, so we need to cover the whole second
447
448
  request_timestamp = request_timestamp.replace(microsecond=999999)
@@ -787,7 +788,7 @@ class Handler:
787
788
  elif dir_list:
788
789
  return HTTPOk(
789
790
  headers={"Content-Type": "text/html"},
790
- body=self.render_html(
791
+ text=self.render_html(
791
792
  dir_list, path=request.path, dates=dates, sizes=sizes
792
793
  ),
793
794
  )
@@ -862,7 +863,7 @@ class Handler:
862
863
  elif dir_list:
863
864
  return HTTPOk(
864
865
  headers={"Content-Type": "text/html"},
865
- body=self.render_html(
866
+ text=self.render_html(
866
867
  dir_list, path=request.path, dates=dates, sizes=sizes
867
868
  ),
868
869
  )
@@ -31,6 +31,7 @@ from pulpcore.app.serializers import (
31
31
  RepositorySerializer,
32
32
  RepositorySyncURLSerializer,
33
33
  RepositoryVersionRelatedField,
34
+ RepositoryVersionSerializer,
34
35
  SingleArtifactContentSerializer,
35
36
  SingleContentArtifactField,
36
37
  TaskGroupOperationResponseSerializer,
@@ -79,6 +80,7 @@ __all__ = [
79
80
  "RepositorySerializer",
80
81
  "RepositorySyncURLSerializer",
81
82
  "RepositoryVersionRelatedField",
83
+ "RepositoryVersionSerializer",
82
84
  "SingleArtifactContentSerializer",
83
85
  "SingleContentArtifactField",
84
86
  "TaskGroupOperationResponseSerializer",
@@ -85,7 +85,7 @@ class UploadSerializerFieldsMixin(Serializer):
85
85
 
86
86
  data = super().validate(data)
87
87
 
88
- if "request" in self.context:
88
+ if self.context.get("request") is not None:
89
89
  upload_fields = {
90
90
  field
91
91
  for field in self.Meta.fields
pulpcore/tasking/tasks.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import contextlib
3
2
  import contextvars
4
3
  import importlib
5
4
  import logging
@@ -11,7 +10,7 @@ import threading
11
10
  from gettext import gettext as _
12
11
 
13
12
  from django.conf import settings
14
- from django.db import connection, transaction
13
+ from django.db import connection
15
14
  from django.db.models import Model
16
15
  from django_guid import get_guid
17
16
  from pulpcore.app.apps import MODULE_PLUGIN_VERSIONS
@@ -26,6 +25,7 @@ from pulpcore.constants import (
26
25
  TASK_INCOMPLETE_STATES,
27
26
  TASK_STATES,
28
27
  IMMEDIATE_TIMEOUT,
28
+ TASK_WAKEUP_HANDLE,
29
29
  TASK_WAKEUP_UNBLOCK,
30
30
  )
31
31
  from pulpcore.middleware import x_task_diagnostics_var
@@ -49,7 +49,7 @@ def _validate_and_get_resources(resources):
49
49
  return list(resource_set)
50
50
 
51
51
 
52
- def wakeup_worker(reason="unknown"):
52
+ def wakeup_worker(reason):
53
53
  # Notify workers
54
54
  with connection.connection.cursor() as cursor:
55
55
  cursor.execute("SELECT pg_notify('pulp_worker_wakeup', %s)", (reason,))
@@ -95,7 +95,7 @@ def _execute_task(task):
95
95
  coro = asyncio.wait_for(coro, timeout=IMMEDIATE_TIMEOUT)
96
96
  loop = asyncio.get_event_loop()
97
97
  try:
98
- loop.run_until_complete(coro)
98
+ result = loop.run_until_complete(coro)
99
99
  except asyncio.TimeoutError:
100
100
  _logger.info(
101
101
  "Immediate task %s timed out after %s seconds.", task.pk, IMMEDIATE_TIMEOUT
@@ -106,7 +106,7 @@ def _execute_task(task):
106
106
  )
107
107
  )
108
108
  else:
109
- func(*args, **kwargs)
109
+ result = func(*args, **kwargs)
110
110
 
111
111
  except Exception:
112
112
  exc_type, exc, tb = sys.exc_info()
@@ -123,7 +123,7 @@ def _execute_task(task):
123
123
  _logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb))))
124
124
  send_task_notification(task)
125
125
  else:
126
- task.set_completed()
126
+ task.set_completed(result)
127
127
  execution_time = task.finished_at - task.started_at
128
128
  execution_time_us = int(execution_time.total_seconds() * 1_000_000) # μs
129
129
  _logger.info(
@@ -222,67 +222,61 @@ def dispatch(
222
222
  resources = exclusive_resources + [f"shared:{resource}" for resource in shared_resources]
223
223
 
224
224
  notify_workers = False
225
- with contextlib.ExitStack() as stack:
226
- with transaction.atomic():
227
- task = Task.objects.create(
228
- state=TASK_STATES.WAITING,
229
- logging_cid=(get_guid()),
230
- task_group=task_group,
231
- name=function_name,
232
- enc_args=args,
233
- enc_kwargs=kwargs,
234
- parent_task=Task.current(),
235
- reserved_resources_record=resources,
236
- versions=versions,
237
- immediate=immediate,
238
- deferred=deferred,
239
- profile_options=x_task_diagnostics_var.get(None),
240
- app_lock=(immediate and AppStatus.objects.current()) or None,
241
- )
242
- task.refresh_from_db() # The database may have assigned a timestamp for us.
243
- if immediate:
244
- # Grab the advisory lock before the task hits the db.
245
- stack.enter_context(task)
246
- else:
247
- notify_workers = True
248
- if immediate:
249
- prior_tasks = Task.objects.filter(
250
- state__in=TASK_INCOMPLETE_STATES, pulp_created__lt=task.pulp_created
251
- )
252
- # Compile a list of resources that must not be taken by other tasks.
253
- colliding_resources = (
254
- shared_resources
255
- + exclusive_resources
256
- + [f"shared:{resource}" for resource in exclusive_resources]
257
- )
258
- # Can we execute this task immediately?
259
- if (
260
- not colliding_resources
261
- or not prior_tasks.filter(
262
- reserved_resources_record__overlap=colliding_resources
263
- ).exists()
264
- ):
265
- task.unblock()
266
-
267
- cur_dir = os.getcwd()
268
- with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as working_dir:
269
- os.chdir(working_dir)
270
- try:
271
- execute_task(task)
272
- finally:
273
- # Whether the task fails or not, we should always restore the workdir.
274
- os.chdir(cur_dir)
275
-
276
- if resources:
277
- notify_workers = True
278
- elif deferred:
279
- # Resources are blocked. Let the others handle it.
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.
241
+ 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
+ colliding_resources = (
247
+ shared_resources
248
+ + exclusive_resources
249
+ + [f"shared:{resource}" for resource in exclusive_resources]
250
+ )
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:
280
270
  notify_workers = True
281
- task.app_lock = None
282
- task.save()
283
- else:
284
- task.set_canceling()
285
- task.set_canceled(TASK_STATES.CANCELED, "Resources temporarily unavailable.")
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
286
280
  if notify_workers:
287
281
  wakeup_worker(TASK_WAKEUP_UNBLOCK)
288
282
  return task
@@ -304,7 +298,7 @@ def cancel_task(task_id):
304
298
  task = Task.objects.select_related("pulp_domain").get(pk=task_id)
305
299
 
306
300
  if task.state in TASK_FINAL_STATES:
307
- # If the task is already done, just stop
301
+ # If the task is already done, just stop.
308
302
  _logger.debug(
309
303
  "Task [{task_id}] in domain: {name} already in a final state: {state}".format(
310
304
  task_id=task_id, name=task.pulp_domain.name, state=task.state
@@ -315,12 +309,14 @@ def cancel_task(task_id):
315
309
  "Canceling task: {id} in domain: {name}".format(id=task_id, name=task.pulp_domain.name)
316
310
  )
317
311
 
318
- # This is the only valid transition without holding the task lock
312
+ # This is the only valid transition without holding the task lock.
319
313
  task.set_canceling()
320
- # Notify the worker that might be running that task and other workers to clean up
314
+ # Notify the worker that might be running that task.
321
315
  with connection.cursor() as cursor:
322
- cursor.execute("SELECT pg_notify('pulp_worker_cancel', %s)", (str(task.pk),))
323
- cursor.execute("NOTIFY pulp_worker_wakeup")
316
+ if task.app_lock is None:
317
+ wakeup_worker(TASK_WAKEUP_HANDLE)
318
+ else:
319
+ cursor.execute("SELECT pg_notify('pulp_worker_cancel', %s)", (str(task.pk),))
324
320
  return task
325
321
 
326
322