Skip to content

feat: inject NodeAffinity into DataBackup status after successful backup#6110

Open
adityaupasani2 wants to merge 1 commit into
fluid-cloudnative:masterfrom
adityaupasani2:feat/databackup-node-affinity
Open

feat: inject NodeAffinity into DataBackup status after successful backup#6110
adityaupasani2 wants to merge 1 commit into
fluid-cloudnative:masterfrom
adityaupasani2:feat/databackup-node-affinity

Conversation

@adityaupasani2

Copy link
Copy Markdown
Contributor

Ⅰ. Describe what this PR does

DataLoad, DataMigrate, and DataProcess all populate status.nodeAffinity after a successful operation using dataflow.GenerateNodeAffinity(job). This allows downstream runAfter operations to be scheduled on nodes where data is already cached, improving cache hit rates.

DataBackup was missing this because it uses a Pod instead of a Job, and dataflow.GenerateNodeAffinity() only accepts *batchv1.Job. As a result, DataBackup.Status.NodeAffinity was never set — breaking the dataflow affinity chain for any operation that depends on a DataBackup via runAfter.

This PR:

  • Adds dataflow.GenerateNodeAffinityFromPod(*corev1.Pod) in pkg/dataflow/helper.go using the same annotation-based logic as the existing GenerateNodeAffinity
  • Calls it in DataBackup.OnceHandler.GetOperationStatus() after the backup pod succeeds, same as other handlers do for jobs
  • Errors (e.g. no affinity annotations injected yet) are logged at V(1) and suppressed so the operation still completes successfully

Ⅱ. Does this pull request fix one issue?

Fixes #6107

Ⅲ. List the added test cases

  • TestGenerateNodeAffinityFromPod in pkg/dataflow/helper_test.go covering: nil pod, missing inject annotation, inject annotation with no affinity labels (error), inject annotation with labels (success)
  • Two new Ginkgo cases in status_handler_test.go: NodeAffinity injected when pod has dataflow annotations, NodeAffinity not injected when pod has no annotations

Ⅳ. Describe how to verify it

…ckup

DataLoad, DataMigrate, and DataProcess all populate status.NodeAffinity
after a successful operation so that downstream runAfter operations are
scheduled on nodes where data is already cached. DataBackup was missing
this because it uses a Pod (not a Job), and the existing
dataflow.GenerateNodeAffinity() only accepts *batchv1.Job.

Add dataflow.GenerateNodeAffinityFromPod(*corev1.Pod) in
pkg/dataflow/helper.go using the same annotation-based logic as
GenerateNodeAffinity, and call it in DataBackup OnceHandler after the
backup pod succeeds. Errors (e.g. no affinity annotations) are logged
at V(1) and suppressed so the operation still completes successfully.

Also adds:
- TestGenerateNodeAffinityFromPod in pkg/dataflow/helper_test.go
  covering nil pod, missing inject annotation, inject with no labels,
  and inject with labels
- Two new Ginkgo cases in status_handler_test.go asserting NodeAffinity
  is injected (with annotations) and not injected (without annotations)

Fixes fluid-cloudnative#6107

Signed-off-by: Aditya Upasani <adityaupasani29@gmail.com>
@fluid-e2e-bot

fluid-e2e-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign zwwhdls for approval by writing /assign @zwwhdls in a comment. For more information see:The Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@fluid-e2e-bot

fluid-e2e-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Hi @adityaupasani2. Thanks for your PR.

I'm waiting for a fluid-cloudnative member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces the ability to inject NodeAffinity into DataBackup status from a successful backup pod's annotations, implementing GenerateNodeAffinityFromPod along with corresponding unit tests. The review feedback suggests avoiding the reuse of the named return parameter err for transient errors to prevent side effects, and extracting the shared annotation-parsing logic into a helper function to eliminate code duplication between pod and job affinity generation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +56 to +62
if kubeclient.IsSucceededPod(backupPod) && result.NodeAffinity == nil {
result.NodeAffinity, err = dataflow.GenerateNodeAffinityFromPod(backupPod)
if err != nil {
ctx.Log.V(1).Info("NodeAffinity not injected", "reason", err.Error())
err = nil
}
}

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.

medium

Reusing the named return parameter err for a transient error inside a nested block can be error-prone and may lead to accidental side effects if the function is refactored in the future. It is safer and more idiomatic in Go to use a local variable (e.g., affinityErr) to handle this transient error.

Suggested change
if kubeclient.IsSucceededPod(backupPod) && result.NodeAffinity == nil {
result.NodeAffinity, err = dataflow.GenerateNodeAffinityFromPod(backupPod)
if err != nil {
ctx.Log.V(1).Info("NodeAffinity not injected", "reason", err.Error())
err = nil
}
}
if kubeclient.IsSucceededPod(backupPod) && result.NodeAffinity == nil {
nodeAffinity, affinityErr := dataflow.GenerateNodeAffinityFromPod(backupPod)
if affinityErr != nil {
ctx.Log.V(1).Info("NodeAffinity not injected", "reason", affinityErr.Error())
} else {
result.NodeAffinity = nodeAffinity
}
}

