Skip to content

Commit 2a17750

Browse files
Merge branch 'remove-dojo-async-task-base-task' into remove-dojo-async-task-base-task-bugfix
# Conflicts: # helm/defectdojo/Chart.yaml
2 parents f12f27e + 9c5b418 commit 2a17750

39 files changed

Lines changed: 849 additions & 319 deletions

.github/workflows/renovate.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ jobs:
2121
uses: suzuki-shunsuke/github-action-renovate-config-validator@ca480cb7ec89a9e1cd8c214ad33bda1617184027 # v2.0.0
2222
with:
2323
strict: "true"
24-
validator_version: 42.92.5 # renovate: datasource=github-releases depName=renovatebot/renovate
24+
validator_version: 43.2.4 # renovate: datasource=github-releases depName=renovatebot/renovate

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ services:
120120
source: ./docker/extra_settings
121121
target: /app/docker/extra_settings
122122
postgres:
123-
image: postgres:18.1-alpine@sha256:4eb15de8e7b692c02427a2df278d18eb89422a534e428efb6d43c968250334d4
123+
image: postgres:18.1-alpine@sha256:aa6eb304ddb6dd26df23d05db4e5cb05af8951cda3e0dc57731b771e0ef4ab29
124124
environment:
125125
POSTGRES_DB: ${DD_DATABASE_NAME:-defectdojo}
126126
POSTGRES_USER: ${DD_DATABASE_USER:-defectdojo}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Upgrading to DefectDojo Version 2.56.x'
3+
toc_hide: true
4+
weight: -20260203
5+
description: No special instructions.
6+
---
7+
There are no special instructions for upgrading to 2.56.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.56.0) for the contents of the release.

dojo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
# Django starts so that shared_task will use this app.
55
from .celery import app as celery_app # noqa: F401
66

7-
__version__ = "2.55.0"
7+
__version__ = "2.56.0-dev"
88
__url__ = "https://github.com/DefectDojo/django-DefectDojo" # noqa: RUF067
99
__docs__ = "https://documentation.defectdojo.com" # noqa: RUF067

