Skip to content

fix: correctly identify alluxio root mount path in multi-mount datasets#6103

Open
Priya-Sharma25 wants to merge 1 commit into
fluid-cloudnative:masterfrom
Priya-Sharma25:fix/issue-3379-multi-mount-root
Open

fix: correctly identify alluxio root mount path in multi-mount datasets#6103
Priya-Sharma25 wants to merge 1 commit into
fluid-cloudnative:masterfrom
Priya-Sharma25:fix/issue-3379-multi-mount-root

Conversation

@Priya-Sharma25

Copy link
Copy Markdown

Ⅰ. Describe what this PR does

This PR fixes a bug where AlluxioRuntime fails to correctly mount the root path (/) if multiple mount points are defined in the dataset. Previously, GenAlluxioUFSRootPath only checked for the root path if len(items) == 1, defaulting to /underFSStorage for multi-mount scenarios. This changes the logic to iterate through all mount items to accurately identify the root path.

Ⅱ. Does this pull request fix one issue?

fixes #3379

Ⅲ. List the added test cases (unit test/integration test) if any, please explain if no tests are needed.

Added test multi mount item with root path to TestGenUFSRootPathForAlluxio in pkg/utils/ufs_path_builder_test.go to ensure root paths are successfully identified in multi-mount scenarios.

Ⅳ. Describe how to verify it

Run unit tests for ufs_path_builder_test.go:
go test ./pkg/utils/ -run "TestGenUFSRootPathForAlluxio" -v

Ⅴ. Special notes for reviews

NONE

@fluid-e2e-bot

fluid-e2e-bot Bot commented Jul 8, 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 8, 2026

Copy link
Copy Markdown

Hi @Priya-Sharma25. 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.

