Skip to content

[autoscaling/cluster] Use target NodePool as base for replica - #50458

Closed
jennchenn wants to merge 6 commits into
mainfrom
jenn/cluster-autoscaling-replica-merge
Closed

[autoscaling/cluster] Use target NodePool as base for replica#50458
jennchenn wants to merge 6 commits into
mainfrom
jenn/cluster-autoscaling-replica-merge

Conversation

@jennchenn

@jennchenn jennchenn commented May 7, 2026

Copy link
Copy Markdown
Member

What does this PR do?

When the cluster-agent autoscaling controller produces a Datadog-managed replica from a user-created target NodePool, use the target as the merge base and overlay RC values selectively, instead of building from RC alone.

  • Always replaced from RC (even if empty): top-level labels/annotations, Spec.Template.Spec.Requirements
  • RC-overrides-when-set, else target preserved: NodeClassRef, Disruption, ExpireAfter, Limits, Taints, StartupTaints, TerminationGracePeriod, Replicas
  • Weight: RC if set, else target.Weight + 1
  • Template labels/annotations: RC merged on top of target's
  • ⚠️ Removes the blocking targetHash check: we now proceed to update/create a NodePool even if the target has changed since the recommendation so we can pull in changes to target NodePool faster

Standalone path (no target) is unchanged.

Motivation

Restores merge semantics from #42171. Without this, operator-edited fields on the target silently disappear from the replica because the RC manifest only carries what the recommendation engine knows about.

Describe how you validated your changes

  • Unit tests in pkg/clusteragent/autoscaling/cluster/model/ cover all merge cases.
  • Manual validation in a live cluster: target with operator-set Limits, Disruption, ExpireAfter, TerminationGracePeriod, Weight, labels/annotations → replica preserved target spec fields, replaced top-level metadata from RC, merged template metadata, narrowed requirements per RC, and Spec.Weight = target+1.

Additional Notes

🤖 Generated with Claude Code

@dd-octo-sts dd-octo-sts Bot added the internal Identify a non-fork PR label May 7, 2026
@github-actions github-actions Bot added the medium review PR review might take time label May 7, 2026
@datadog-prod-us1-4

