Skip to content

Commit 309ba35

Browse files
committed
Add X-TASK-DIAGNOSTICS header support
This allows individual tasks to be profiled. The value must be a comma separated list of "memory", "pyinstrument", "memray". closes: #6725
1 parent 82b29a3 commit 309ba35

File tree

13 files changed

+126
-15
lines changed

13 files changed

+126
-15
lines changed

.github/workflows/scripts/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ if [ "$TEST" = "azure" ]; then
120120
- ./azurite:/etc/pulp\
121121
command: "azurite-blob --blobHost 0.0.0.0"' vars/main.yaml
122122
sed -i -e '$a azure_test: true\
123-
pulp_scenario_settings: {"api_root_rewrite_header": "X-API-Root", "content_origin": null, "domain_enabled": true, "rest_framework__default_permission_classes": ["pulpcore.plugin.access_policy.DefaultAccessPolicy"]}\
123+
pulp_scenario_settings: {"api_root_rewrite_header": "X-API-Root", "content_origin": null, "domain_enabled": true, "rest_framework__default_permission_classes": ["pulpcore.plugin.access_policy.DefaultAccessPolicy"], "task_diagnostics": ["memory"]}\
124124
pulp_scenario_env: {}\
125125
' vars/main.yaml
126126
fi

CHANGES/6725.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added support for profiling individual tasks with X-TASK-DIAGNOSTICS header.
2+
The TASK_DIAGNOSTICS setting now determines which profilers are available to users of the REST API.

docs/admin/reference/settings.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,18 +344,15 @@ Defaults to `'REMOTE_USER'`.
344344

345345
### TASK\_DIAGNOSTICS
346346

347-
When enabled, each task records various diagnostics for analysis by a developer.
348-
Some of these diagnostics add runtime overhead, and should probably not be enabled in production environments without careful thought and supervision.
347+
When enabled, users are allowed to request various diagnostics for analysis by a developer.
349348

350-
The list of possible diagnostics that can be used are `["memory", "pyinstrument", "memray"]`.
349+
Administrators can enable support for `["memory", "pyinstrument", "memray"]`.
351350
Any subset of those can be used, or all of them, or none of them.
352351

353-
A value of `True` enables all diagnostics.
354-
355352
!!! note
356353
`memray` and `pyinstrument` diagnostics require additional packages to be installed.
357354

358-
Defaults to `[]` (no diagnostics).
355+
Defaults to `[]` (no diagnostics enabled).
359356

360357
See [task diagnostics documentation] for more details.
361358

