Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize updateAndSetOwner to Reduce Multiple Updates and Prevent Conflicts #1874

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

OdedViner
Copy link
Contributor

@OdedViner OdedViner commented Mar 2, 2025

This PR ensures that the updateAndSetOwner function updates the Placement and DRPC resources only once per reconciliation loop. Previously, multiple Update() calls caused resource version conflicts and delayed execution.

Changes Introduced:

  • Update Placement only after adding annotations and finalizers.
  • Update DRPC only after adding the finalizer and owner reference.
  • Added logs to track updates for easier debugging.

These optimizations reduce redundant updates, improving performance and stability.

Fixes: #1066

@OdedViner OdedViner force-pushed the fix_updateandsetowner branch 5 times, most recently from 6aae30e to 6a23c9d Compare March 3, 2025 12:00
Copy link
Member

@nirs nirs left a comment

Choose a reason for hiding this comment

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

You are doing too many changes at once - moving entire functions and modifying them. It is not possible to review such changes safely.

Try to do minimal changes:

  1. Modify existing tests or add new test to reproduce the failure. This is good first commit proving that the test is correct.
  2. Remove the actual update call from existing functions and add the update after we call all functions.

"context"
"testing"

"github.com/stretchr/testify/assert"
Copy link
Member

Choose a reason for hiding this comment

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

We don't use these kind of helpers in ramen tests. Check how existing tests are implemented and use the same code.

If you want to add new test infra it should be done separately from this pr.

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(drpc, placementObj).
Build()
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need fake client?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use a fake client to test updateAndSetOwner in isolation, ensuring it correctly updates DRPC and placement objects without needing a real cluster. This allows us to verify annotations, finalizers, and owner references while making debugging easier. I followed a similar approach used in Rook for clarity and consistency.
https://github.com/rook/rook/blob/9a5bf16247ce1b13e2475256ce20dc2f8bfce54c/pkg/operator/ceph/file/controller_test.go#L209

Of course, I will remove it from this PR, but my question is whether we can add unit tests to the controllers package. Many private functions (not starting with a capital letter) cannot be tested from the controller_test package.
Additionally, having unit tests in controllers could help team members navigate the code more easily. It helped me a lot when I started working on the OCS Operator and Rook projects.

@nirs @BenamarMk
What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image

drpcModified, placementModified, err := r.prepareObjectModifications(drpc, usrPlacement, log)
if err != nil {
return false, err
}
Copy link
Member

Choose a reason for hiding this comment

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

I would keep the old code - having small and focus functions that do one change instead of big and complex prepare function. Just remove the actual update from the existing functions, and finally run the actual update at the end.

We already have code doing this in other places, for example when deleting drpc we modify all resources and update only at the end.

}