Comment thread pkg/dataflow/helper.go
Comment on lines +69 to +111
// GenerateNodeAffinityFromPod generates a NodeAffinity from a Pod's annotations,
// using the same annotation-based logic as GenerateNodeAffinity for Jobs.
// This is used by DataBackup which runs as a Pod rather than a Job.
func GenerateNodeAffinityFromPod(pod *corev1.Pod) (*corev1.NodeAffinity, error) {
if pod == nil {
return nil, nil
}
// not inject, i.e. feature gate not enabled
if v := pod.Annotations[common.AnnotationDataFlowAffinityInject]; v != "true" {
return nil, nil
}

annotations := pod.Annotations

nodeAffinity := &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{
{
MatchExpressions: nil,
},
},
},
}

hasInjectedLabels := false
for key, value := range annotations {
if strings.HasPrefix(key, common.AnnotationDataFlowCustomizedAffinityPrefix) {
nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions =
append(nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions,
corev1.NodeSelectorRequirement{
Key: strings.TrimPrefix(key, common.AnnotationDataFlowCustomizedAffinityPrefix),
Operator: corev1.NodeSelectorOpIn,
Values: []string{value},
})
hasInjectedLabels = true
}
}
if !hasInjectedLabels {
return nil, errors.New("the affinity label is not set, wait for next reconcile")
}

return nodeAffinity, nil
}

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.

medium

The logic in GenerateNodeAffinityFromPod is almost identical to GenerateNodeAffinity for Jobs, resulting in significant code duplication. We can extract the common annotation-parsing logic into a helper function GenerateNodeAffinityFromAnnotations(annotations map[string]string) and reuse it in both functions. This improves maintainability and readability.

// GenerateNodeAffinityFromAnnotations generates a NodeAffinity from annotations.
func GenerateNodeAffinityFromAnnotations(annotations map[string]string) (*corev1.NodeAffinity, error) {
	if v := annotations[common.AnnotationDataFlowAffinityInject]; v != "true" {
		return nil, nil
	}

	nodeAffinity := &corev1.NodeAffinity{
		RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
			NodeSelectorTerms: []corev1.NodeSelectorTerm{
				{
					MatchExpressions: nil,
				},
			},
		},
	}

	hasInjectedLabels := false
	for key, value := range annotations {
		if strings.HasPrefix(key, common.AnnotationDataFlowCustomizedAffinityPrefix) {
			nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions = 
				append(nodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions, 
					corev1.NodeSelectorRequirement{
						Key:      strings.TrimPrefix(key, common.AnnotationDataFlowCustomizedAffinityPrefix),
						Operator: corev1.NodeSelectorOpIn,
						Values:   []string{value},
					})
			hasInjectedLabels = true
		}
	}
	if !hasInjectedLabels {
		return nil, errors.New("the affinity label is not set, wait for next reconcile")
	}

	return nodeAffinity, nil
}

// GenerateNodeAffinityFromPod generates a NodeAffinity from a Pod's annotations,
// using the same annotation-based logic as GenerateNodeAffinity for Jobs.
// This is used by DataBackup which runs as a Pod rather than a Job.
func GenerateNodeAffinityFromPod(pod *corev1.Pod) (*corev1.NodeAffinity, error) {
	if pod == nil {
		return nil, nil
	}
	return GenerateNodeAffinityFromAnnotations(pod.Annotations)
}

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.80%. Comparing base (afca615) to head (6ffe9d7).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6110      +/-   ##
==========================================
+ Coverage   64.76%   64.80%   +0.03%     
==========================================
  Files         485      485              
  Lines       33895    33930      +35     
==========================================
+ Hits        21952    21987      +35     
  Misses      10220    10220              
  Partials     1723     1723              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cheyang

cheyang commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

/copilot review

@cheyang

cheyang commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Thanks for picking up the old TODO here. The mechanics of building the NodeAffinity look right and mirror the DataLoad/DataMigrate handlers, but I have one structural concern before this can land: I can't find anything that puts the dataflow-affinity.fluid.io.* annotations onto the DataBackup pod (that logic today only runs for Jobs), so in a real cluster this branch looks like it stays a no-op and Status.NodeAffinity never gets set. The unit tests pass only because they inject those annotations by hand. Could you point me at where the backup pod gets those annotations, or add the pod-side injection? An e2e test under test/gha-e2e/ exercising a DataBackup feeding a dependent dataflow op would both satisfy the feature-test requirement and settle this question. Left the details inline.


