[WIP] - Upstream 16530 - Optimize HostList API: conditional DISTINCT + composite index on JobHostSummary#560
Closed
cigamit wants to merge 18 commits into
Closed
[WIP] - Upstream 16530 - Optimize HostList API: conditional DISTINCT + composite index on JobHostSummary#560cigamit wants to merge 18 commits into
cigamit wants to merge 18 commits into
Conversation
…ite index on JobHostSummary
Contributor
There was a problem hiding this comment.
Pull request overview
Optimizes the HostList API query plan by reducing unnecessary DISTINCT usage and improving the correlated “latest JobHostSummary per host” lookup via a composite index on JobHostSummary.
Changes:
- Makes
HostList.get_queryset()apply.distinct()conditionally instead of unconditionally. - Adds a composite DB index on
JobHostSummary (host_id, id DESC)to speed up thewith_latest_summary_id()correlated subquery. - Adds the corresponding Django migration for the new index.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| awx/api/views/init.py | Makes distinct() conditional in HostList.get_queryset() before annotating latest summary id. |
| awx/main/models/jobs.py | Adds a composite (host, -id) index on JobHostSummary to accelerate “latest summary per host” queries. |
| awx/main/migrations/0201_jobhostsummary_main_jobhostsumm_host_id_desc.py | Introduces the migration that adds the new composite index. |
* Add artifact-based conditional workflow connectors (backend) Implements the backend for #505: a fourth workflow connector type that only activates the downstream node when the parent job finishes with the selected outcome AND a condition over its artifacts (set_stats data) evaluates to true. - New condition_nodes m2m on template and job workflow nodes, backed by through models storing trigger (success/failure/always, default success), artifact_key, operator (eq, ne) and expected_value - Workflow DAG engine evaluates conditions against the parent node's ancestor_artifacts merged with its own job artifacts (same semantics as artifact propagation to downstream extra_vars); the edge only fires when the parent outcome matches the trigger; nodes whose unified job template was deleted are treated as failures for condition routing - A condition edge only counts as an error handling path for a failed node when it will actually fire (trigger covers the failure and the condition holds); otherwise the workflow is marked failed, matching failure_nodes semantics - Artifact comparison parses expected_value as JSON with a bool/int cross-type guard (True does not match 1), falling back to string comparison - API sublists /workflow_job_template_nodes/N/condition_nodes/ and /workflow_job_nodes/N/condition_nodes/; association POST accepts trigger, artifact_key, operator and expected_value; re-posting updates the condition; node serializers expose condition_nodes and condition_edges (through rows prefetched to avoid N+1 on node lists) - Condition data is preserved on template-to-job instantiation, relaunch (including relaunch-from-failed carry-forward) and WFJT deep copy; the deep-copy through-row clone is idempotent and direction-aware - RBAC: connection management requires WFJT admin, same as other edges * Add On Condition workflow connector support to the UI - Link add/edit modal offers an On Condition run type with an Evaluate on selector (On Success by default, On Failure, Always) plus artifact key, operator (equals / not equals) and expected value fields - Add Node wizard Run step gains an On Condition card with the same fields; the in-flight condition values survive the launch-config resetForm, and wizard nav can no longer jump past a step whose Next button is disabled - Visualizer loads condition edges from condition_nodes/condition_edges, saves them through the condition_nodes association endpoint (re-posting updates the stored condition) and disassociates them like other links - Condition links render in warning color in the visualizer and the workflow job output view; the link tooltip shows the trigger and the condition; the legend documents the new connector - Node-level fields (identifier, nodeType, nodeResource) are stripped from promptValues when adding a node, fixing a pre-existing 400 from posting a blank identifier on node creation - New strings extracted to the i18n catalogs with Spanish translations * Add awxkit support for conditional workflow connectors Register the condition_nodes endpoints so awx export includes the fourth edge type instead of silently dropping it (an imported workflow would otherwise run condition children unconditionally as root nodes). Each exported edge keeps the target node natural key under 'id' plus its trigger, artifact_key, operator and expected_value, and the import posts the full association. Re-importing an existing link updates its condition in place, so export/import round-trips stay idempotent. Also adds WorkflowJobTemplateNode.add_condition_node for parity with the other add_*_node helpers.
…d host lists) (#562) * Add automatic ascender_stats_* job artifacts derived from playbook stats Every finished playbook job (jobs launched from job templates; project updates, inventory syncs and ad hoc commands are not affected) now automatically contributes a set of ascender_stats_* keys to its artifacts, computed from the final playbook_on_stats event without requiring set_stats in the playbook: - ascender_stats_changed / ascender_stats_failed booleans (failed includes unreachable hosts, matching the job event semantics) - ascender_stats_changed_hosts / ascender_stats_non_changed_hosts / ascender_stats_failed_hosts / ascender_stats_non_failed_hosts sorted host lists - ascender_stats_hosts_truncated, set when the play involved more hosts than ASCENDER_AUTO_STATS_MAX_HOSTS (default 100), in which case the host lists are omitted so artifacts stay small as they propagate through workflows; the boolean flags are always kept Since these are regular artifacts, they propagate to descendant workflow nodes as extra vars and can drive conditional workflow connectors (requested in #505: traverse a path only when the previous job reported changes). Keys set by the playbook via set_stats win over the automatic ones on collision, and _ansible_no_log redaction is unaffected. The feature is gated by the ASCENDER_AUTO_STATS_ENABLED setting (default true); both settings are exposed in the settings API (Jobs category) and can be overridden per job or per workflow with extra variables of the same name, resolved at the single per-job stats event so the event handler hot path is untouched. A negative max hosts override through extra vars is ignored in favor of the setting, which is itself validated with min_value=0. * Merge ascender_stats_* keys when aggregating artifacts of sibling jobs Every playbook job now emits the same ascender_stats_* key names, so aggregating artifacts with plain dict.update() let the last finished sibling mask the others: with a sliced job template, a failed host reported by slice 1 disappeared if slice 2 finished clean afterwards, so a conditional connector on ascender_stats_failed would not fire and the per-host lists passed downstream were wrong. The same applied to nested workflows and to several parents converging into one child. The ascender_stats_* keys are now merged where sibling artifacts meet (WorkflowJob.get_effective_artifacts and the ancestor artifacts built in get_job_kwargs): booleans are OR-ed and the host lists unioned, with truncation on any side dropping the merged lists. Everything else keeps last-writer-wins, and within a single branch a node's own output still supersedes what it inherited.
* Add webhook support to Projects to sync on repository push A push to the configured GitHub/GitLab/Bitbucket DC repository now triggers the same update the Sync button does, so the local copy of the project follows the repo without polling schedules or paying the update-on-launch penalty on every job. Projects reuse the existing webhook receivers and signature checks. Only push and tag push events start a sync, everything else is acknowledged and ignored. An optional fnmatch ref filter (webhook_ref_filter) limits which refs cause a sync, and project updates record webhook_service and webhook_guid so duplicate deliveries are dropped, same as jobs. The webhook credential handling stays on the template mixin; projects only take the service/key part (WebhookKeyTemplateMixin) since a sync has no status to post back. * Allow supplying your own webhook key Until now the webhook key could only be generated by the server, which does not play well with configuration as code: every time the resource is re-applied the key changes and the repository webhook has to be updated by hand. The webhook_key field is now also writable (write only) on projects, job templates and workflow job templates. When a key is supplied it is kept as is, so the same secret can be stored in a vault and applied to both the repository and the resource by automation. When the field is left blank the previous behavior remains: a new key is generated whenever the webhook service is set or changed, and blanking the key of an active webhook generates a fresh one. Keys are never returned on the resource itself, reading them still requires the webhook_key endpoint, and copies of a resource always get their own key. The key field in the UI is now an editable input with the same semantics. * Renumber webhook migration to 0202 to follow merged 0201 from PR #561 The conditional connectors PR (#561) already claimed migration 0201, so this moves the project webhooks migration to 0202 and updates its dependency chain accordingly.
We removed react-router-dom and added react-router, so also need to ensure the license files are properly add / removed.
Co-authored-by: test <test@test.com>
Workflow nodes get a max_retries setting (0-100, default 0). While a failed or errored node has retries left the scheduler treats it as still running instead of following its failure paths, and respawns its job on the next cycle with the same launch configuration. The node tracks the attempt count in retry_attempts and keeps superseded attempts in retried_jobs, which stay protected from deletion while the workflow runs. Canceled jobs, approval nodes, deleted templates and jobs that never started are not retried. The field lives on both the template node and the job node and is copied at launch, editable through the API, awxkit and the node form in the workflow visualizer.
* Allow pinning hosts to every slice of a sliced job Job slicing distributes the inventory evenly across slices, which breaks playbooks where a play targets a host the rest of the run depends on. The usual victim is a preparatory play against localhost: after slicing, only the slice that happened to receive localhost runs it, and the other slices miss whatever it set up. This adds a job_slice_pinned_hosts field to job templates: a comma separated list of inventory host names that are taken out of the round robin distribution and included in every slice instead, keeping their group memberships and variables. Names that do not match an inventory host are ignored and the field does nothing when the job is not sliced. Pinned hosts do not add to the work worth distributing, so they no longer count when capping the slice count to the number of hosts: an inventory of 4 hosts with one of them pinned yields at most 3 slices. The docs spell out the tradeoff: plays targeting a pinned host run once per slice, concurrently, so this is meant for idempotent coordination plays (localhost being the typical case). * Fix title case for Job Slice Pinned Hosts form label The form label used sentence case while the detail view and surrounding fields use title case. Align them for consistency. * Fix task impact counting and waitFor in pinned hosts test Use inventory query instead of len() for pinned host count in _get_task_impact so duplicates and unknown names are excluded. Fix waitFor in the test to throw when element is absent so it actually waits for the re-render.
…ad of failing (#573) When exporting resources (e.g. job_templates), the awxkit export code iterates over POST fields from the OPTIONS response. For write_only fields like webhook_key, the key appears in both post_fields and _page.related (added by the serializer's get_related), but the resolved endpoint (e.g. /api/v2/job_templates/N/webhook_key/) is served by a view whose page class (Base) has no NATURAL_KEY defined. Previously this caused the export to log an ERROR, set _has_error=True, and return None for every affected resource, producing empty asset lists. Fix: 1. Phase A (direct fields): When the resolved related endpoint has no NATURAL_KEY attribute, log a warning and skip the field instead of erroring out. 2. Documentation: Add webhook_key to DEPENDENT_NONEXPORT for Project, JobTemplate, and WorkflowJobTemplate. Co-authored-by: test <test@test.com>
…bes (#576) The task pod readiness probe runs awx-manage check_instance_ready when task_readiness_period is greater than 0, but the command was missing from this repo, so task pods never became ready. Bring in the command as it exists in upstream AWX (identical in 24.6.1 and devel): exit non-zero unless the local instance node_state is ready. Fixes #571 Co-authored-by: test <test@test.com>
#575) Commit c8eeb60 (#559) added user=root to the [supervisord] section of tools/docker-compose/supervisor.conf, but the docker-compose development container runs as the invoking user (uid 1000), not root. supervisord refuses to start with "Error: Can't drop privilege as nonroot user", so the awx container exits right after bootstrap and make docker-compose is broken on main. Removing the line restores the previous behavior: supervisord runs as whatever user launched it, which is what the development container expects. Co-authored-by: test <test@test.com>
…aries (#577) The job_host_summaries API endpoint for hosts and groups in constructed inventories was returning empty results because it queried the host FK (which points to the source inventory host) instead of the constructed_host FK. This matches the fix already present in the serializer for recent_jobs. Also adds a relink step after inventory sync that reconnects orphaned JobHostSummary records (where the host FK became NULL after the host was deleted by an overwrite sync) to the new host object by matching on host_name, scoped to the same inventory to prevent cross-inventory mis-linking.
* Add context_template to workflow approval nodes for approver visibility Approval nodes can now include a Jinja2 context_template that gets rendered with upstream set_stats artifacts when the approval is created. The rendered result is stored as context_message on the WorkflowApproval instance and displayed to the approver in the UI detail view and in the pending approval notification. This lets workflow authors surface relevant context (dry-run output, planned changes, terraform plan summaries) directly to the person who needs to approve, without them having to go find the previous job output. * Harden approval context_template rendering against resource exhaustion The Jinja2 sandbox blocks attribute escapes but not templates that burn CPU or memory, so rendering now happens in a short lived forked process with RLIMIT_CPU and RLIMIT_AS applied, and the task manager abandons and kills it after a hard timeout instead of blocking the scheduler loop. The rendered output is capped at 64KB. Also render with the artifacts passed as a context dict instead of **kwargs, so set_stats keys that are not valid identifiers cannot raise TypeError, and allow static templates to render when a node has no upstream artifacts. Any child process failure is logged and the approval is simply created without a context message. Adds functional tests covering rendering, static templates, weird artifact keys, template errors, the render timeout, the CPU and memory limits and output truncation. * Keep context_message out of the workflow approval list serializer The rendered context can be large, so only the detail endpoint returns it. The UI detail view already reads it from the detail endpoint. Adds an API test asserting the field shows up on detail and not on the list.
Comment on lines
439
to
441
| endpoint.post(post_data) | ||
| log.error("endpoint: %s, id: %s", endpoint.endpoint, rel_page['id']) | ||
| self._has_error = True |
| additional_galaxy_env: | ||
| # These paths control where ansible-galaxy installs collections and roles on top the filesystem | ||
| ANSIBLE_COLLECTIONS_PATHS: "{{ projects_root }}/.__awx_cache/{{ local_path }}/stage/requirements_collections" | ||
| ANSIBLE_COLLECTIONS_PATH: "{{ projects_root }}/.__awx_cache/{{ local_path }}/stage/requirements_collections" |
|
|
||
|
|
||
| class Command(BaseCommand): | ||
| help = 'Check if the task manager instance is ready throw error if not ready, can be use as readiness probe for k8s.' |
Comment on lines
56
to
+59
| remainingValues.webhook_credential = webhook_credential?.id || null; | ||
| if (webhook_key) { | ||
| remainingValues.webhook_key = webhook_key; | ||
| } |
Comment on lines
59
to
+63
| remainingValues.project = project.id; | ||
| remainingValues.webhook_credential = webhook_credential?.id; | ||
| if (webhook_key) { | ||
| remainingValues.webhook_key = webhook_key; | ||
| } |
Comment on lines
38
to
+41
| templatePayload.webhook_credential = webhook_credential?.id || null; | ||
| if (webhook_key) { | ||
| templatePayload.webhook_key = webhook_key; | ||
| } |
Comment on lines
32
to
+35
| templatePayload.webhook_credential = webhook_credential?.id; | ||
| if (webhook_key) { | ||
| templatePayload.webhook_key = webhook_key; | ||
| } |
Comment on lines
+37
to
+39
| if (webhook_key) { | ||
| values.webhook_key = webhook_key; | ||
| } |
Comment on lines
+36
to
+38
| if (webhook_key) { | ||
| values.webhook_key = webhook_key; | ||
| } |
Comment on lines
1661
to
+1668
| def get_queryset(self): | ||
| qs = super(HostList, self).get_queryset() | ||
| filter_string = self.request.query_params.get('host_filter', None) | ||
| if filter_string: | ||
| filter_qs = SmartFilter.query_from_string(filter_string) | ||
| qs &= filter_qs | ||
| return qs.distinct().with_latest_summary_id() | ||
| qs = qs.distinct() | ||
| return qs.with_latest_summary_id() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Upstream Summary
.distinct()conditional onhost_filterquery parameter being set — without it, the RBACINsubquery on a direct FK (inventory_id) cannot produce duplicates, soDISTINCTis pure overhead forcing PostgreSQL to sort/hash-dedup the entire result set(host_id, id DESC)onmain_jobhostsummaryso thewith_latest_summary_id()correlated subquery can use an index-only top-1 scan instead of scanning and sorting per rowThe
host_list_rbacquery pattern is the #2 DB time consumer in Scale Lab testing at 2,871 seconds total, and is worsening (+15%). Unlike AAP-81082, the RBAC subquery itself is clean (simpleINon a direct FK) — the cost is dominated by the unconditionalDISTINCTand the correlated subquery lacking a composite index.Why
.distinct()is safe to make conditionalSmartFilter.query_from_string()can filter through M2M relationships (e.g.,groups__name=...), which produce duplicate rows via JOINs..distinct()is only needed whenhost_filteris set. Withouthost_filter, the queryset flows throughHostAccess.filtered_queryset()which filters oninventory_id IN (RBAC subquery)— a direct FK that cannot produce duplicates.Why the composite index helps
with_latest_summary_id()adds a correlated subquery:This runs for every result row. The existing auto-index on
host_idalone requires scanning all matching rows then sorting. The composite(host_id, id DESC)index enables a single backward index scan to fetch the top-1 result directly.EXPLAIN analysis (Scale Lab, Jul 2)
Ran on the aap26-next read replica (37,824 hosts, 16.7M job host summaries, 320K role evaluations).
The correlated subquery is the smoking gun — it scans the entire 367MB PK index backward with a filter instead of using a targeted index:
Cost 624,118 per host row. With ~23K summaries per host on average (max 92K), each probe is extremely expensive.
The correlated subquery adds +4,167 cost (56% of total). DISTINCT adds +8 in planner estimate but forces Unique + Sort on 138-byte rows.
Test plan
(host_id, id DESC)index existshost_list_rbacmean query time improvement on Scale LabClassification
New or Enhanced Feature
Fixes: https://issues.redhat.com/browse/AAP-81517