// applyUpdates applies updates to both DRPC and placement objects if they were modified
func (r *DRPlacementControlReconciler) applyUpdates(
Copy link
Member

Choose a reason for hiding this comment

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

Adding updatePlacement() and updateDRPC() will be more useful and simpler. For example this can be used in other places that need to update only one of them.

placementModified = placementModified || modPlacement

// Modify DRPC to set the owner reference
mod, err = r.setDRPCOwner(drpc, usrPlacement, log)
Copy link
Member

Choose a reason for hiding this comment

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

ownerSet, err := ...

placementModified = placementModified || mod

// Modify DRPC and placement via metadata updates
modDRPC, modPlacement, err := r.updateObjectMetadata(drpc, usrPlacement)
Copy link
Member

Choose a reason for hiding this comment

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

This should really be 2 functions, one to update the placement and another for the drpc.

var drpcModified, placementModified bool

// Annotate the placement object
mod, err := r.annotateObject(drpc, usrPlacement, log)
Copy link
Member

Choose a reason for hiding this comment

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

placementAnnotated, err := ...

@OdedViner OdedViner force-pushed the fix_updateandsetowner branch 13 times, most recently from 71d2202 to 66c030e Compare March 9, 2025 13:58
update = rmnutil.AddLabel(drpc, rmnutil.OCMBackupLabelKey, rmnutil.OCMBackupLabelValue)
update = rmnutil.AddFinalizer(drpc, DRPCFinalizer) || update
added := rmnutil.AddFinalizer(drpc, DRPCFinalizer)
if added {
Copy link
Member

Choose a reason for hiding this comment

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

The "added" variable does not strictly need to be declared. You can inline the function call directly into the if statement

return false, err
}

return r.setDRPCOwner(ctx, drpc, usrPlacement, log)
// Merge both modifications
placementModified = placementAnnotateModified || placementMetadataModified
Copy link
Member

@ELENAGER ELENAGER Mar 16, 2025

Choose a reason for hiding this comment

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

placementModified can be computed inline, I mean something like this:

	if placementAnnotateModified || placementMetadataModified {
		if err := r.updatePlacement(ctx, usrPlacement, log); err != nil {
			return false, err
		}
	}

Copy link
Member

@nirs nirs left a comment

Choose a reason for hiding this comment

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

Still too big. We should not change so much code in one commit.

) (drpcModified bool, placementModified bool, err error) {
// Explicit initialization
drpcModified = false
placementModified = false
Copy link
Member

Choose a reason for hiding this comment

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

Not really needed. A better way would be to avoid named return values which lead to abuse.


vrgNamespace, err := selectVRGNamespace(r.Client, r.Log, drpc, placementObj)
if err != nil {
return err
return drpcModified, placementModified, err
Copy link
Member

Choose a reason for hiding this comment

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

When this fails we return with an error, but the drpc and placementObj may have changes that may fool the code using them after this point.

Maybe we need to call selectVRGNamespace() before we modify the objects?

placementModified = placementAnnotateModified || placementMetadataModified

// Set DRPC Owner
updated, err = r.setDRPCOwner(drpc, usrPlacement, log)
Copy link
Member

Choose a reason for hiding this comment

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

What is "updated"? it is not clear as the other xxxModified variables.


if err = r.updateDRPC(ctx, drpc, log); err != nil {
return false, err
}
Copy link
Member

Choose a reason for hiding this comment

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

We don't need to check if the drpc needs an update?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The DRPC CR updates every time without any condition.

log logr.Logger,
) error {
if err := r.Update(ctx, owner); err != nil {
log.Error(err, "Failed to update Placement object")
Copy link
Member

Choose a reason for hiding this comment

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

Don't log an error here, we return an error. This will log the same error multiple times.


func (r *DRPlacementControlReconciler) updatePlacement(
ctx context.Context,
owner client.Object,
Copy link
Member

Choose a reason for hiding this comment

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

owner is not clear. Based on the code this is the a placement or placementRule object. Check how we call this object in other code around. We should use the same name.

log logr.Logger,
) error {
if err := r.Update(ctx, drpc); err != nil {
log.Error(err, "Failed to update DRPC")
Copy link
Member

Choose a reason for hiding this comment

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

Don't log an error if you return the error.

}

func (r *DRPlacementControlReconciler) setDRPCOwner(
ctx context.Context, drpc *rmn.DRPlacementControl, owner client.Object, log logr.Logger,
drpc *rmn.DRPlacementControl, owner client.Object, log logr.Logger,
) (bool, error) {
const updated = true
Copy link
Member

Choose a reason for hiding this comment

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

updated is confusing now. Better rename to "needUpdate" or "dirty".

var update bool
func (r DRPlacementControlReconciler) updateObjectMetadata(
drpc *rmn.DRPlacementControl, placementObj client.Object,
) (drpcModified bool, placementModified bool, err error) {
Copy link
Member

Choose a reason for hiding this comment

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

Why we change both objects in this function? It will be better split this to 2 functions , each modifying one object. This avoids the error prone function return value (bool, bool, error). It is very easy to call the function in the wrong way:

drpcModified, placementModified, err := r.updateObjectMetadata()

placementModified, drpcModified, err := r.updateObjectMetadata()

Which call it the correct one? The type system does not help with such code.

return false, err
}

return updated, nil
Copy link
Member

Choose a reason for hiding this comment

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

This may return false when the drpc and/or placement were modified.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

correct, based on the original function

return r.setDRPCOwner(ctx, drpc, usrPlacement, log)
}


if rmnutil.AddFinalizer(drpc, DRPCFinalizer) {
modified = true
}

vrgNamespace, err := selectVRGNamespace(r.Client, r.Log, drpc, placementObj)
Copy link
Member

Choose a reason for hiding this comment

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

@OdedViner , maybe we can selectVRGNamespace and only then add finalizer and label?

@OdedViner OdedViner force-pushed the fix_updateandsetowner branch from c904262 to e1a8345 Compare March 19, 2025 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ensure updateAndSetOwner() updates Placement resource only once as well as DRPC
3 participants