// TODO: inject nodeaffinity like other data operations when using job instead of pod
if kubeclient.IsSucceededPod(backupPod) && result.NodeAffinity == nil {
result.NodeAffinity, err = dataflow.GenerateNodeAffinityFromPod(backupPod)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I traced where the annotations this reads actually come from, and I don't think this branch ever fires in a real cluster. GenerateNodeAffinityFromPod returns early unless the pod carries dataflow-affinity.fluid.io.inject=true plus the affinity.dataflow.fluid.io.* node-label annotations. Those annotations are only ever written by DataOpJobReconciler in pkg/controllers/v1alpha1/fluidapp/dataflowaffinity/, whose ManagedResource() returns &batchv1.Job{} and which writes the labels onto the Job (injectPodNodeLabelsToJob). DataBackup runs as a Pod, not a Job — the pod template in charts/fluid-databackup/*/templates/databackup.yaml sets no such annotations, and there's no pod-level reconciler that backfills them. So for a real DataBackup, pod.Annotations[AnnotationDataFlowAffinityInject] != "true" and this returns (nil, nil); result.NodeAffinity stays nil. The new unit tests only pass because they hand-set those annotations on a fake pod. Where is the backup pod supposed to get the inject/affinity annotations? If there isn't a source yet, this needs the pod-side injection (equivalent of the Job reconciler) to actually deliver the feature.

}
}

func TestGenerateNodeAffinityFromPod(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a feat PR that adds a new user-visible behavior (populating DataBackup.Status.NodeAffinity so dataflow runAfter chains can schedule against it), but it only ships unit tests. The repo's e2e suite lives under test/gha-e2e/ (driven by .github/workflows/kind-e2e.yml), and there's no databackup/affinity scenario there. Please add an e2e case that runs a DataBackup and a dependent dataflow operation and asserts the affinity actually propagates end to end — that would also directly confirm or refute the annotation-source concern above, since a real run is exactly where the no-op would show up. If you believe an e2e case isn't feasible for this change, let's get a maintainer's call on that rather than skipping it.

result.NodeAffinity, err = dataflow.GenerateNodeAffinityFromPod(backupPod)
if err != nil {
ctx.Log.V(1).Info("NodeAffinity not injected", "reason", err.Error())
err = nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The the affinity label is not set, wait for next reconcile error is logged at V(1) and then cleared (err = nil), after which this same pass goes on to set Phase = Complete. Once the status is terminal there's no follow-up reconcile to backfill the affinity, so the "wait for next reconcile" wording doesn't match what happens for a backup. Separately, reusing the named return err inside this nested block is a bit fragile — a local variable (e.g. affinityErr) makes the intent clearer and avoids accidental leakage if this function grows later.

Comment thread pkg/dataflow/helper.go
// GenerateNodeAffinityFromPod generates a NodeAffinity from a Pod's annotations,
// using the same annotation-based logic as GenerateNodeAffinity for Jobs.
// This is used by DataBackup which runs as a Pod rather than a Job.
func GenerateNodeAffinityFromPod(pod *corev1.Pod) (*corev1.NodeAffinity, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

GenerateNodeAffinityFromPod is a near-verbatim copy of GenerateNodeAffinity — both only read .Annotations. Consider extracting the shared body into something like generateNodeAffinityFromAnnotations(annotations map[string]string) (*corev1.NodeAffinity, error) and having both the Job and Pod entrypoints delegate to it, so the two paths can't drift apart.

@adityaupasani2

Copy link
Copy Markdown
Contributor Author

@cheyang Thanks for tracing this all the way through you're right, and it's actually a bit worse than annotation-timing: even a pod-side reconciler mirroring DataOpJobReconciler would race against this handler's own Phase = Complete transition, since there's no follow-up reconcile once the backup is terminal.

Rather than replicate the Job path (separate reconciler writing dataflow-affinity.fluid.io/* annotations onto the pod, then reading them back), I'd like to compute the NodeAffinity directly in OnceHandler.GetOperationStatus() once we have the succeeded pod: read pod.Spec.NodeName, fetch the Node, and build result.NodeAffinity from its labels the same logic injectPodNodeLabelsToJob uses, just inlined instead of round-tripped through annotations. That removes the dependency on a second controller ever running, which seems like the right call for a Pod-backed operation that transitions to Complete in a single pass.

Before I rewrite this, does that direction make sense to you, or would you rather see a pod-side reconciler added to dataflowaffinity for consistency with the Job path (accepting the ordering risk, or with some safeguard against it)? Want to make sure I'm not solving this a third way before you've had a chance to weigh in.

Will also fold in the affinityErr local-var fix and extract the shared annotation/label-parsing helper regardless of which direction we land on, and add an e2e case under test/gha-e2e/ once the mechanism is settled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] DataBackup does not inject NodeAffinity into status after successful backup, unlike other data operations

2 participants