datadog-prod-us1-4 Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 58.46%
Overall Coverage: 49.89% (-0.01%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 219c4de | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Files inventory check summary

File checks results against ancestor 95c0fc54:

Results for datadog-agent_7.80.0~devel.git.798.219c4de.pipeline.113222045-1_amd64.deb:

No change detected

@dd-octo-sts

dd-octo-sts Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Static quality checks

✅ Please find below the results from static quality gates
Comparison made with ancestor 95c0fc5
📊 Static Quality Gates Dashboard
🔗 SQG Job

Successful checks

Info

Quality gate Change Size (prev → curr → max)
docker_cluster_agent_amd64 +8.0 KiB (0.00% increase) 206.833 → 206.841 → 207.710
31 successful checks with minimal change (< 2 KiB)
Quality gate Current Size
agent_deb_amd64 743.482 MiB
agent_deb_amd64_fips 701.513 MiB
agent_heroku_amd64 309.683 MiB
agent_rpm_amd64 743.466 MiB
agent_rpm_amd64_fips 701.497 MiB
agent_rpm_arm64 721.209 MiB
agent_rpm_arm64_fips 682.362 MiB
agent_suse_amd64 743.466 MiB
agent_suse_amd64_fips 701.497 MiB
agent_suse_arm64 721.209 MiB
agent_suse_arm64_fips 682.362 MiB
docker_agent_amd64 803.628 MiB
docker_agent_arm64 806.184 MiB
docker_agent_jmx_amd64 994.547 MiB
docker_agent_jmx_arm64 985.882 MiB
docker_cluster_agent_arm64 220.826 MiB
docker_cws_instrumentation_amd64 7.154 MiB
docker_cws_instrumentation_arm64 6.689 MiB
docker_dogstatsd_amd64 39.499 MiB
docker_dogstatsd_arm64 37.690 MiB
docker_host_profiler_amd64 302.254 MiB
docker_host_profiler_arm64 313.761 MiB
dogstatsd_deb_amd64 30.155 MiB
dogstatsd_deb_arm64 28.280 MiB
dogstatsd_rpm_amd64 30.155 MiB
dogstatsd_suse_amd64 30.155 MiB
iot_agent_deb_amd64 44.377 MiB
iot_agent_deb_arm64 41.346 MiB
iot_agent_deb_armhf 42.055 MiB
iot_agent_rpm_amd64 44.377 MiB
iot_agent_suse_amd64 44.377 MiB

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented May 7, 2026

Copy link
Copy Markdown

Regression Detector

Regression Detector Results

Metrics dashboard
Target profiles
Run ID: 50f028e7-fc54-4310-a39c-fa6bf65dee2d

Baseline: 95c0fc5
Comparison: 219c4de
Diff

Optimization Goals: ✅ No significant changes detected

Experiments ignored for regressions

Regressions in experiments with settings containing erratic: true are ignored.

perf experiment goal Δ mean % Δ mean % CI trials links
docker_containers_cpu % cpu utilization -0.23 [-3.10, +2.65] 1 Logs

Fine details of change detection per experiment

perf experiment goal Δ mean % Δ mean % CI trials links
tcp_syslog_to_blackhole ingress throughput +2.06 [+1.88, +2.25] 1 Logs
quality_gate_logs % cpu utilization +1.17 [+0.18, +2.16] 1 Logs bounds checks dashboard
otlp_ingest_metrics memory utilization +0.46 [+0.31, +0.62] 1 Logs
uds_dogstatsd_20mb_12k_contexts_20_senders memory utilization +0.40 [+0.36, +0.45] 1 Logs
ddot_logs memory utilization +0.16 [+0.10, +0.22] 1 Logs
ddot_metrics_sum_cumulative memory utilization +0.13 [-0.03, +0.29] 1 Logs
docker_containers_memory memory utilization +0.06 [-0.05, +0.17] 1 Logs
file_to_blackhole_500ms_latency egress throughput +0.03 [-0.38, +0.44] 1 Logs
quality_gate_idle memory utilization +0.02 [-0.03, +0.06] 1 Logs bounds checks dashboard
uds_dogstatsd_to_api_v3 ingress throughput +0.01 [-0.20, +0.22] 1 Logs
uds_dogstatsd_to_api ingress throughput +0.00 [-0.20, +0.21] 1 Logs
tcp_dd_logs_filter_exclude ingress throughput -0.00 [-0.09, +0.09] 1 Logs
file_to_blackhole_0ms_latency egress throughput -0.01 [-0.54, +0.52] 1 Logs
file_tree memory utilization -0.01 [-0.06, +0.04] 1 Logs
ddot_metrics memory utilization -0.02 [-0.21, +0.18] 1 Logs
file_to_blackhole_1000ms_latency egress throughput -0.02 [-0.45, +0.41] 1 Logs
ddot_metrics_sum_delta memory utilization -0.04 [-0.23, +0.16] 1 Logs
file_to_blackhole_100ms_latency egress throughput -0.05 [-0.20, +0.10] 1 Logs
quality_gate_idle_all_features memory utilization -0.08 [-0.11, -0.04] 1 Logs bounds checks dashboard
docker_containers_cpu % cpu utilization -0.23 [-3.10, +2.65] 1 Logs
ddot_metrics_sum_cumulativetodelta_exporter memory utilization -0.35 [-0.59, -0.12] 1 Logs
quality_gate_metrics_logs memory utilization -0.67 [-0.92, -0.43] 1 Logs bounds checks dashboard
otlp_ingest_logs memory utilization -0.76 [-0.86, -0.66] 1 Logs

Bounds Checks: ✅ Passed

perf experiment bounds_check_name replicates_passed observed_value links
docker_containers_cpu simple_check_run 10/10 712 ≥ 26
docker_containers_memory memory_usage 10/10 246.75MiB ≤ 370MiB
docker_containers_memory simple_check_run 10/10 692 ≥ 26
file_to_blackhole_0ms_latency memory_usage 10/10 0.16GiB ≤ 1.20GiB
file_to_blackhole_0ms_latency missed_bytes 10/10 0B = 0B
file_to_blackhole_1000ms_latency memory_usage 10/10 0.20GiB ≤ 1.20GiB
file_to_blackhole_1000ms_latency missed_bytes 10/10 0B = 0B
file_to_blackhole_100ms_latency memory_usage 10/10 0.17GiB ≤ 1.20GiB
file_to_blackhole_100ms_latency missed_bytes 10/10 0B = 0B
file_to_blackhole_500ms_latency memory_usage 10/10 0.18GiB ≤ 1.20GiB
file_to_blackhole_500ms_latency missed_bytes 10/10 0B = 0B
quality_gate_idle intake_connections 10/10 3 ≤ 4 bounds checks dashboard
quality_gate_idle memory_usage 10/10 143.44MiB ≤ 147MiB bounds checks dashboard
quality_gate_idle_all_features intake_connections 10/10 3 ≤ 4 bounds checks dashboard
quality_gate_idle_all_features memory_usage 10/10 472.49MiB ≤ 495MiB bounds checks dashboard
quality_gate_logs intake_connections 10/10 3 ≤ 6 bounds checks dashboard
quality_gate_logs memory_usage 10/10 175.04MiB ≤ 195MiB bounds checks dashboard
quality_gate_logs missed_bytes 10/10 0B = 0B bounds checks dashboard
quality_gate_metrics_logs cpu_usage 10/10 368.41 ≤ 2000 bounds checks dashboard
quality_gate_metrics_logs intake_connections 10/10 3 ≤ 6 bounds checks dashboard
quality_gate_metrics_logs memory_usage 10/10 377.43MiB ≤ 430MiB bounds checks dashboard
quality_gate_metrics_logs missed_bytes 10/10 0B = 0B bounds checks dashboard

Explanation

Confidence level: 90.00%
Effect size tolerance: |Δ mean %| ≥ 5.00%

Performance changes are noted in the perf column of each table:

  • ✅ = significantly better comparison variant performance
  • ❌ = significantly worse comparison variant performance
  • ➖ = no significant change in performance

A regression test is an A/B test of target performance in a repeatable rig, where "performance" is measured as "comparison variant minus baseline variant" for an optimization goal (e.g., ingress throughput). Due to intrinsic variability in measuring that goal, we can only estimate its mean value for each experiment; we report uncertainty in that value as a 90.00% confidence interval denoted "Δ mean % CI".

For each experiment, we decide whether a change in performance is a "regression" -- a change worth investigating further -- if all of the following criteria are true:

  1. Its estimated |Δ mean %| ≥ 5.00%, indicating the change is big enough to merit a closer look.

  2. Its 90.00% confidence interval "Δ mean % CI" does not contain zero, indicating that if our statistical model is accurate, there is at least a 90.00% chance there is a difference in performance between baseline and comparison variants.

  3. Its configuration does not mark it "erratic".

CI Pass/Fail Decision

Passed. All Quality Gates passed.

  • quality_gate_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.
  • quality_gate_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check missed_bytes: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check cpu_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_metrics_logs, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_idle, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_idle, bounds check memory_usage: 10/10 replicas passed. Gate passed.
  • quality_gate_idle_all_features, bounds check intake_connections: 10/10 replicas passed. Gate passed.
  • quality_gate_idle_all_features, bounds check memory_usage: 10/10 replicas passed. Gate passed.

@jennchenn jennchenn added changelog/no-changelog No changelog entry needed qa/done QA done before merge and regressions are covered by tests team/container-autoscaling labels May 14, 2026
@jennchenn

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48c855d4da

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +160 to +163
func mergeSlices[T any](rc, target []T) []T {
if len(rc) > 0 {
return rc
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit empty taint overrides in replica merge

mergeSlices uses len(rc) > 0 to decide whether recommendation values are set, which conflates an omitted slice with an explicitly provided empty slice. In replica mode, if a recommendation intentionally sets taints: [] or startupTaints: [] to clear inherited taints from the target NodePool, this code preserves the target taints instead of applying the clear, so stale taints remain on the Datadog-managed NodePool.

Useful? React with 👍 / 👎.

Comment on lines +199 to +200
if len(rc.Spec.Limits) > 0 {
merged.Spec.Limits = rc.Spec.Limits

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty limits map as an explicit override

The limits merge only applies RC values when len(rc.Spec.Limits) > 0, so an explicit empty limits object from the recommendation is treated as "unset" and the target limits are retained. That prevents replica recommendations from removing existing spec.limits inherited from the target NodePool, which breaks the documented "RC overrides when set" behavior.

Useful? React with 👍 / 👎.

jennchenn and others added 3 commits May 14, 2026 15:49
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The replica merge used the target NodePool fetched from the cluster as
its base, carrying ResourceVersion/UID/Generation/CreationTimestamp/
ManagedFields into the merged result. Sending that as a Create made the
API server reject it with "resourceVersion should not be set on objects
to be created".

Replace the top-level ObjectMeta wholesale with a fresh struct so server-
set fields are zero by construction, and exercise the path in fixture +
assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two small generic helpers that express "RC wins if set, else
preserve target" without repeating nil/len guards for every field.
Replace reflect.DeepEqual zero-checks with explicit field checks
(ConsolidationPolicy != "" for Disruption, Duration/Raw for
ExpireAfter), dropping the reflect import.
@jennchenn jennchenn changed the title Use target NodePool as base for replica merge [autoscaling/cluster] Use target NodePool as base for replica May 14, 2026
@jennchenn
jennchenn force-pushed the jenn/cluster-autoscaling-replica-merge branch from 48c855d to 219c4de Compare May 14, 2026 16:42
@jennchenn
jennchenn marked this pull request as ready for review May 14, 2026 16:42
@jennchenn
jennchenn requested a review from a team as a code owner May 14, 2026 16:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 219c4de7e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

// NodeClaimTemplateSpec fields: use RC values if set, else fallback to target
merged.Spec.Template.Spec.Requirements = mergeSlices(rc.Spec.Template.Spec.Requirements, merged.Spec.Template.Spec.Requirements)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor explicit empty requirements override in replica merge

When building replica NodePools, this assignment uses mergeSlices (which checks len(rc) > 0), so a recommendation that intentionally sets spec.template.spec.requirements: [] is treated the same as “unset” and the target NodePool’s requirements are kept. In replica mode that prevents clearing inherited constraints from the target, so stale requirements continue to restrict scheduling instead of applying the recommendation’s explicit empty override.

Useful? React with 👍 / 👎.


// NodePoolSpec fields: use RC values if set, else fallback to target
if rc.Spec.Weight != nil {
merged.Spec.Weight = rc.Spec.Weight

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this is lower than the targetNp? I think we should always check the targetNp weight.

Comment on lines +167 to +168
Labels: maps.Clone(rc.Labels),
Annotations: maps.Clone(rc.Annotations),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not give preference to the targetNp here?


func GetNodePoolWeight(replicaNp *karpenterv1.NodePool) *int32 {
// BuildReplicaNodePool produces a NodePool for a Datadog-managed replica of an existing target NodePool
// The target is used as the base and RC values are applied on top:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do a merge? I would think we would prefer the target's values if there is a conflict.

}

// NodeClaimTemplateSpec fields: use RC values if set, else fallback to target
merged.Spec.Template.Spec.Requirements = mergeSlices(rc.Spec.Template.Spec.Requirements, merged.Spec.Template.Spec.Requirements)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For requirements, we should check if there are some conflicts that can't be overcome with a merge, like arch/os for example. If they do, then we can't proceed with the merge.

@dd-octo-sts

dd-octo-sts Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had activity in the past 15 days.

It will be closed in 30 days if no further activity occurs. If this pull request is still relevant, adding a comment or pushing new commits will keep it open. Also, you can always reopen the pull request if you missed the window.

Thank you for your contributions!

@dd-octo-sts dd-octo-sts Bot added the stale label Jun 4, 2026
@jennchenn

Copy link
Copy Markdown
Member Author

No longer necessary as we are now sending the entire spec from the backend

@jennchenn jennchenn closed this Jun 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/no-changelog No changelog entry needed internal Identify a non-fork PR medium review PR review might take time qa/done QA done before merge and regressions are covered by tests stale team/container-autoscaling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants