Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7362d7e
Reapply "feat: support graceful scale-down for AlluxioRuntime using A…
jakharmonika364 Jun 25, 2026
1d75363
test(e2e): add Kind e2e coverage for AlluxioRuntime graceful scale-down
jakharmonika364 Jun 25, 2026
f4d0b01
test(e2e): address review feedback on alluxio-scaledown test.sh
jakharmonika364 Jun 25, 2026
c4b93aa
test(e2e): harden alluxio-scaledown test per review feedback
jakharmonika364 Jun 25, 2026
82bbc20
test(e2e): disable service account automount on minio bucket-create job
jakharmonika364 Jun 25, 2026
b8bc215
fix(e2e): read fixture from the correct mounted path
jakharmonika364 Jun 25, 2026
7a229bc
fix(e2e): wait for minio readiness and address review feedback on dec…
jakharmonika364 Jul 6, 2026
f7ffd4e
fix(e2e): add real minio readiness probe and capture job logs on failure
jakharmonika364 Jul 6, 2026
edcc338
fix(alluxio): use worker web port for decommission and avoid blocking…
jakharmonika364 Jul 6, 2026
106feb3
fix(e2e): require worker pod Running before decommission, harden job …
jakharmonika364 Jul 6, 2026
2478055
fix(alluxio): degrade gracefully on old Alluxio masters, verify decom…
jakharmonika364 Jul 11, 2026
97c440d
fix(e2e): pin minio images, retry job diagnostics with --previous
jakharmonika364 Jul 11, 2026
0b9c1b3
fix(alluxio): retry decommission after a failed attempt instead of sk…
jakharmonika364 Jul 13, 2026
65aeba8
fix(alluxio): track decommission target addresses, not just attempt s…
jakharmonika364 Jul 14, 2026
045deca
fix(alluxio): compare condition Status too, avoid redundant address l…
jakharmonika364 Jul 15, 2026
fb494d7
fix(alluxio): skip terminating pods, bracket IPv6 addresses, trim rep…
jakharmonika364 Jul 16, 2026
91dadbc
fix(alluxio): narrow the decommission failure-indicator keyword, guar…
jakharmonika364 Jul 17, 2026
f55bab5
fix(alluxio): skip graceful decommission when scaling down to zero
jakharmonika364 Jul 18, 2026
69cd638
fix(alluxio): clear stranded decommission condition when exiting the …
jakharmonika364 Jul 19, 2026
f201620
fix(alluxio): resolve decommission worker web port from the pod, not …
jakharmonika364 Jul 20, 2026
69ed16f
fix(alluxio): make read-after-scaledown prove data survived via cache…
jakharmonika364 Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/scripts/gha-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
bash test/gha-e2e/alluxio/test.sh
}

function alluxio_scaledown_e2e() {

Check warning on line 93 in .github/scripts/gha-e2e.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add an explicit return statement at the end of the function.

See more on https://sonarcloud.io/project/issues?id=fluid-cloudnative_fluid&issues=AZ79D8zckXMtfYKNdGnT&open=AZ79D8zckXMtfYKNdGnT&pullRequest=6061
set -e
bash test/gha-e2e/alluxio-scaledown/test.sh
}

function jindo_e2e() {
set -e
bash test/gha-e2e/jindo/test.sh
Expand All @@ -107,6 +112,7 @@

check_control_plane_status
alluxio_e2e
alluxio_scaledown_e2e
jindo_e2e
juicefs_e2e
curvine_e2e
9 changes: 9 additions & 0 deletions api/v1alpha1/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ const (
RuntimeFusesScaledIn RuntimeConditionType = "FusesScaledIn"
// RuntimeFusesScaledOut means the fuses of runtime just scaled out
RuntimeFusesScaledOut RuntimeConditionType = "FusesScaledOut"
// RuntimeWorkerDecommissioning means the runtime is draining workers ahead of a scale-down
RuntimeWorkerDecommissioning RuntimeConditionType = "WorkerDecommissioning"
)

const (
Expand All @@ -214,6 +216,13 @@ const (
RuntimeFusesScaledInReason = "Fuses scaled in"
// RuntimeFusesScaledInReason means the fuses of runtime just scaled out
RuntimeFusesScaledOutReason = "Fuses scaled out"
// RuntimeWorkerDecommissioningReason means workers are being decommissioned ahead of a scale-down
RuntimeWorkerDecommissioningReason = "Workers are being decommissioned"
// RuntimeWorkerDecommissionFailedReason means the last attempt to decommission workers ahead of
// a scale-down did not reach the Alluxio master (e.g. a transient network error, the master pod
// restarting) and will be retried on the next reconcile, as opposed to an attempt that reached
// the master and is still waiting for the workers to finish draining.
RuntimeWorkerDecommissionFailedReason = "DecommissionFailed"
)

// Condition describes the state of the cache at a certain point.
Expand Down
22 changes: 22 additions & 0 deletions pkg/ddc/alluxio/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ limitations under the License.

package alluxio

import "time"

const (
// NON_NATIVE_MOUNT_DATA_NAME also used in master 'statefulset.yaml' and config 'alluxio-mount.conf.yaml'
NON_NATIVE_MOUNT_DATA_NAME = "mount.info"
Expand Down Expand Up @@ -57,6 +59,26 @@ const (
defaultGracefulShutdownLimits int32 = 3
defaultCleanCacheGracePeriodSeconds int32 = 60

// defaultWorkerWebPort is the fallback Alluxio worker web port used only
// when a worker pod's own container spec doesn't expose one (see
// workerWebPortFromPod). decommissionWorker addresses this port, not the
// RPC port, since it monitors the worker's workload as exposed on the web
// server.
defaultWorkerWebPort = 30000

// alluxioWorkerContainerName is the name charts/alluxio gives the
// Alluxio worker container, see
// charts/alluxio/templates/worker/statefulset.yaml. Used to pick the
// worker's own "web" container port apart from the alluxio-job-worker
// container, which exposes a differently-numbered port of the same name.
alluxioWorkerContainerName = "alluxio-worker"

MountConfigStorage = "ALLUXIO_MOUNT_CONFIG_STORAGE"
ConfigmapStorageName = "configmap"

// defaultWorkerDecommissionDeadline bounds how long the engine keeps
// retrying a stuck worker drain (e.g. an unhealthy master, unreplicable
// blocks) before forcing the scale-down to proceed anyway, rather than
// stalling on every reconcile indefinitely.
defaultWorkerDecommissionDeadline = 10 * time.Minute
)
144 changes: 144 additions & 0 deletions pkg/ddc/alluxio/operations/decommission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
Copyright 2026 The Fluid Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package operations

import (
"fmt"
"strings"
)

// DecommissionWorkers signals the Alluxio master to decommission the given
// workers. Each address must be in "<host>:<webPort>" form.
// The call is idempotent: re-issuing it against an already-decommissioned
// worker is safe.
//
// Requires Alluxio >= 2.9, where "fsadmin decommissionWorker" was introduced;
// against older masters this subcommand does not exist and the command exits
// non-zero. The caller (drainScalingDownWorkers/SyncReplicas) does not treat
// that as fatal: it retries on every reconcile, bounded by
// defaultWorkerDecommissionDeadline, after which it degrades to an
// ungraceful scale-down rather than stalling forever - so an old master
// still converges, just without the graceful drain.
func (a AlluxioFileUtils) DecommissionWorkers(addresses []string) error {
if len(addresses) == 0 {
return nil
}
command := []string{
"alluxio", "fsadmin", "decommissionWorker",
Comment thread
jakharmonika364 marked this conversation as resolved.
"--addresses", strings.Join(addresses, ","),
// --wait defaults to 5m, which would block this exec call (and the
// engine's reconcile loop) waiting for the worker to idle. The
// engine already polls CountActiveWorkers across reconciles, so
// just initiate the decommission and return immediately.
"--wait", "0s",
}
Comment thread
jakharmonika364 marked this conversation as resolved.
stdout, stderr, err := a.exec(command, false)
if err != nil {
a.log.Error(err, "AlluxioFileUtils.DecommissionWorkers() failed", "addresses", addresses, "stdout", stdout, "stderr", stderr)
return err
}
// decommissionWorker can report a per-worker problem (an address it
// couldn't resolve, a worker it couldn't reach) in its stdout while still
// exiting 0 for the batch as a whole. stderr is deliberately not scanned
// here: Alluxio's CLI is a JVM process and can print benign warnings to
// stderr on a successful run (as every other command wrapper in this
// package already assumes by only inspecting stderr once err != nil), so
// treating any stderr output as failure would false-positive constantly.
// The actual "did it work" signal is re-verified independently on every
// subsequent reconcile via CountActiveWorkers, and a false positive here
// only costs one extra bounded retry - so this stdout scan is a
// best-effort signal, not the authoritative check.
if reason, failed := decommissionOutputIndicatesFailure(stdout); failed {
err = fmt.Errorf("decommissionWorker for addresses %v reported a possible failure (%s): stdout=%q stderr=%q",
addresses, reason, stdout, stderr)
a.log.Error(err, "AlluxioFileUtils.DecommissionWorkers() output looked unsuccessful", "addresses", addresses)
return err
}
return nil
}

// decommissionOutputIndicatesFailure does a best-effort scan of
// decommissionWorker's stdout for signs that it didn't fully succeed despite
// exiting 0.
func decommissionOutputIndicatesFailure(stdout string) (reason string, failed bool) {
lower := strings.ToLower(stdout)
for _, indicator := range []string{"failed to", "unrecognized"} {
if strings.Contains(lower, indicator) {
return fmt.Sprintf("stdout contains %q", indicator), true
}
}
return "", false
}
Comment thread
jakharmonika364 marked this conversation as resolved.

// CountActiveWorkers returns the number of live workers according to
// "alluxio fsadmin report capacity -live". The "-live" flag is what makes
// this safe to compare against immediately after a decommission: it asks the
// master for currently live workers rather than every worker it still has a
// record of, so a worker that was just decommissioned doesn't linger in the
// count until its heartbeat times out.
func (a AlluxioFileUtils) CountActiveWorkers() (int, error) {
report, _, err := a.exec([]string{"alluxio", "fsadmin", "report", "capacity", "-live"}, false)
if err != nil {
a.log.Error(err, "AlluxioFileUtils.CountActiveWorkers() failed")
return 0, err
}
count, err := parseActiveWorkerCount(report)
if err != nil {
a.log.Error(err, "AlluxioFileUtils.CountActiveWorkers() failed to parse capacity report", "report", report)
return 0, err
}
return count, nil
}

// parseActiveWorkerCount counts workers in the capacity report produced by
// "alluxio fsadmin report capacity". Worker entries begin at the non-indented
// line after the "Worker Name" header; the indented line that follows each
// entry contains the used-capacity detail.
//
// Worker Name Last Heartbeat Storage MEM
// 192.168.1.147 0 capacity 2048.00MB <- worker entry
// used 443.89MB (21%) <- detail, indented
// 192.168.1.146 0 capacity 2048.00MB <- worker entry
// used 0B (0%)
//
// It returns an error rather than silently reporting 0 workers when the
// "Worker Name" header is never found: a report this caller can't recognize
// (a format change, an unexpected error message in place of the report, ...)
// must not be misread as "every worker has drained", since that's exactly
// the signal the engine acts on to let a scale-down proceed.
func parseActiveWorkerCount(report string) (int, error) {
inWorkerSection := false
count := 0
for _, line := range strings.Split(report, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "Worker Name") {
inWorkerSection = true
continue
}
if !inWorkerSection || strings.TrimSpace(line) == "" {
continue
}
// Non-indented lines are new worker entries; indented lines are
// the used-capacity continuation for the previous entry.
if line[0] != ' ' && line[0] != '\t' {
count++
}
}
if !inWorkerSection {
return 0, fmt.Errorf("unrecognized capacity report format: missing %q header", "Worker Name")
}
return count, nil
}
Loading
Loading