docs/user/guides/troubleshoot-tasks.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,33 @@ Cancel a running task:
4747
pulp task cancel --href "$TASK_HREF"
4848
```
4949

50+
## Profiling Tasks
51+
52+
Task profiling is enabled by an administrator using the [TASK_DIAGNOSTICS] setting.
53+
54+
Users can submit a `X-TASK-DIAGNOSTICS` header with their API requests that generate tasks.
55+
The header value should be a comma separated list of : "memory", "pyinstrument",or "memray".
56+
For example:
57+
58+
```bash
59+
`pulp --header X-TASK-DIAGNOSTICS:pyinstrument,memory rpm publication create --repository foo
60+
````
61+
62+
Once the task finishes, the files generated by the profiler can be found using the following command:
63+
64+
```bash
65+
pulp task profile-artifact-urls --href "$TASK_HREF"
66+
{
67+
"memory_profile": "/pulp/content/default/115341ffbc5c32b379142936bd85ab658a83209a0ab03f495d0448bf1f9ffee0/0197af04-272a-71cd-b90d-174be8d8ddd8?expires=1750992034&validate_token=e56ce8188a4c16bcb36ddc916d314e623d959b3ae1ebd84a79ebafac47bc49ea:46fcedc3ca8e826fd578016cb79fa9842e472572122c312e59a6f3682d50c4aa",
68+
"pyinstrument_profile": "/pulp/content/default/115341ffbc5c32b379142936bd85ab658a83209a0ab03f495d0448bf1f9ffee0/0197af04-2769-78a1-9b1d-96854d8573a0?expires=1750992034&validate_token=b58aba0031f662de5666ef2bc1682093ba07d3d9cefcfbfe254ad762b34e9d0e:377c1b92d327a1432a46b64d747ecb50e830aadd79892c770e8249d087eaecdd"
69+
}
70+
```
71+
72+
The files can then be downloaded from Pulp using the paths returned by the above command.
73+
5074
## Tracing workloads
5175

52-
To help users better trace workloads in Pulp, Pulp provides [support for correlation
53-
ids](site:pulpcore/docs/user/guides/correlation-id/).
76+
To help users better trace workloads in Pulp, Pulp provides support for [correlation ids].
77+
78+
[correlation ids]: site:pulpcore/docs/user/guides/correlation-id/
79+
[TASK_DIAGNOSTICS]: site:pulpcore/docs/admin/reference/settings/#task_diagnostics
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.2.10 on 2025-06-26 19:44
2+
3+
import django.contrib.postgres.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('core', '0131_distribution_checkpoint_publication_checkpoint'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='task',
16+
name='profile_options',
17+
field=django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None),
18+
),
19+
]

pulpcore/app/models/task.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ class Task(BaseModel, AutoAddObjPermsMixin):
147147
pulp_domain = models.ForeignKey("Domain", default=get_domain_pk, on_delete=models.CASCADE)
148148
versions = HStoreField(default=dict)
149149

150+
profile_options = ArrayField(models.TextField(), null=True)
150151
profile_artifacts = models.ManyToManyField("Artifact", through=ProfileArtifact)
151152

152153
immediate = models.BooleanField(default=False, null=True)

pulpcore/app/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
"django.middleware.clickjacking.XFrameOptionsMiddleware",
174174
"pulpcore.middleware.DomainMiddleware",
175175
"pulpcore.middleware.APIRootRewriteMiddleware",
176+
"pulpcore.middleware.TaskProfilerMiddleware",
176177
]
177178

178179
AUTHENTICATION_BACKENDS = [

pulpcore/middleware.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
from contextvars import ContextVar
23

34
from django.http.response import Http404
45
from django.conf import settings
@@ -13,6 +14,8 @@
1314
set_domain,
1415
)
1516

17+
x_task_diagnostics_var = ContextVar("x_profile_task")
18+
1619

1720
class DomainMiddleware:
1821
"""
@@ -143,3 +146,25 @@ def _process_path(request, response):
143146
match = getattr(request, "resolver_match", "")
144147
route = getattr(match, "route", "")
145148
return route
149+
150+
151+
class TaskProfilerMiddleware:
152+
"""
153+
Middleware that looks for the presence of X-TASK-DIAGNOSTICS header and provides its value
154+
as a ContextVar to the dispatch() method in the tasking system. It enables generating
155+
profiling data of tasks dispatched via API.
156+
"""
157+
158+
def __init__(self, get_response):
159+
self.get_response = get_response
160+
161+
def __call__(self, request):
162+
if "HTTP_X_TASK_DIAGNOSTICS" in request.META:
163+
task_diagnostics = request.META["HTTP_X_TASK_DIAGNOSTICS"]
164+
ctx_token = x_task_diagnostics_var.set(task_diagnostics.split(","))
165+
try:
166+
return self.get_response(request)
167+
finally:
168+
x_task_diagnostics_var.reset(ctx_token)
169+
else:
170+
return self.get_response(request)

pulpcore/openapi/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,19 @@ def _get_serializer_name(self, serializer, direction, bypass_extensions=False):
201201
name = name + "Response"
202202
return name
203203

204+
def get_override_parameters(self):
205+
parameters = super().get_override_parameters()
206+
parameters.append(
207+
OpenApiParameter(
208+
name="X-Task-Diagnostics",
209+
location=OpenApiParameter.HEADER,
210+
required=False,
211+
type={"type": "array", "items": {"type": "string"}},
212+
description="List of profilers to use on tasks.",
213+
)
214+
)
215+
return parameters
216+
204217
def map_parsers(self):
205218
"""
206219
Get request parsers.

pulpcore/tasking/_util.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,26 @@ def perform_task(task_pk, task_working_dir_rel_path):
127127
set_domain(task.pulp_domain)
128128
os.chdir(task_working_dir_rel_path)
129129

130-
if settings.TASK_DIAGNOSTICS:
131-
_execute_task_and_profile(task)
130+
if task.profile_options:
131+
profilers = set(task.profile_options) & set(settings.TASK_DIAGNOSTICS)
132+
if unavailable_profilers := set(task.profile_options) - set(settings.TASK_DIAGNOSTICS):
133+
_logger.warning(
134+
"Requested task diagnostic profilers are not available: %s", unavailable_profilers
135+
)
136+
_execute_task_and_profile(task, profilers)
132137
else:
133138
execute_task(task)
134139

135140

136-
def _execute_task_and_profile(task):
141+
def _execute_task_and_profile(task, profile_options):
137142
with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_dir:
138143
_execute_task = execute_task
139144

140-
if settings.TASK_DIAGNOSTICS is True or "memory" in settings.TASK_DIAGNOSTICS:
145+
if "memory" in profile_options:
141146
_execute_task = _memory_diagnostic_decorator(temp_dir, _execute_task)
142-
if settings.TASK_DIAGNOSTICS is True or "pyinstrument" in settings.TASK_DIAGNOSTICS:
147+
if "pyinstrument" in profile_options:
143148
_execute_task = _pyinstrument_diagnostic_decorator(temp_dir, _execute_task)
144-
if settings.TASK_DIAGNOSTICS is True or "memray" in settings.TASK_DIAGNOSTICS:
149+
if "memray" in profile_options:
145150
_execute_task = _memray_diagnostic_decorator(temp_dir, _execute_task)
146151

147152
_execute_task(task)

0 commit comments

Comments
 (0)