@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 updates GenAlluxioUFSRootPath to iterate through all mount items to find the root path in the cache UFS, rather than only checking when there is a single item, and adds a corresponding test case. A review comment correctly points out that taking the address of the loop variable m (&m) returns a pointer to a local copy of the slice element, which can lead to bugs or inefficiencies. The reviewer suggests iterating by index to safely reference the actual slice element.

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 thread pkg/utils/ufs_path_builder.go Outdated
Comment on lines 57 to 61
for _, m := range items {
// iff m matches all of the two following conditions (1) m is not a fluid-native mount point; (2) m.Path is "/", it should be the root path in cache UFS.
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(m) == common.RootDirPath {
return m.MountPoint, &m
}

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

Taking the address of the loop variable m (&m) returns a pointer to a local copy of the slice element rather than the actual element in the slice. If the caller modifies the returned mount, those changes will be lost. Additionally, copying the entire datav1alpha1.Mount struct on every iteration is inefficient.

To fix this, iterate by index and use a pointer to the slice element.

	for i := range items {
		m := &items[i]
		// iff m matches all of the two following conditions (1) m is not a fluid-native mount point; (2) m.Path is "/", it should be the root path in cache UFS.
		if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(*m) == common.RootDirPath {
			return m.MountPoint, m
		}

@Priya-Sharma25

Priya-Sharma25 commented Jul 8, 2026

Copy link
Copy Markdown
Author

Could you please take a look when you have a chance?
I've also updated the code based on the suggestions from gemini-code-assist.

@cheyang @TrafalgarZZZ @xliuqq

Thanks!

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch 2 times, most recently from cabf68f to 5d568f5 Compare July 8, 2026 05:30
@cheyang

cheyang commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

/copilot review

@cheyang

cheyang commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@gemini-code-assist /review

@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 updates GenAlluxioUFSRootPath to support identifying the root path in multi-mount scenarios instead of restricting it to single-mount configurations, and adds a corresponding unit test. The feedback suggests updating the function's documentation comment, which is now outdated as it still references the single-mount restriction.

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.

// e.g. alluxio fs mount /underFSStorage / && alluxio fs mount s3://mybucket /mybucket
//
// [unified namespace]: https://docs.alluxio.io/os/user/stable/en/core-services/Unified-Namespace.html
func (u UFSPathBuilder) GenAlluxioUFSRootPath(items []datav1alpha1.Mount) (string, *datav1alpha1.Mount) {

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 documentation comment for GenAlluxioUFSRootPath (lines 50-53) is now outdated. It still states that the root path is only used if there is only a single mount item defined in dataset.spec.mounts. Since this PR enables identifying the root path in multi-mount scenarios, please update the documentation comment to reflect this new behavior.

@cheyang

cheyang commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Clean bug fix. The logic change is correct: iterating all mounts to find the root-path candidate instead of gating on len(items) == 1 fixes the reported issue for multi-mount datasets. Pointer handling (&items[i]) is sound and the new test case covers the key regression scenario. Only nit: the GoDoc on line 52 still describes the old single-item restriction and should be updated to match the new behavior.

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.76%. Comparing base (afca615) to head (7c1d439).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #6103   +/-   ##
=======================================
  Coverage   64.76%   64.76%           
=======================================
  Files         485      485           
  Lines       33895    33895           
=======================================
  Hits        21952    21952           
  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.

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch from 5d568f5 to 120356d Compare July 8, 2026 07:18
@Priya-Sharma25

Copy link
Copy Markdown
Author

@cheyang , Thanks for the review and catching that! I've updated the GoDoc comment for GenAlluxioUFSRootPath to properly describe the new multi-mount behavior instead of the old single-item restriction.

@cheyang

cheyang commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

/copilot review

@cheyang

cheyang commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

@gemini-code-assist /review

@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 updates GenAlluxioUFSRootPath to iterate through all mount items to find the root path, rather than restricting the check to when only a single mount item exists. A corresponding unit test was also added. The review feedback highlights a potential issue where returning a pointer directly to a slice element (&items[i]) could lead to accidental mutations of the original slice; it is recommended to copy the element by value first and return a pointer to the copy.

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 thread pkg/utils/ufs_path_builder.go Outdated
Comment on lines 57 to 63
for i := range items {
m := &items[i]
// only iff m matches all of the two following conditions (1) m is not a fluid-native mount point; (2) m.Path is "/", it should be the root path in cache UFS.
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(m) == common.RootDirPath {
return m.MountPoint, &m
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(*m) == common.RootDirPath {
return m.MountPoint, m
}
}

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

Returning a pointer directly to the slice element (&items[i]) can lead to accidental mutations of the original slice elements (which typically belong to dataset.Spec.Mounts in Kubernetes controllers). This breaks the read-only assumption of the spec and can cause subtle bugs or unexpected reconciliations.

To prevent this, copy the slice element by value first, and then return a pointer to the copy (which will be safely allocated on the heap by Go's escape analysis). This also simplifies the code by avoiding the need to dereference *m when calling GenUFSPathInUnifiedNamespace.

Suggested change
for i := range items {
m := &items[i]
// only iff m matches all of the two following conditions (1) m is not a fluid-native mount point; (2) m.Path is "/", it should be the root path in cache UFS.
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(m) == common.RootDirPath {
return m.MountPoint, &m
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(*m) == common.RootDirPath {
return m.MountPoint, m
}
}
for i := range items {
m := items[i]
// only iff m matches all of the two following conditions (1) m is not a fluid-native mount point; (2) m.Path is "/", it should be the root path in cache UFS.
if !common.IsFluidNativeScheme(m.MountPoint) && u.GenUFSPathInUnifiedNamespace(m) == common.RootDirPath {
return m.MountPoint, &m
}
}

@Priya-Sharma25

Copy link
Copy Markdown
Author

@cheyang , I've addressed your GoDoc comment. The gemini-code-assist bot is suggesting a copy-based pointer return, but since the original code already used &items[0] with the same pattern and you confirmed the pointer handling is sound, I believe this is ready. Please let me know if any further changes are needed!

Copilot AI 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.

Pull request overview

Fixes AlluxioRuntime root mount selection for datasets with multiple mount entries by scanning all mounts for a unified-namespace root path (/) instead of only handling the single-mount case. This aligns Alluxio root UFS mounting with dataset specs that explicitly set path: "/" on one of several mounts.

Changes:

  • Update GenAlluxioUFSRootPath to iterate over all mount items and return the first eligible mount whose unified-namespace path is /.
  • Return a pointer to the actual slice element (&items[i]) when a matching mount is found.
  • Add a unit test covering multi-mount datasets where one mount explicitly targets root (path: "/").

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
pkg/utils/ufs_path_builder.go Adjusts Alluxio root UFS selection logic to support multi-mount datasets by scanning all mounts for /.
pkg/utils/ufs_path_builder_test.go Adds a test to ensure root path mounts are correctly identified in multi-mount scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/utils/ufs_path_builder.go Outdated
Comment on lines 50 to 51
// 1. If any `mount` item defined in `dataset.spec.mounts[*]` has its ufs path equal to "/", its `mountpoint` should be on the root path.
// e.g. alluxio fs mount s3://mybucket /

@cheyang cheyang left a comment

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.

Thanks for the contribution. I have a concern about the direction of this fix.

The original len(items) == 1 check was intentional: path: "/" (mounting a UFS endpoint directly to the Alluxio root) is only a valid configuration when the Dataset has a single mount. In a multi-mount Dataset, each mount is expected to live under its own sub-path (e.g. /{name}), and the Alluxio root is backed by the default local storage /underFSStorage.

Allowing path: "/" in a multi-mount Dataset introduces ambiguity — what happens to the other mounts that also need to be accessible under the unified namespace root? This is not a supported use case.

Instead of extending GenAlluxioUFSRootPath to iterate over all items, the fix should go in the opposite direction:

  1. Add validation (either in a webhook or at the controller/engine level) that rejects a Dataset if len(mounts) > 1 and any mount has path: "/". This is an invalid configuration and the user should receive a clear error message.
  2. Keep the existing single-mount root path logic as-is (or fix minor issues like the pointer-to-copy problem separately).

Could you share the use case that motivated this change? If there's a real scenario where multi-mount + root path is needed, I'd like to understand it better before we loosen this constraint.

@Priya-Sharma25

Priya-Sharma25 commented Jul 9, 2026

Copy link
Copy Markdown
Author

@cheyang Thank you for the detailed explanation. I want to share the context that motivated this change:

Issue #3379 was filed by @xliuqq with a concrete use case: a Dataset with two mounts where one explicitly sets path: "/" to mount a specific UFS endpoint at the Alluxio root, while the other mount lives under its own sub-path (/spark). The expected behavior described in the issue was that the path: "/" mount should be honored, and the issue author identified the len(items) == 1 check as the root cause.

I understand your concern about ambiguity. However, in the use case from #3379, there is no conflict — the flink mount takes / and spark takes /spark. They don't overlap.

That said, I agree that validation would be valuable as well. Would a combined approach work?

  1. Allow path: "/" in multi-mount datasets (as this PR does), since Alluxio's unified namespace supports it when paths don't conflict.
  2. Add validation to reject configurations where multiple mounts would conflict (e.g., two mounts both claiming /).
    I'm happy to go in whichever direction you agree on. Since this is a design decision , I'll wait for your guidance before making further changes.

@cheyang

cheyang commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

@Priya-Sharma25 Thanks for the context and for referencing #3379.

I understand the use case, but I'd like to stick with my original position here. The example in #3379 works only because the paths happen not to collide — spark defaults to /spark and flink takes /. However, this "no conflict" guarantee is fragile:

  1. Implicit overlap risk: If the UFS behind the root mount (flink) already contains a spark/ directory, then /spark becomes ambiguous — is it coming from the root-mounted UFS or from the independently mounted spark endpoint? Alluxio resolves this by mount ordering, which is non-obvious and hard to debug for users.
  2. Fluid cannot validate this at admission time: We have no way to inspect the remote UFS directory structure when the Dataset is created, so we cannot guarantee "no conflict" programmatically. The only safe approach is to disallow the combination entirely by rule.

My suggestion for the fix direction:

  • Do not extend GenAlluxioUFSRootPath to support path: "/" in multi-mount Datasets.
  • Instead, add validation in the controller reconcile path: if len(mounts) > 1 and any mount has path: "/", set the Dataset status to an error condition with a clear message explaining that root-path mounting is only supported for single-mount Datasets. This gives users immediate, actionable feedback.

I'll also revisit #3379 to update the resolution direction there.

cc @xliuqq

@Priya-Sharma25

Copy link
Copy Markdown
Author

@cheyang Thank you for the clear explanation — the implicit overlap risk and the inability to validate remote UFS structures at admission time are convincing points. I'll update this PR to take the validation approach instead:

Revert the GenAlluxioUFSRootPath changes back to the original logic
Add validation in the controller reconcile path that rejects len(mounts) > 1 && path: "/" with a clear error message on the Dataset status
I'll push the updated implementation shortly. Thanks for the guidance!

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch 2 times, most recently from 952a2cb to 4da0740 Compare July 10, 2026 02:54
@Priya-Sharma25

Copy link
Copy Markdown
Author

@cheyang Thank you for the guidance! I've updated the PR to follow your suggested approach. The GenAlluxioUFSRootPath logic has been reverted, and I've added a validation rule in the Dataset controller that sets the Dataset to a Failed phase with an error message when len(mounts) > 1 and path: "/" is used. Let me know if everything looks good!

@cheyang

cheyang commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

/copilot review

@cheyang

cheyang commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@gemini-code-assist /review

@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 validation to ensure that root-path mounting is only supported for single-mount Datasets, updating the dataset phase to FailedDatasetPhase and adding a corresponding unit test. The review feedback highlights two important issues: first, the validation check can be bypassed if mount.Path is empty (since it defaults to /{Name}), and second, returning a requeue error for a non-transient validation failure when the dataset is already in a failed phase can cause an infinite reconcile loop.

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 +137 to +138
for _, mount := range ctx.Dataset.Spec.Mounts {
if mount.Path == common.RootDirPath {

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.

high

The validation check mount.Path == common.RootDirPath can be bypassed if mount.Path is omitted (empty string) and mount.Name is also empty. According to the Mount struct definition in api/v1alpha1/dataset_types.go, if Path is not set, it defaults to /{Name}. If both Path and Name are empty, the effective mount path is / (which is common.RootDirPath), but the current check mount.Path == common.RootDirPath will evaluate to "" == "/", which is false.

To make this validation robust and prevent bypassing, we should resolve the effective path of the mount before comparing it to common.RootDirPath.

Suggested change
for _, mount := range ctx.Dataset.Spec.Mounts {
if mount.Path == common.RootDirPath {
for _, mount := range ctx.Dataset.Spec.Mounts {
path := mount.Path
if path == "" {
path = "/" + mount.Name
}
if path == common.RootDirPath {

Comment on lines +143 to +150
if ctx.Dataset.Status.Phase != datav1alpha1.FailedDatasetPhase {
dataset := ctx.Dataset.DeepCopy()
dataset.Status.Phase = datav1alpha1.FailedDatasetPhase
if err := r.Status().Update(ctx, dataset); err != nil {
ctx.Log.Error(err, "Failed to update the dataset phase to Failed", "StatusUpdateError", ctx)
}
}
return utils.RequeueIfError(err)

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

Returning utils.RequeueIfError(err) for a non-transient validation error when the dataset phase is already FailedDatasetPhase causes the controller to enter an infinite reconcile loop, continuously logging errors and consuming CPU resources.

To prevent this, we can check if the phase is already FailedDatasetPhase and return utils.NoRequeue() to stop reconciling. If the status update fails, we should return the update error to retry the status update.

				if ctx.Dataset.Status.Phase == datav1alpha1.FailedDatasetPhase {
					return utils.NoRequeue()
				}
				dataset := ctx.Dataset.DeepCopy()
				dataset.Status.Phase = datav1alpha1.FailedDatasetPhase
				if err := r.Status().Update(ctx, dataset); err != nil {
					ctx.Log.Error(err, "Failed to update the dataset phase to Failed", "StatusUpdateError", ctx)
					return utils.RequeueIfError(err)
				}
				return utils.RequeueIfError(err)

}

// 0.5 Validate multiple mounts with root path
if len(ctx.Dataset.Spec.Mounts) > 1 {

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 validation sits ahead of the deletion handling in step 1, so it also fires while a Dataset is being torn down. Consider a Dataset that was created valid (so it already picked up the finalizer at step 2), then edited into a multi-mount config where one mount uses /. From that point on, every reconcile returns here via RequeueIfError before ever reaching reconcileDatasetDeletion, which is the only place the finalizer is removed. Once someone deletes that object, the finalizer never gets cleared and it stays stuck in Terminating.

spec.mounts is mutable, so this is reachable in practice: create a valid Dataset, add a second mount at /, then delete it. The name check at step 0 doesn't have this problem because metadata.name is immutable, but the mount list isn't.

Easiest fix is to move this block below the deletion check (after step 1), or to skip it when utils.HasDeletionTimestamp(ctx.Dataset.ObjectMeta) is true, so deletion can always proceed. A minor secondary point: returning RequeueIfError on a spec that can only be fixed by a user edit means the controller keeps re-queuing with backoff and re-emitting the error event; a watch on the spec change will re-trigger reconcile anyway, so returning without an error after setting the Failed phase would be quieter.

Expect(result).To(Equal(ctrl.Result{}))
})

It("returns error and sets FailedDatasetPhase when dataset has multiple mounts and one has root path", func() {

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 new case covers create-time rejection and the Failed phase, which is good. It doesn't exercise deletion of an already-finalized multi-mount / Dataset. Once the ordering above is fixed, a case that sets a deletion timestamp on such a Dataset and asserts the finalizer is removed / deletion completes would lock the fix in and guard against the Terminating-stuck regression.

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch from 4da0740 to fd0c082 Compare July 10, 2026 08:08
@Priya-Sharma25

Priya-Sharma25 commented Jul 10, 2026

Copy link
Copy Markdown
Author

@cheyang Thank you both for the excellent feedback and catching those edge cases! I've pushed an update that addresses all the points raised:

1. Deletion Lock Fixed: I moved the validation block down to step 2.5 (after the deletion check and finalizer addition). This ensures that if a Dataset is edited into an invalid state, the validation won't block the controller from removing the finalizer and deleting the object.
2. Infinite Requeue Fixed: The controller now returns utils.NoRequeue() when encountering a non-transient validation failure, preventing the infinite loop.
3. Implicit Root Path Caught: I updated the validation logic to resolve the effective path (falling back to common.UFSMountPathFormat if mount.Path is empty), ensuring that implicit / paths are correctly caught.
4. Tests Added: I added the unit test that explicitly asserts the finalizer is removed when a DeletionTimestamp is set on an invalid multi-mount dataset. I also added a test to verify the implicit root path behavior.

Let me know if there's anything else you'd like me to adjust!

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch 2 times, most recently from cc620af to 7c1d439 Compare July 10, 2026 08:37
@cheyang

cheyang commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

/copilot review

@cheyang

cheyang commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

@gemini-code-assist /review

@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 validation to prevent root-path mounting when multiple mounts are defined in a Dataset, and refactors reference dataset runtime creation into a helper method. It also adds unit tests to verify these validation checks and ensure that invalid datasets can still be deleted. The review feedback highlights a critical issue where a dataset can get stuck in FailedDatasetPhase even after the user corrects the invalid multi-mount configuration. The reviewer suggests implementing a recovery mechanism using a specific dataset condition to reset the phase back to NotBoundDatasetPhase once validation passes, along with corresponding unit tests.

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 +286 to +317
func (r *DatasetReconciler) validateMultiMountRoot(ctx reconcileRequestContext) (bool, ctrl.Result, error) {
if len(ctx.Dataset.Spec.Mounts) <= 1 {
return false, ctrl.Result{}, nil
}

for _, mount := range ctx.Dataset.Spec.Mounts {
effectivePath := mount.Path
if effectivePath == "" {
effectivePath = fmt.Sprintf(common.UFSMountPathFormat, strings.TrimLeft(mount.Name, "/"))
}
if effectivePath == common.RootDirPath {
err := errors.New("root-path mounting is only supported for single-mount Datasets")
ctx.Log.Error(err, "Failed to validate dataset", "DatasetValidationError", ctx)
r.Recorder.Eventf(&ctx.Dataset, v1.EventTypeWarning, common.ErrorProcessDatasetReason, "Failed to validate dataset because err: %v", err)

if ctx.Dataset.Status.Phase == datav1alpha1.FailedDatasetPhase {
res, _ := utils.NoRequeue()
return true, res, nil
}
dataset := ctx.Dataset.DeepCopy()
dataset.Status.Phase = datav1alpha1.FailedDatasetPhase
if updateErr := r.Status().Update(ctx, dataset); updateErr != nil {
ctx.Log.Error(updateErr, "Failed to update the dataset phase to Failed", "StatusUpdateError", ctx)
res, err := utils.RequeueIfError(updateErr)
return true, res, err
}
res, _ := utils.NoRequeue()
return true, res, nil
}
}
return false, ctrl.Result{}, 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.

high

The current implementation of validateMultiMountRoot sets the dataset phase to FailedDatasetPhase when an invalid root path is detected in a multi-mount dataset. However, if the user subsequently updates the dataset spec to fix the invalid configuration (either by correcting the path or reducing the number of mounts to 1), the dataset will remain stuck in FailedDatasetPhase because there is no mechanism to reset the phase back to NotBoundDatasetPhase once the validation passes.

To resolve this, we can set a specific condition (e.g., with reason InvalidDatasetSpec) when the validation fails. If the validation subsequently passes, we can check for this condition, clear it, and reset the phase to NotBoundDatasetPhase so that reconciliation can proceed normally.

func (r *DatasetReconciler) validateMultiMountRoot(ctx reconcileRequestContext) (bool, ctrl.Result, error) {
	hasInvalidRoot := false
	var validationErr error

	if len(ctx.Dataset.Spec.Mounts) > 1 {
		for _, mount := range ctx.Dataset.Spec.Mounts {
			effectivePath := mount.Path
			if effectivePath == "" {
				effectivePath = fmt.Sprintf(common.UFSMountPathFormat, strings.TrimLeft(mount.Name, "/"))
			}
			if effectivePath == common.RootDirPath {
				hasInvalidRoot = true
				validationErr = errors.New("root-path mounting is only supported for single-mount Datasets")
				break
			}
		}
	}

	if hasInvalidRoot {
		ctx.Log.Error(validationErr, "Failed to validate dataset", "DatasetValidationError", ctx)
		r.Recorder.Eventf(&ctx.Dataset, v1.EventTypeWarning, common.ErrorProcessDatasetReason, "Failed to validate dataset because err: %v", validationErr)

		if ctx.Dataset.Status.Phase == datav1alpha1.FailedDatasetPhase {
			res, _ := utils.NoRequeue()
			return true, res, nil
		}
		dataset := ctx.Dataset.DeepCopy()
		dataset.Status.Phase = datav1alpha1.FailedDatasetPhase
		cond := utils.NewDatasetCondition(
			datav1alpha1.DatasetReady,
			"InvalidDatasetSpec",
			validationErr.Error(),
			v1.ConditionFalse,
		)
		dataset.Status.Conditions = utils.UpdateDatasetCondition(dataset.Status.Conditions, cond)
		if updateErr := r.Status().Update(ctx, dataset); updateErr != nil {
			ctx.Log.Error(updateErr, "Failed to update the dataset phase to Failed", "StatusUpdateError", ctx)
			res, err := utils.RequeueIfError(updateErr)
			return true, res, err
		}
		res, _ := utils.NoRequeue()
		return true, res, nil
	}

	// If validation passes, check if we need to recover from a previous InvalidDatasetSpec failure
	idx, cond := utils.GetDatasetCondition(ctx.Dataset.Status.Conditions, datav1alpha1.DatasetReady)
	if idx != -1 && cond != nil && cond.Reason == "InvalidDatasetSpec" {
		dataset := ctx.Dataset.DeepCopy()
		dataset.Status.Phase = datav1alpha1.NotBoundDatasetPhase

		// Remove the InvalidDatasetSpec condition
		var newConditions []datav1alpha1.DatasetCondition
		for _, c := range dataset.Status.Conditions {
			if c.Type == datav1alpha1.DatasetReady && c.Reason == "InvalidDatasetSpec" {
				continue
			}
			newConditions = append(newConditions, c)
		}
		dataset.Status.Conditions = newConditions

		if updateErr := r.Status().Update(ctx, dataset); updateErr != nil {
			ctx.Log.Error(updateErr, "Failed to reset dataset phase from Failed", "StatusUpdateError", ctx)
			res, err := utils.RequeueIfError(updateErr)
			return true, res, err
		}
		return true, ctrl.Result{Requeue: true}, nil
	}

	return false, ctrl.Result{}, nil
}

Comment on lines +337 to +366
It("allows deletion of a dataset that was edited into an invalid multi-mount config", func() {
now := metav1.Now()
ds := datav1alpha1.Dataset{
ObjectMeta: metav1.ObjectMeta{
Name: "del-invalid",
Namespace: "default",
Finalizers: []string{finalizer},
DeletionTimestamp: &now,
},
Spec: datav1alpha1.DatasetSpec{
Mounts: []datav1alpha1.Mount{
{Name: "m1", MountPoint: "local:///path1", Path: "/"},
{Name: "m2", MountPoint: "local:///path2", Path: "/path2"},
},
},
}
r := newTestReconciler(&ds)
ctx := makeReconcileCtx(r, ds)

result, err := r.reconcileDataset(ctx, false)
// Should proceed to deletion, not block on validation
Expect(err).NotTo(HaveOccurred())
Expect(result).To(Equal(ctrl.Result{}))

// Assert the finalizer is removed, demonstrating that the Terminating-stuck regression is fixed.
// Once the finalizer is removed on an object with a deletion timestamp, the API server (or mock client) deletes it.
stored := &datav1alpha1.Dataset{}
getErr := r.Get(ctx, types.NamespacedName{Namespace: "default", Name: "del-invalid"}, stored)
Expect(apierrors.IsNotFound(getErr)).To(BeTrue())
})

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

Add a unit test to verify that the dataset controller successfully recovers from FailedDatasetPhase back to NotBoundDatasetPhase once the invalid multi-mount configuration is corrected.

		It("allows deletion of a dataset that was edited into an invalid multi-mount config", func() {
			now := metav1.Now()
			ds := datav1alpha1.Dataset{
				ObjectMeta: metav1.ObjectMeta{
					Name:              "del-invalid",
					Namespace:         "default",
					Finalizers:        []string{finalizer},
					DeletionTimestamp: &now,
				},
				Spec: datav1alpha1.DatasetSpec{
					Mounts: []datav1alpha1.Mount{
						{Name: "m1", MountPoint: "local:///path1", Path: "/"},
						{Name: "m2", MountPoint: "local:///path2", Path: "/path2"},
					},
				},
			}
			r := newTestReconciler(&ds)
			ctx := makeReconcileCtx(r, ds)

			result, err := r.reconcileDataset(ctx, false)
			// Should proceed to deletion, not block on validation
			Expect(err).NotTo(HaveOccurred())
			Expect(result).To(Equal(ctrl.Result{}))

			// Assert the finalizer is removed, demonstrating that the Terminating-stuck regression is fixed.
			// Once the finalizer is removed on an object with a deletion timestamp, the API server (or mock client) deletes it.
			stored := &datav1alpha1.Dataset{}
			getErr := r.Get(ctx, types.NamespacedName{Namespace: "default", Name: "del-invalid"}, stored)
			Expect(apierrors.IsNotFound(getErr)).To(BeTrue())
		})

		It("recovers from FailedDatasetPhase when the invalid multi-mount config is fixed", func() {
			ds := datav1alpha1.Dataset{
				ObjectMeta: metav1.ObjectMeta{
					Name:       "recover-invalid",
					Namespace:  "default",
					Finalizers: []string{finalizer},
				},
				Spec: datav1alpha1.DatasetSpec{
					Mounts: []datav1alpha1.Mount{
						{Name: "m1", MountPoint: "local:///path1", Path: "/"},
						{Name: "m2", MountPoint: "local:///path2", Path: "/path2"},
					},
				},
				Status: datav1alpha1.DatasetStatus{
					Phase: datav1alpha1.FailedDatasetPhase,
					Conditions: []datav1alpha1.DatasetCondition{
						{
							Type:   datav1alpha1.DatasetReady,
							Status: corev1.ConditionFalse,
							Reason: "InvalidDatasetSpec",
						},
					},
				},
			}
			r := newTestReconciler(&ds)

			// Now update the spec to be valid
			ds.Spec.Mounts[0].Path = "/path1"
			ctx := makeReconcileCtx(r, ds)

			result, err := r.reconcileDataset(ctx, false)
			Expect(err).NotTo(HaveOccurred())
			Expect(result).To(Equal(ctrl.Result{Requeue: true}))

			stored := &datav1alpha1.Dataset{}
			Expect(r.Get(ctx, types.NamespacedName{Namespace: "default", Name: "recover-invalid"}, stored)).To(Succeed())
			Expect(stored.Status.Phase).To(Equal(datav1alpha1.NotBoundDatasetPhase))

			idx, _ := utils.GetDatasetCondition(stored.Status.Conditions, datav1alpha1.DatasetReady)
			Expect(idx).To(Equal(-1))
		})

@cheyang

cheyang commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Round 4 - both items from last round are addressed. Validation now lives at step 2.5, after the deletion check at step 1, so a finalized Dataset edited into a multi-mount / config can no longer get stuck in Terminating, and there's a dedicated delete-path test confirming the finalizer is still removed. The extra explicit/implicit root cases are good coverage, and the effective-path derivation matches ufs_path_builder.go. The one remaining blocker is recoverability - a Dataset that lands in Failed can't get back out after the spec is fixed. Left the detail inline.

return true, res, nil
}
dataset := ctx.Dataset.DeepCopy()
dataset.Status.Phase = datav1alpha1.FailedDatasetPhase

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.

Moving this to step 2.5 (after the deletion handling at step 1) does fix the Terminating-stuck case, and the new delete-path test proves the finalizer still gets removed - thanks for that.

One thing is still open though: this is a one-way door into FailedDatasetPhase. When validation fails you flip the phase to Failed here, but the only code that ever moves a Dataset out of Failed is step 4, and that transition is gated on phase == NoneDatasetPhase. So if a user corrects the spec (drops the extra mount, or gives the root mount a real path), validateMultiMountRoot now returns cleanly, but the phase stays Failed and the Dataset never reconciles on to NotBound - it's silently stuck.

Since the spec is the source of truth, I think we should let it recover: when validation passes and the phase is Failed, reset it back to NotBound. Tagging the failure with a specific reason (e.g. InvalidDatasetSpec) so the reset only clears failures this check owns, rather than stomping other Failed causes, would keep it safe. Could you also add a unit test for the invalid->fixed recovery path, in the same style as the delete test?

@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch from 7c1d439 to 28a1c36 Compare July 10, 2026 11:15
	Signed-off-by: Priya Sharma <priyasharma1001a@gmail.com>

Signed-off-by: Priya Sharma <priyasharma1001a@gmail.com>
@Priya-Sharma25 Priya-Sharma25 force-pushed the fix/issue-3379-multi-mount-root branch from 28a1c36 to ad1d7e9 Compare July 10, 2026 11:20
@sonarqubecloud

Copy link
Copy Markdown

@Priya-Sharma25

Copy link
Copy Markdown
Author

@cheyang Thank you for the review! I've implemented the recovery mechanism. Now, when validation fails, it sets an InvalidDatasetSpec condition. If the user corrects the spec, the controller detects this, removes the condition, and resets the phase to NotBoundDatasetPhase so it's no longer stuck. I also added the corresponding unit test for this recovery path.

Additionally, I refactored validateMultiMountRoot into smaller helper functions to successfully reduce its Cognitive Complexity and resolve the SonarCloud failure.

Let me know if everything looks good!

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.

[BUG][AlluxioRuntime] multiple mountPoint containing root path is not correct mounted in alluxio

3 participants