AAP-81173 — Replace 4-way OR with UNION in unified job list RBAC query (#16555)

* AAP-81173 — Replace 4-way OR with UNION in unified job list RBAC query

The UnifiedJobAccess.filtered_queryset() method used a 4-way OR to
determine which unified jobs a user can see. Under load, the OR forces
PostgreSQL to evaluate all four branches in a single plan, preventing
branch-specific index optimization and causing 35 hours of DB time per
30-minute Scale Lab window.

Split each RBAC branch (template read_role, inventory update, ad-hoc
command, org auditor) into separate querysets combined with UNION, giving
the planner an independent optimal plan per branch. The UNION result is
wrapped in pk__in= for compatibility with BaseAccess.get_queryset()
prefetch_related and the workflowapproval filter.

This follows the same pattern proven in AAP-83319 (team list UNION fix).

* AAP-81173 — Add UnifiedJobPagination to prevent COUNT regression

The pk__in UNION pattern used for RBAC filtering forces the large
outer table as the driving table for COUNT(*), requiring PostgreSQL
to materialize all subquery result sets.  On large deployments this
produces catastrophic query times (see AAP-83773 for the identical
issue on activity_stream).

Override the paginator count to use an unfiltered
UnifiedJob.objects.count() — the over-count is acceptable for
pagination UI.  Also fix test_unified_job_list_rando_sees_nothing
to assert on results length instead of count, since count is now
unfiltered.

* AAP-81173 — Add unit tests for UnifiedJobPagination coverage

Cover the UnifiedJobPaginator.count cached property and the
count_disabled branch in UnifiedJobPagination.paginate_queryset
to satisfy SonarCloud's 90% new-code coverage gate.

* AAP-81173 — Address review feedback: save/restore paginator class, format consistency

- Fix Pagination.paginate_queryset() to save/restore django_paginator_class
  instead of hardcoding DjangoPaginator in the finally block. This lets
  subclasses set the class attribute without needing to override the method.
- Remove UnifiedJobPagination.paginate_queryset() override — now only needs
  to set django_paginator_class = UnifiedJobPaginator as a class attribute.
- Format by_org_auditor consistently with the other three UNION branches.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dirk Julich
2026-08-03 16:27:46 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9d67c302fd
commit c84575c215
5 changed files with 220 additions and 23 deletions
+14 -7
View File
@@ -13,7 +13,7 @@ from rest_framework.utils.urls import replace_query_param
from rest_framework.settings import api_settings
from django.utils.translation import gettext_lazy as _
from awx.main.models import ActivityStream
from awx.main.models import ActivityStream, UnifiedJob
class DisabledPaginator(DjangoPaginator):
@@ -27,18 +27,21 @@ class DisabledPaginator(DjangoPaginator):
class ActivityStreamPaginator(DjangoPaginator):
"""Use unfiltered table count for activity stream pagination (AAP-83773).
The RBAC-filtered COUNT query takes ~36 min on large tables due to the
pk__in subquery shape from AAP-81860. An unfiltered count is acceptable
for pagination UI -- an approximate over-count is harmless.
"""
"""Use unfiltered table count for activity stream pagination (AAP-83773)."""
@cached_property
def count(self):
return ActivityStream.objects.count()
class UnifiedJobPaginator(DjangoPaginator):
"""Use unfiltered table count for unified job pagination."""
@cached_property
def count(self):
return UnifiedJob.objects.count()
class Pagination(pagination.PageNumberPagination):
page_size_query_param = 'page_size'
max_page_size = settings.MAX_PAGE_SIZE
@@ -91,6 +94,10 @@ class ActivityStreamPagination(Pagination):
django_paginator_class = ActivityStreamPaginator
class UnifiedJobPagination(Pagination):
django_paginator_class = UnifiedJobPaginator
class LimitPagination(pagination.BasePagination):
default_limit = api_settings.PAGE_SIZE
limit_query_param = 'limit'
+2 -1
View File
@@ -129,7 +129,7 @@ from awx.api.views.mixin import (
NoTruncateMixin,
UnifiedJobExcludeMixin,
)
from awx.api.pagination import ActivityStreamPagination, UnifiedJobEventPagination
from awx.api.pagination import ActivityStreamPagination, UnifiedJobEventPagination, UnifiedJobPagination
from awx.main.utils import set_environ
logger = logging.getLogger('awx.api.views')
@@ -4573,6 +4573,7 @@ class UnifiedJobList(UnifiedJobExcludeMixin, ListAPIView):
model = models.UnifiedJob
serializer_class = serializers.UnifiedJobListSerializer
search_fields = ('description', 'name', 'job__playbook')
pagination_class = UnifiedJobPagination
resource_purpose = 'unified jobs'