dojo/api_v2/views.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
)
4747
from dojo.api_v2.prefetch.prefetcher import _Prefetcher
4848
from dojo.authorization.roles_permissions import Permissions
49+
from dojo.celery_dispatch import dojo_dispatch_task
4950
from dojo.cred.queries import get_authorized_cred_mappings
5051
from dojo.endpoint.queries import (
5152
get_authorized_endpoint_status,
@@ -679,13 +680,13 @@ def update_jira_epic(self, request, pk=None):
679680
try:
680681

681682
if engagement.has_jira_issue:
682-
jira_helper.update_epic(engagement.id, **request.data)
683+
dojo_dispatch_task(jira_helper.update_epic, engagement.id, **request.data)
683684
response = Response(
684685
{"info": "Jira Epic update query sent"},
685686
status=status.HTTP_200_OK,
686687
)
687688
else:
688-
jira_helper.add_epic(engagement.id, **request.data)
689+
dojo_dispatch_task(jira_helper.add_epic, engagement.id, **request.data)
689690
response = Response(
690691
{"info": "Jira Epic create query sent"},
691692
status=status.HTTP_200_OK,

dojo/celery.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,56 @@
1212
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dojo.settings.settings")
1313

1414

15-
class PgHistoryTask(Task):
15+
class DojoAsyncTask(Task):
16+
17+
"""
18+
Base task class that provides dojo_async_task functionality without using a decorator.
19+
20+
This class:
21+
- Injects user context into task kwargs
22+
- Tracks task calls for performance testing
23+
- Supports all Celery features (signatures, chords, groups, chains)
24+
"""
25+
26+
def apply_async(self, args=None, kwargs=None, **options):
27+
"""Override apply_async to inject user context and track tasks."""
28+
from dojo.decorators import dojo_async_task_counter # noqa: PLC0415 circular import
29+
from dojo.utils import get_current_user # noqa: PLC0415 circular import
30+
31+
if kwargs is None:
32+
kwargs = {}
33+
34+
# Inject user context if not already present
35+
if "async_user" not in kwargs:
36+
kwargs["async_user"] = get_current_user()
37+
38+
# Control flag used for sync/async decision; never pass into the task itself
39+
kwargs.pop("sync", None)
40+
41+
# Track dispatch
42+
dojo_async_task_counter.incr(
43+
self.name,
44+
args=args,
45+
kwargs=kwargs,
46+
)
47+
48+
# Call parent to execute async
49+
return super().apply_async(args=args, kwargs=kwargs, **options)
50+
51+
52+
class PgHistoryTask(DojoAsyncTask):
1653

1754
"""
1855
Custom Celery base task that automatically applies pghistory context.
1956
20-
When a task is dispatched via dojo_async_task, the current pghistory
21-
context is captured and passed in kwargs as "_pgh_context". This base
22-
class extracts that context and applies it before running the task,
23-
ensuring all database events share the same context as the original
24-
request.
57+
This class inherits from DojoAsyncTask to provide:
58+
- User context injection and task tracking (from DojoAsyncTask)
59+
- Automatic pghistory context application (from this class)
60+
61+
When a task is dispatched via dojo_dispatch_task or dojo_async_task, the current
62+
pghistory context is captured and passed in kwargs as "_pgh_context". This base
63+
class extracts that context and applies it before running the task, ensuring all
64+
database events share the same context as the original request.
2565
"""
2666

2767
def __call__(self, *args, **kwargs):

dojo/celery_dispatch.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any, Protocol, cast
4+
5+
from celery.canvas import Signature
6+
7+
if TYPE_CHECKING:
8+
from collections.abc import Mapping
9+
10+
11+
class _SupportsSi(Protocol):
12+
def si(self, *args: Any, **kwargs: Any) -> Signature: ...
13+
14+
15+
class _SupportsApplyAsync(Protocol):
16+
def apply_async(self, args: Any | None = None, kwargs: Any | None = None, **options: Any) -> Any: ...
17+
18+
19+
def _inject_async_user(kwargs: Mapping[str, Any] | None) -> dict[str, Any]:
20+
result: dict[str, Any] = dict(kwargs or {})
21+
if "async_user" not in result:
22+
from dojo.utils import get_current_user # noqa: PLC0415 circular import
23+
24+
result["async_user"] = get_current_user()
25+
return result
26+
27+
28+
def _inject_pghistory_context(kwargs: Mapping[str, Any] | None) -> dict[str, Any]:
29+
"""Capture and inject pghistory context if available."""
30+
result: dict[str, Any] = dict(kwargs or {})
31+
if "_pgh_context" not in result:
32+
from dojo.pghistory_utils import get_serializable_pghistory_context # noqa: PLC0415 circular import
33+
34+
if pgh_context := get_serializable_pghistory_context():
35+
result["_pgh_context"] = pgh_context
36+
return result
37+
38+
39+
def dojo_create_signature(task_or_sig: _SupportsSi | Signature, *args: Any, **kwargs: Any) -> Signature:
40+
"""
41+
Build a Celery signature with DefectDojo user context and pghistory context injected.
42+
43+
- If passed a task, returns `task_or_sig.si(*args, **kwargs)`.
44+
- If passed an existing signature, returns a cloned signature with merged kwargs.
45+
"""
46+
injected = _inject_async_user(kwargs)
47+
injected = _inject_pghistory_context(injected)
48+
injected.pop("countdown", None)
49+
50+
if isinstance(task_or_sig, Signature):
51+
merged_kwargs = {**(task_or_sig.kwargs or {}), **injected}
52+
return task_or_sig.clone(kwargs=merged_kwargs)
53+
54+
return task_or_sig.si(*args, **injected)
55+
56+
57+
def dojo_dispatch_task(task_or_sig: _SupportsSi | _SupportsApplyAsync | Signature, *args: Any, **kwargs: Any) -> Any:
58+
"""
59+
Dispatch a task/signature using DefectDojo semantics.
60+
61+
- Inject `async_user` if missing.
62+
- Capture and inject pghistory context if available.
63+
- Respect `sync=True` (foreground execution) and user `block_execution`.
64+
- Support `countdown=<seconds>` for async dispatch.
65+
66+
Returns:
67+
- async: AsyncResult-like return from Celery
68+
- sync: underlying return value of the task
69+
70+
"""
71+
from dojo.decorators import dojo_async_task_counter, we_want_async # noqa: PLC0415 circular import
72+
73+
countdown = cast("int", kwargs.pop("countdown", 0))
74+
injected = _inject_async_user(kwargs)
75+
injected = _inject_pghistory_context(injected)
76+
77+
sig = dojo_create_signature(task_or_sig if isinstance(task_or_sig, Signature) else cast("_SupportsSi", task_or_sig), *args, **injected)
78+
sig_kwargs = dict(sig.kwargs or {})
79+
80+
if we_want_async(*sig.args, func=getattr(sig, "type", None), **sig_kwargs):
81+
# DojoAsyncTask.apply_async tracks async dispatch. Avoid double-counting here.
82+
return sig.apply_async(countdown=countdown)
83+
84+
# Track foreground execution as a "created task" as well (matches historical dojo_async_task behavior)
85+
dojo_async_task_counter.incr(str(sig.task), args=sig.args, kwargs=sig_kwargs)
86+
87+
sig_kwargs.pop("sync", None)
88+
sig = sig.clone(kwargs=sig_kwargs)
89+
eager = sig.apply()
90+
try:
91+
return eager.get(propagate=True)
92+
except RuntimeError:
93+
# Since we are intentionally running synchronously, we can propagate exceptions directly, and enable sync subtasks
94+
# If the requests desires this. Celery docs explain that this is a rare use case, but we support it _just in case_
95+
return eager.get(propagate=True, disable_sync_subtasks=False)

dojo/endpoint/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from dojo.authorization.authorization import user_has_permission_or_403
1919
from dojo.authorization.authorization_decorators import user_is_authorized
2020
from dojo.authorization.roles_permissions import Permissions
21+
from dojo.celery_dispatch import dojo_dispatch_task
2122
from dojo.endpoint.queries import get_authorized_endpoints_for_queryset
2223
from dojo.endpoint.utils import clean_hosts_run, endpoint_meta_import
2324
from dojo.filters import EndpointFilter, EndpointFilterWithoutObjectLookups
@@ -345,7 +346,7 @@ def endpoint_bulk_update_all(request, pid=None):
345346
product_calc = list(Product.objects.filter(endpoint__id__in=endpoints_to_update).distinct())
346347
endpoints.delete()
347348
for prod in product_calc:
348-
calculate_grade(prod.id)
349+
dojo_dispatch_task(calculate_grade, prod.id)
349350

350351
if skipped_endpoint_count > 0:
351352
add_error_message_to_response(f"Skipped deletion of {skipped_endpoint_count} endpoints because you are not authorized.")

dojo/engagement/services.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.dispatch import receiver
66

77
import dojo.jira_link.helper as jira_helper
8+
from dojo.celery_dispatch import dojo_dispatch_task
89
from dojo.models import Engagement
910

1011
logger = logging.getLogger(__name__)
@@ -16,7 +17,7 @@ def close_engagement(eng):
1617
eng.save()
1718

1819
if jira_helper.get_jira_project(eng):
19-
jira_helper.close_epic(eng.id, push_to_jira=True)
20+
dojo_dispatch_task(jira_helper.close_epic, eng.id, push_to_jira=True)
2021

2122

2223
def reopen_engagement(eng):

dojo/engagement/views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from dojo.authorization.authorization import user_has_permission_or_403
3838
from dojo.authorization.authorization_decorators import user_is_authorized
3939
from dojo.authorization.roles_permissions import Permissions
40+
from dojo.celery_dispatch import dojo_dispatch_task
4041
from dojo.endpoint.utils import save_endpoints_to_add
4142
from dojo.engagement.queries import get_authorized_engagements
4243
from dojo.engagement.services import close_engagement, reopen_engagement
@@ -392,7 +393,7 @@ def copy_engagement(request, eid):
392393
form = DoneForm(request.POST)
393394
if form.is_valid():
394395
engagement_copy = engagement.copy()
395-
calculate_grade(product.id)
396+
dojo_dispatch_task(calculate_grade, product.id)
396397
messages.add_message(
397398
request,
398399
messages.SUCCESS,

0 commit comments

Comments
 (0)