Skip to content

fix(cache): preserve CacheRuntimeClass template resources when unset - #6165

Merged
cheyang merged 2 commits into
fluid-cloudnative:masterfrom
btxu-db:fix/cacheruntime-preserve-template-resources
Aug 30, 2026
Merged

cheyang merged 2 commits into
fluid-cloudnative:masterfrom
btxu-db:fix/cacheruntime-preserve-template-resources

Conversation

@btxu-db

@btxu-db btxu-db commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Ⅰ. Describe what this PR does

When a CacheRuntimeClass template declares container resources and the CacheRuntime does not
set spec.master.resources / spec.worker.resources, the template's values were silently
reset to {} on the first reconcile after creation. The AdvancedStatefulSet's generation
went from 1 to 2 and the pods rolled once, with no error and no event — a component the user
capped at 2Gi could then consume the whole node.

Why it happened. syncRuntimeSpec did guard against the zero value, but only when
deciding what to assign to a local variable; the zero value was passed to
SyncComponentSpec anyway:

var workerResources corev1.ResourceRequirements
if runtime.Spec.Worker.Resources.Requests != nil || runtime.Spec.Worker.Resources.Limits != nil {
    workerResources = runtime.Spec.Worker.Resources
}

updateResources treats an empty ResourceRequirements as a valid desired state meaning
"clear the resources". That is deliberate and covered by its own unit test
(sync_component_spec_test.go, "should update to nil resources (remove limits)"), so it
faithfully wrote the empty value through. The information that the user had specified
nothing was lost at the package boundary, because ComponentSpec.Resources is a value type
and cannot distinguish "unset" from "explicitly empty".

Approach. Make ComponentSpec.Resources a *corev1.ResourceRequirements, so nil
means "leave the workload's current resources untouched". This mirrors
ComponentSpec.Replicas, which is already a pointer documented as
(optional, nil means no change) and already nil-checked by SyncComponentSpec:

// 1. Update replicas if specified and changed
if newSpec.Replicas != nil {
    if s.updateReplicas(astsToUpdate, *newSpec.Replicas, logger) {

// 3. Update resources if specified
if newSpec.Resources != nil {
    if s.updateResources(astsToUpdate, *newSpec.Resources, logger) {

updateResources itself is unchanged — a non-nil value is still applied verbatim, so
explicitly clearing resources keeps working and its existing test keeps passing.
ComponentSpec is internal to pkg/ddc/cache/component; no CRD or API type changes.

Ⅱ. Does this pull request fix one issue?

fixes #6161

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

syncRuntimeSpec had no direct test coverage, which is how this shipped. Added a
Describe("syncRuntimeSpec") block in pkg/ddc/cache/engine/sync_test.go with three specs,
written at the behaviour level (what the AdvancedStatefulSet looks like after a sync) rather
than against updateResources, so they survive any later refactor of the sync path:

  • CacheRuntime specifies no resources -> master and worker both keep the template's values
  • CacheRuntime specifies master resources -> master is updated, worker is left alone
  • CacheRuntime specifies worker resources -> worker is updated, master is left alone

The last two matter as much as the first. Without them, simply deleting updateResources
would also make the suite pass; their cross-assertions additionally pin down that the two
components do not bleed into each other.

sync_component_spec_test.go is touched only to pass &corev1.ResourceRequirements{...}
where ComponentSpec literals are built; no assertion in that file changed.

Ⅳ. Describe how to verify it

gofmt -l pkg/ddc/cache/                        # no output
go build ./...
go vet ./pkg/ddc/cache/...
go test -gcflags=all=-l ./pkg/ddc/cache/...    # ok
go test ./pkg/ddc/cache/...                    # 228 passed, up from 225 on the base commit

Without -gcflags=all=-l the suite also reports 12 failures in ufs_test.go and one
gomonkey spec in sync_test.go; those need inlining disabled for the patches to take
effect, fail identically on the base commit, and are unrelated to this change.

The new specs were confirmed to be genuine regression tests: checking out only
pkg/ddc/cache/engine/sync_test.go from this branch into a worktree at the base commit —
tests present, fix absent — fails all three with Expected "0" to equal "2Gi". Reverting
the master guard and the worker guard individually each fails a spec too, so neither half of
the change is left uncovered.

On a cluster. kind v0.23.0 / Kubernetes v1.30.0, the manifests from #6161, only the
controller image differs between the two runs. Polling metadata.generation and
spec.template.spec.containers[0].resources on the worker AdvancedStatefulSet, with 2Gi
declared in the CacheRuntimeClass template and nothing in the CacheRuntime:

before

  1s  gen=1 res={"limits":{"memory":"2Gi"}}  dataset=NotBound
 33s  gen=2 res={}                           dataset=Bound
 61s  gen=2 res={}                           dataset=Failed

after

  1s  gen=1 res={"limits":{"memory":"2Gi"}}  dataset=NotBound
  6s  gen=1 res={"limits":{"memory":"2Gi"}}  dataset=Bound
        (unchanged through 90s)

Field semantics. A second run on the same cluster, this time against the master
component — whose code path is byte-identical in both builds — with limits.memory: 2Gi
declared in the CacheRuntimeClass template, walking spec.master.resources through its
three states:

step spec.master.resources resulting AdvancedStatefulSet generation
1 omitted {"limits":{"memory":"2Gi"}} — template preserved 1
2 {"limits":{"memory":"1Gi"}} {"limits":{"memory":"1Gi"}} — applied 2
3 omitted again {"limits":{"memory":"1Gi"}} — left untouched 2
4 {"requests":{}} {} — cleared 3

Step 3 is the trade-off this PR makes: once an override has been set and then removed, the
workload keeps the last value rather than falling back to the template, because an override
that was never set and one that was removed are byte-identical in the CacheRuntime spec.
Step 4 shows an override can still be cleared explicitly — requests: {} and limits: {}
survive CRD pruning (confirmed with kubectl apply --dry-run=server, which returns them
verbatim) and deserialize into non-nil empty maps, so they pass the guard and are applied.
Before this PR steps 1 and 3 were indistinguishable and both cleared the workload; the
distinction between "leave it alone" and "clear it" is what this change introduces.

Sampling the master pod UID after each rollout converged (observedGeneration caught up and
updatedReplicas ready) shows the UID changes across a resources change, so the reset this
PR removes was costing a real pod recreation on every freshly created CacheRuntime, not just
a metadata bump.

Ⅴ. Special notes for reviews

The Dataset reaching Failed in the "before" run is #6160: the spurious rollout is a
transient runtime outage, and Failed is a one-way trap. This branch does not contain that
fix, yet the Dataset stays Bound after this change — because the spurious rollout no
longer happens. The two issues are still independent: #6160 also triggers on legitimate
rollouts (an image or replica change), so it needs its own fix.

This is the narrow fix for the reported symptom. It does not address a second, distinct way
the same code path loses resources: when a worker uses a processMemory tiered-store level,
handleProcessMemory adds the level's quota on top of the container's memory limit, so the
stored value is baseline + quota while syncRuntimeSpec only knows the baseline and
overwrites the sum away. That reproduces with this PR applied — a CacheRuntime with
worker.resources.limits.memory: 4Gi and processMemory.quota: 8Gi shows
gen=1 mem=12Gi at 1s and gen=2 mem=4Gi at 6s — and cannot be fixed by nil-handling,
since the value the sync path would need does not exist in any single field. Filing that
separately; it likely wants syncRuntimeSpec to compute the desired pod template through
the existing transform chain and diff that, rather than assembling raw spec fields.

Motivation:
When a CacheRuntimeClass template declares container resources and the
CacheRuntime does not set spec.master.resources / spec.worker.resources,
the template values were silently reset to {} on the first reconcile after
creation. The AdvancedStatefulSet's generation bumped from 1 to 2 and the
pods rolled once, with no error or event. A component the user capped at
2Gi could then consume the whole node.

syncRuntimeSpec already guarded against the zero value, but only when
choosing what to assign to a local variable; the zero value was passed on
to SyncComponentSpec regardless. updateResources treats an empty
ResourceRequirements as a valid desired state meaning "clear the
resources" -- a deliberate contract covered by its own unit test -- so it
faithfully wrote the empty value through. The information that the user
had not specified anything was lost at the package boundary, because
ComponentSpec.Resources is a value type and therefore cannot distinguish
"unset" from "explicitly empty".

Approach:
Make ComponentSpec.Resources a *corev1.ResourceRequirements so that nil
means "leave the workload's current resources untouched", mirroring the
existing ComponentSpec.Replicas field, which is already a pointer
documented as "nil means no change". syncRuntimeSpec now yields nil when
the user specified neither requests nor limits, and SyncComponentSpec
skips updateResources on nil, exactly as it already does for Replicas.

updateResources itself is unchanged: a non-nil value is still applied
verbatim, so explicitly clearing resources keeps working and its existing
tests keep passing.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go build ./...
- go vet ./pkg/ddc/cache/...
- go test -gcflags=all=-l ./pkg/ddc/cache/... -> ok
- go test ./pkg/ddc/cache/... -> 228 passed, up from 225 on the base
  commit. Without the flag the suite also reports 12 failures in
  ufs_test.go and one gomonkey spec in sync_test.go; those need inlining
  disabled for the patches to take effect, fail identically on the base
  commit, and are unrelated to this change.
- Confirmed the new specs are genuine regression tests: checking out only
  sync_test.go from this branch into a worktree at the base commit --
  tests present, fix absent -- fails all three with
  Expected "0" to equal "2Gi". Reverting the master guard and the worker
  guard individually each fails a spec too, so neither half is uncovered.

Signed-off-by: btxu-db <btxu-db@outlook.com>
@fluid-e2e-bot

fluid-e2e-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

Hi @btxu-db. 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.

@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.20%. Comparing base (0e24a95) to head (4389ec7).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
pkg/ddc/cache/engine/sync.go 78.57% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master    #6165   +/-   ##
=======================================
  Coverage   65.19%   65.20%           
=======================================
  Files         486      486           
  Lines       34150    34151    +1     
=======================================
+ Hits        22263    22267    +4     
+ Misses      10136    10134    -2     
+ Partials     1751     1750    -1     

☔ 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.

@btxu-db btxu-db closed this Aug 17, 2026
@btxu-db btxu-db reopened this Aug 17, 2026
@cheyang
cheyang requested review from xliuqq and a balanced review from Copilot August 18, 2026 05:50

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 CacheRuntime reconciliation so unset resource overrides preserve CacheRuntimeClass template resources.

Changes:

  • Makes component resources optional during synchronization.
  • Skips resource updates when CacheRuntime resources are unset.
  • Adds master and worker regression tests.

Reviewed changes

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

Show a summary per file
File Description
pkg/ddc/cache/engine/sync.go Passes resource overrides only when configured.
pkg/ddc/cache/engine/sync_test.go Tests template preservation and component-specific overrides.
pkg/ddc/cache/component/component_manager.go Makes synchronized resources pointer-valued.
pkg/ddc/cache/component/advanced_statefulset_manager.go Skips nil resource updates.
pkg/ddc/cache/component/sync_component_spec_test.go Adapts tests to pointer-valued resources.
Suppressed comments (1)

pkg/ddc/cache/engine/sync.go:224

  • The same regression applies to worker resources: after a user removes previously configured worker requests/limits, the persisted value has nil maps and this passes nil, so the workload retains stale limits rather than clearing them. Please preserve a way to distinguish “no override was ever specified” from “remove the existing override”; the current value-typed CacheRuntime API cannot represent that distinction directly.
		var workerResources *corev1.ResourceRequirements
		if runtime.Spec.Worker.Resources.Requests != nil || runtime.Spec.Worker.Resources.Limits != nil {
			workerResources = &runtime.Spec.Worker.Resources

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/ddc/cache/engine/sync.go Outdated
Comment on lines +198 to +200
var resources *corev1.ResourceRequirements
if runtime.Spec.Master.Resources.Requests != nil || runtime.Spec.Master.Resources.Limits != nil {
resources = runtime.Spec.Master.Resources
resources = &runtime.Spec.Master.Resources

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks — the behaviour change is real. I've added the measurements to section 4.

"Never set" and "removed after being set" are byte-identical in the CacheRuntime spec, so syncRuntimeSpec can't tell them apart by construction. Before this PR both meant "clear to {}", which wiped the template's values — that's #6161. After it, both mean "leave it alone". Step 3 in the table is your case, and I'd rather leave a stale limit than silently drop a cap the admin declared.

The tri-state actually already exists after this change, no API change needed: omitting the field leaves the workload alone, resources: {requests: {}} clears it (I checked — empty maps survive CRD pruning and deserialize non-nil, so they pass the guard), and a set value gets applied. What's missing is falling back to the template on removal. That needs the desired pod template computed through the transform chain and diffed against the live one — the follow-up described in the PR body, which also covers the processMemory problem.

updateResources and its unit test are untouched here. Happy to add a test pinning the explicit-clear path if you want it as a contract.

if newSpec.Resources != nil {
if s.updateResources(astsToUpdate, *newSpec.Resources, logger) {
needsUpdate = true
}

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.

When calculating resource requirements, should we consider the configuration of runtimeclass?

We need to clarify in the document how the resource defined in runtimeclass and runtime takes effect, and how changes affect the final resource calculation.

My thought is: we will first take the not nil resource value defined in the runtime (high priority) or runtime class. If both nil, then the value is nil.

So,

  1. If runtime class sets the default resource, user can not remove resource. we will take the not nil resource value defined in the runtime class or runtime.
  2. If runtime class does not set the default resource, user can set the resource and then remove resource.

@cheyang What Do You Think?

Motivation:
Review feedback on fluid-cloudnative#6165 asked for the resources to be resolved by priority
rather than by skipping the sync: take the value from the CacheRuntime when it
sets one, otherwise take the value from the CacheRuntimeClass template, and only
leave the workload alone when neither declares anything.

The previous commit passed nil whenever the CacheRuntime set no resources, which
kept the template values in place but only because nothing was written. A
workload whose resources no longer matched the template stayed that way, since
the sync had no desired value to compare against.

Approach:
desiredComponentResources resolves the value for a component and both callers use
it. Only Containers[0] is read from the template, which is what the creation path
fills in. The returned value is a deep copy so updateResources cannot write
through into the CacheRuntime spec or the CacheRuntimeClass template, both of
which are shared objects.

Passing nil still means "leave the workload's resources untouched", matching
ComponentSpec.Replicas.

One consequence is worth stating: once a CacheRuntimeClass template declares
resources, removing resources from the CacheRuntime no longer leaves the
component unconstrained, it falls back to the template value. corev1.
ResourceRequirements is a value type, so an omitted field and an explicitly empty
one are indistinguishable after decoding, and the fallback has to pick one
meaning. Both sample docs now describe the resolution order and this limitation.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go build ./...
- go vet ./pkg/ddc/cache/...
- go test -gcflags=all=-l ./pkg/ddc/cache/... -> ok
- Confirmed the new specs are genuine regression tests: making
  desiredComponentResources return nil instead of the template value fails
  "should restore the template value when the CacheRuntime specifies none",
  while the five other specs in the Describe still pass. That last part also
  shows the specs already on this branch could not tell the two behaviours
  apart, since the seeded workload already carries the template value.

Signed-off-by: btxu-db <btxu-db@outlook.com>
btxu-db added a commit to btxu-db/fluid that referenced this pull request Aug 28, 2026
Motivation:
A CacheRuntime whose worker declares both spec.worker.resources and a
memory-backed tieredStore level has the tiered store quota added to the
container's memory request and limit when the workload is created, and
loses it again on the first reconcile afterwards. With a 4Gi baseline and
an 8Gi processMemory quota the worker's AdvancedStatefulSet is created
with a 12Gi limit and is rewritten to 4Gi a few seconds later, with no
error and no event. The state is then stable: the sync keeps proposing
4Gi, the workload already holds 4Gi, the comparison in updateResources
succeeds and nothing is ever reported again.

The container is left with three numbers that disagree, each defensible
on its own: the cgroup memory limit is 4Gi, /dev/shm is an 8Gi tmpfs
sized from the quota and never touched by the sync, and the cache tier is
configured to use 8Gi. Filling the cache gets the worker OOMKilled with
nothing in any manifest to explain why.

The creation path derives the container's memory in two steps:
transformComponentPodTemplate writes the user's baseline over the
template, then TransformRuntimeTieredStore adds the quota on top.
syncRuntimeSpec rebuilds the desired state from
runtime.Spec.Worker.Resources alone, reproducing only the first step, and
updateResources replaces the container's resources wholesale rather than
merging them, so the second step is dropped.

This is distinct from fluid-cloudnative#6161. There the CacheRuntime specified no
resources at all and the sync overwrote the template's values with the
zero value; fluid-cloudnative#6165 fixes that by passing nil. Here the user does specify a
baseline, so that guard is satisfied and the sync proceeds with an
under-computed value.

Approach:
Extract the arithmetic that charges a memory quota to a container into
withTieredStoreMemoryQuota, and the summing of memory-backed levels into
tieredStoreMemoryQuota. handleProcessMemory and handleEmptyDir already
held two byte-identical copies of that arithmetic; both now call the
helper, and syncRuntimeSpec calls it too. The derivation has a single
implementation, so the creation path and the sync path cannot compute
different values again.

withTieredStoreMemoryQuota returns a new value rather than mutating in
place. The previous inline code wrote through the ResourceList maps that
transform_common.go shares with runtime.Spec.Worker.Resources, so the
transform silently modified the runtime object it was handed; a caller
that reused that object within one reconcile would have accumulated the
quota more than once.

Master is unaffected: CacheRuntimeMasterSpec has no TieredStore field.
Client is unaffected: it runs as a DaemonSet and is deliberately not
synced. The nil guard from fluid-cloudnative#6165 is preserved, so a CacheRuntime that
specifies no resources still leaves the template's values untouched.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go vet ./pkg/ddc/cache/...
- FLUID_UNIT_TEST=true go test -gcflags="all=-N -l" ./pkg/ddc/cache/...
  -> ok, 241 specs
- The new spec compares the sync's output against the value the creation
  path derives, instead of asserting a hard-coded quantity, and guards
  that comparison against being vacuous. Copying only sync_test.go into a
  worktree at the base commit -- test present, fix absent -- fails with
  Expected "4Gi" to equal "12Gi", matching the reported symptom.
- kind v1.30.0, Kubernetes v1.30.0: with the base controller the worker
  workload goes gen=1 mem=12Gi -> gen=2 mem=4Gi. With this change an
  already-broken workload is repaired in place (gen=2 mem=4Gi -> gen=3
  mem=12Gi) and a freshly created one stays at gen=1 mem=12Gi. The three
  numbers then agree: cgroup limit 12Gi, /dev/shm 8Gi, cache tier 8Gi.

Signed-off-by: btxu-db <btxu-db@outlook.com>
@sonarqubecloud

Copy link
Copy Markdown

btxu-db added a commit to btxu-db/fluid that referenced this pull request Aug 28, 2026
Mooncake has no POSIX mount semantics, so its CacheRuntimeClass declares
only master and worker. No existing e2e case covers that shape: curvine
ships a client component, so the client-less path through the controller
is untested.

The case pins down the behaviour that path is expected to have:

- the controller does not panic and the Dataset reaches Bound with the
  client component omitted (a regression guard for the nil pointer
  dereference fixed in fluid-cloudnative#6157);
- no client DaemonSet or client pods are created, and
  status.client.phase stays empty;
- the ReportSummary script populates status.cacheStates, and cached
  reflects data written through the cache system's own client;
- the Dataset PVC reaches Bound but cannot be mounted by application
  pods, which is what the docs' FAQ describes.

The image is built in-repo from test/gha-e2e/mooncake/image rather than
pulled from an external registry, for the same reason as the jindo
oss-emulator: e2e runs on every PR, an external image going away turns
the whole pipeline red, and the two scripts Fluid invokes inside the
image have to be reviewable. The base image is pinned by digest and every
resolved pip version is pinned explicitly, installed from wheels only so
no package build script runs at image build time.

The CacheRuntimeClass template deliberately declares no container
resources. A template that sets them while the CacheRuntime does not
currently has them overwritten with an empty value on the first reconcile
(fluid-cloudnative#6161), and if the resulting rollout flips the Dataset to Failed it does
not recover on its own (fluid-cloudnative#6160). Neither is what this case is meant to
cover, so it stays clear of both until fluid-cloudnative#6165 lands.

Signed-off-by: btxu-db <btxu-db@outlook.com>

@xliuqq xliuqq 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.

lgtm. This new document explains the new resource calculation logic

@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.

/lgtm
/approve

@fluid-e2e-bot

fluid-e2e-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: cheyang, xliuqq

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

The pull request process is described 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

@cheyang
cheyang merged commit 64a10a1 into fluid-cloudnative:master Aug 30, 2026
22 of 23 checks passed
btxu-db added a commit to btxu-db/fluid that referenced this pull request Aug 30, 2026
Motivation:
A CacheRuntime whose worker declares both spec.worker.resources and a
memory-backed tieredStore level has the tiered store quota added to the
container's memory request and limit when the workload is created, and
loses it again on the first reconcile afterwards. With a 4Gi baseline and
an 8Gi processMemory quota the worker's AdvancedStatefulSet is created
with a 12Gi limit and is rewritten to 4Gi a few seconds later, with no
error and no event. The state is then stable: the sync keeps proposing
4Gi, the workload already holds 4Gi, the comparison in updateResources
succeeds and nothing is ever reported again.

The container is left with three numbers that disagree, each defensible
on its own: the cgroup memory limit is 4Gi, /dev/shm is an 8Gi tmpfs
sized from the quota and never touched by the sync, and the cache tier is
configured to use 8Gi. Filling the cache gets the worker OOMKilled with
nothing in any manifest to explain why.

The creation path derives the container's memory in two steps:
transformComponentPodTemplate writes the user's baseline over the
template, then TransformRuntimeTieredStore adds the quota on top.
syncRuntimeSpec rebuilds the desired state from
runtime.Spec.Worker.Resources alone, reproducing only the first step, and
updateResources replaces the container's resources wholesale rather than
merging them, so the second step is dropped.

This is distinct from fluid-cloudnative#6161. There the CacheRuntime specified no
resources at all and the sync overwrote the template's values with the
zero value; fluid-cloudnative#6165 fixes that by passing nil. Here the user does specify a
baseline, so that guard is satisfied and the sync proceeds with an
under-computed value.

Approach:
Extract the arithmetic that charges a memory quota to a container into
withTieredStoreMemoryQuota, and the summing of memory-backed levels into
tieredStoreMemoryQuota. handleProcessMemory and handleEmptyDir already
held two byte-identical copies of that arithmetic; both now call the
helper, and syncRuntimeSpec calls it too. The derivation has a single
implementation, so the creation path and the sync path cannot compute
different values again.

withTieredStoreMemoryQuota returns a new value rather than mutating in
place. The previous inline code wrote through the ResourceList maps that
transform_common.go shares with runtime.Spec.Worker.Resources, so the
transform silently modified the runtime object it was handed; a caller
that reused that object within one reconcile would have accumulated the
quota more than once.

Master is unaffected: CacheRuntimeMasterSpec has no TieredStore field.
Client is unaffected: it runs as a DaemonSet and is deliberately not
synced. The nil guard from fluid-cloudnative#6165 is preserved, so a CacheRuntime that
specifies no resources still leaves the template's values untouched.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go vet ./pkg/ddc/cache/...
- FLUID_UNIT_TEST=true go test -gcflags="all=-N -l" ./pkg/ddc/cache/...
  -> ok, 241 specs
- The new spec compares the sync's output against the value the creation
  path derives, instead of asserting a hard-coded quantity, and guards
  that comparison against being vacuous. Copying only sync_test.go into a
  worktree at the base commit -- test present, fix absent -- fails with
  Expected "4Gi" to equal "12Gi", matching the reported symptom.
- kind v1.30.0, Kubernetes v1.30.0: with the base controller the worker
  workload goes gen=1 mem=12Gi -> gen=2 mem=4Gi. With this change an
  already-broken workload is repaired in place (gen=2 mem=4Gi -> gen=3
  mem=12Gi) and a freshly created one stays at gen=1 mem=12Gi. The three
  numbers then agree: cgroup limit 12Gi, /dev/shm 8Gi, cache tier 8Gi.

Signed-off-by: btxu-db <btxu-db@outlook.com>
btxu-db added a commit to btxu-db/fluid that referenced this pull request Sep 1, 2026
…aseline

A CacheRuntime that names only some resource keys used to replace the
container's whole ResourceRequirements, dropping every requirement it did
not restate. Raising just limits.memory also cleared limits.cpu,
requests.cpu, requests.memory and claims, leaving the container with no CPU
request and no CPU limit at all, and letting Kubernetes default the memory
request up to the new limit.

Both the creation and the update path treated the CacheRuntime resources
and the CacheRuntimeClass template as alternatives. They are not: the
template carries the runtime's own requirements and the CacheRuntime
expresses the deltas an owner wants for their instance. Overlay them key by
key instead, through one helper both paths call, so the two cannot drift
apart and leave the workload rolling on every reconcile.

The corollary is that a key set by the template can now be overridden but no
longer removed by omitting it from the CacheRuntime. Removing a requirement
is a change to the template, which is where the runtime's requirements are
described. This is the same semantic question raised in the review of
fluid-cloudnative#6165, settled here in favour of the template staying the baseline.

Fixes fluid-cloudnative#6173
btxu-db added a commit to btxu-db/fluid that referenced this pull request Sep 1, 2026
…aseline

A CacheRuntime that names only some resource keys used to replace the
container's whole ResourceRequirements, dropping every requirement it did
not restate. Raising just limits.memory also cleared limits.cpu,
requests.cpu, requests.memory and claims, leaving the container with no CPU
request and no CPU limit at all, and letting Kubernetes default the memory
request up to the new limit.

Both the creation and the update path treated the CacheRuntime resources
and the CacheRuntimeClass template as alternatives. They are not: the
template carries the runtime's own requirements and the CacheRuntime
expresses the deltas an owner wants for their instance. Overlay them key by
key instead, through one helper both paths call, so the two cannot drift
apart and leave the workload rolling on every reconcile.

The corollary is that a key set by the template can now be overridden but no
longer removed by omitting it from the CacheRuntime. Removing a requirement
is a change to the template, which is where the runtime's requirements are
described. This is the same semantic question raised in the review of
fluid-cloudnative#6165, settled here in favour of the template staying the baseline.

Fixes fluid-cloudnative#6173

Signed-off-by: btxu-db <btxu-db@outlook.com>
cheyang pushed a commit that referenced this pull request Sep 2, 2026
* fix(cache): charge tiered store memory quota on every reconcile

Motivation:
A CacheRuntime whose worker declares both spec.worker.resources and a
memory-backed tieredStore level has the tiered store quota added to the
container's memory request and limit when the workload is created, and
loses it again on the first reconcile afterwards. With a 4Gi baseline and
an 8Gi processMemory quota the worker's AdvancedStatefulSet is created
with a 12Gi limit and is rewritten to 4Gi a few seconds later, with no
error and no event. The state is then stable: the sync keeps proposing
4Gi, the workload already holds 4Gi, the comparison in updateResources
succeeds and nothing is ever reported again.

The container is left with three numbers that disagree, each defensible
on its own: the cgroup memory limit is 4Gi, /dev/shm is an 8Gi tmpfs
sized from the quota and never touched by the sync, and the cache tier is
configured to use 8Gi. Filling the cache gets the worker OOMKilled with
nothing in any manifest to explain why.

The creation path derives the container's memory in two steps:
transformComponentPodTemplate writes the user's baseline over the
template, then TransformRuntimeTieredStore adds the quota on top.
syncRuntimeSpec rebuilds the desired state from
runtime.Spec.Worker.Resources alone, reproducing only the first step, and
updateResources replaces the container's resources wholesale rather than
merging them, so the second step is dropped.

This is distinct from #6161. There the CacheRuntime specified no
resources at all and the sync overwrote the template's values with the
zero value; #6165 fixes that by passing nil. Here the user does specify a
baseline, so that guard is satisfied and the sync proceeds with an
under-computed value.

Approach:
Extract the arithmetic that charges a memory quota to a container into
withTieredStoreMemoryQuota, and the summing of memory-backed levels into
tieredStoreMemoryQuota. handleProcessMemory and handleEmptyDir already
held two byte-identical copies of that arithmetic; both now call the
helper, and syncRuntimeSpec calls it too. The derivation has a single
implementation, so the creation path and the sync path cannot compute
different values again.

withTieredStoreMemoryQuota returns a new value rather than mutating in
place. The previous inline code wrote through the ResourceList maps that
transform_common.go shares with runtime.Spec.Worker.Resources, so the
transform silently modified the runtime object it was handed; a caller
that reused that object within one reconcile would have accumulated the
quota more than once.

Master is unaffected: CacheRuntimeMasterSpec has no TieredStore field.
Client is unaffected: it runs as a DaemonSet and is deliberately not
synced. The nil guard from #6165 is preserved, so a CacheRuntime that
specifies no resources still leaves the template's values untouched.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go vet ./pkg/ddc/cache/...
- FLUID_UNIT_TEST=true go test -gcflags="all=-N -l" ./pkg/ddc/cache/...
  -> ok, 241 specs
- The new spec compares the sync's output against the value the creation
  path derives, instead of asserting a hard-coded quantity, and guards
  that comparison against being vacuous. Copying only sync_test.go into a
  worktree at the base commit -- test present, fix absent -- fails with
  Expected "4Gi" to equal "12Gi", matching the reported symptom.
- kind v1.30.0, Kubernetes v1.30.0: with the base controller the worker
  workload goes gen=1 mem=12Gi -> gen=2 mem=4Gi. With this change an
  already-broken workload is repaired in place (gen=2 mem=4Gi -> gen=3
  mem=12Gi) and a freshly created one stays at gen=1 mem=12Gi. The three
  numbers then agree: cgroup limit 12Gi, /dev/shm 8Gi, cache tier 8Gi.

Signed-off-by: btxu-db <btxu-db@outlook.com>

* fix(cache): recover the tiered store quota from the workload

The previous revision recomputed the worker's tiered store memory quota from
spec.worker.tieredStore on every sync. tieredStore is not a supported update
field and SyncComponentSpec only patches resources, so editing the quota from
8Gi to 16Gi moved the container to baseline+16Gi while the tmpfs volume stayed
at the 8Gi it was created with -- an unsupported edit half-applying, and a fresh
disagreement between the container's memory and the volume it has to cover.

Sum the size limits of the workload's memory-backed tiered store volumes
instead. Those volumes are written by the same two handlers that charge
container memory, so the sum is exactly what the creation path charged, and the
container and its tmpfs cannot diverge. Editing tieredStore now continues to
have no effect, as documented in cacheruntime_spec_update.md.

Reading the quota from the volume rather than the container is also what lets a
workload that an earlier release already stripped be repaired in place: the
container's copy is gone, but the volume still carries it.

Route the generated volume names through a shared prefix constant, so the
recovery cannot select a tmpfs the CacheRuntimeClass template declares itself.

Tests: two specs in sync_test.go covering the edited-quota and stripped-workload
paths, and four in transform_tiered_store_test.go covering the recovery in
isolation, including the volumes it must skip.

Signed-off-by: btxu-db <btxu-db@outlook.com>

* fix(cache): compare container resources semantically

Motivation:
Charging the tiered store quota builds the desired memory value by adding to the
baseline. resource.Quantity caches the string it was parsed from, and Add clears
that cache, while the value decoded from the workload still carries it. The two
are numerically identical but not structurally identical, so the
reflect.DeepEqual in updateResources read every reconcile as a change.

The workload itself was never modified, because the resulting merge patch body is
empty, so no generation bump and no pod restart. What it did produce was a PATCH
request and a misleading "resources changed, will update" log line on every
reconcile. Observed on a kind cluster against a CacheRuntime that sets resources
and declares a processMemory tier: the line appeared every 90 seconds while the
AdvancedStatefulSet stayed at generation 1 and the worker pod at 0 restarts.

This only shows up once the quota is charged on every reconcile, so it arrived
with this branch rather than being pre-existing.

Approach:
Use equality.Semantic.DeepEqual, which apimachinery registers a Quantity-aware
comparison for. It treats the recomputed value as unchanged without loosening the
comparison: a genuinely different quantity is still reported and still applied.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go build ./...
- go vet ./pkg/ddc/cache/...
- go test -gcflags=all=-l -count=1 ./pkg/ddc/cache/... -> ok
- Confirmed the new spec is a genuine regression test: restoring
  reflect.DeepEqual fails "should not report a change when an equal quantity was
  produced by arithmetic" while the other 43 specs pass. The companion spec,
  "should still report a change when the quantity really differs", passes under
  both implementations and guards against fixing this by making the comparison
  too permissive.

Signed-off-by: btxu-db <btxu-db@outlook.com>

* test(cache): cover a tiered store quota charged on a template baseline

Resolving resources by priority means the baseline the tiered store quota is
charged on top of can now come from the CacheRuntimeClass template, not only from
the CacheRuntime. That combination had no coverage: every existing spec in this
area sets spec.worker.resources first.

The new spec leaves the CacheRuntime's resources unset, seeds the workload the way
creation leaves it (template value plus quota, with the tmpfs volume carrying the
quota as its size limit), and asserts the sync lands on the same value rather than
dropping back to the bare template value.

Confirmed it is a genuine regression test: restricting the quota to the case where
the CacheRuntime sets resources explicitly, which is what the code did before this
branch was rebased, fails this spec while the other 250 pass.

Validation:
- gofmt -l pkg/ddc/cache/ (no output)
- go build ./...
- go vet ./pkg/ddc/cache/...
- go test -gcflags=all=-l -count=1 ./pkg/ddc/cache/... -> ok

Signed-off-by: btxu-db <btxu-db@outlook.com>

* refactor(cache): read the worker pod spec through ComponentManager

syncRuntimeSpec recovered the worker's charged tiered store memory quota by
fetching the AdvancedStatefulSet itself, which pulled the concrete workload type
and its API package into the engine layer. The component package exists to hide
that choice: NewComponentHelper decides between AdvancedStatefulSet and DaemonSet
per component type, and nothing in ComponentManager names either. A second copy
of "the worker is an AdvancedStatefulSet" in the engine would not follow that
switch if it ever changed.

Add ComponentManager.GetPodSpec, returning a copy of the workload's pod template
spec, and implement it in both managers next to GetNodeAffinity, which already
has this exact shape. corev1.PodSpec is the common denominator of the two
workloads, so the seam carries no workload type across it.

chargedTieredStoreMemoryQuota stays in the engine: it recognises memory-backed
emptyDirs and the tiered store volume name prefix, which is tiered store domain
knowledge the component package should not hold. Its signature already took a
*corev1.PodSpec, so it plugs straight into GetPodSpec. The engine drops the
context, workloadv1alpha1 and resource imports.

Tests: three specs per manager in component_test.go covering the returned spec,
the copy contract that keeps callers from mutating the workload, and the
not-found error. The existing sync specs are unchanged, since the behaviour is
the same and only the ownership of the fetch moved.

Signed-off-by: btxu-db <btxu-db@outlook.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e654E3UJwDBv9fWSuqNbG

* refactor(cache): generate tiered store volume names through helpers

The three call sites in TransformRuntimeTieredStore each spelled out the volume
naming rule, so the rule for indexed levels lived in two identical format strings
and the process memory rule in a third. Routing them through the shared prefix
constant removed the duplicated literal but not the duplicated rule, and reading
a name off "%s%d-index-%d" is harder than reading the name itself.

Add getTieredStoreVolumeName and getMemoryTieredStoreVolumeName in util.go beside
getTieredStoreMountPath, which already organises the mount paths this way, so
both halves of a level's identity are now generated in one place. The helpers
keep building on tieredStoreVolumeNamePrefix, because chargedTieredStoreMemoryQuota
recovers the charged quota by matching that prefix and must stay tied to whatever
the generators produce.

Tests: the specs that assert what the transform wrote now call the helpers, and
util_test.go pins the literals once, including the prefix the recovery matches
and DNS-1035 validity. The chargedTieredStoreMemoryQuota specs keep their literal
volume names: they exercise the recognition side, and sharing a helper with the
generators there would hide a wrong prefix from both. The multi-path loop also
drops string(rune('0'+i)), which produces ':' once an index reaches ten.

Signed-off-by: btxu-db <btxu-db@outlook.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e654E3UJwDBv9fWSuqNbG

---------

Signed-off-by: btxu-db <btxu-db@outlook.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
btxu-db added a commit to btxu-db/fluid that referenced this pull request Sep 5, 2026
Mooncake has no POSIX mount semantics, so its CacheRuntimeClass declares
only master and worker. No existing e2e case covers that shape: curvine
ships a client component, so the client-less path through the controller
is untested.

The case pins down the behaviour that path is expected to have:

- the controller does not panic and the Dataset reaches Bound with the
  client component omitted (a regression guard for the nil pointer
  dereference fixed in fluid-cloudnative#6157);
- no client DaemonSet or client pods are created, and
  status.client.phase stays empty;
- the ReportSummary script populates status.cacheStates, and cached
  reflects data written through the cache system's own client;
- the Dataset PVC reaches Bound but cannot be mounted by application
  pods, which is what the docs' FAQ describes.

The image is built in-repo from test/gha-e2e/mooncake/image rather than
pulled from an external registry, for the same reason as the jindo
oss-emulator: e2e runs on every PR, an external image going away turns
the whole pipeline red, and the two scripts Fluid invokes inside the
image have to be reviewable. The base image is pinned by digest and every
resolved pip version is pinned explicitly, installed from wheels only so
no package build script runs at image build time.

The CacheRuntimeClass template deliberately declares no container
resources. A template that sets them while the CacheRuntime does not
currently has them overwritten with an empty value on the first reconcile
(fluid-cloudnative#6161), and if the resulting rollout flips the Dataset to Failed it does
not recover on its own (fluid-cloudnative#6160). Neither is what this case is meant to
cover, so it stays clear of both until fluid-cloudnative#6165 lands.

Signed-off-by: btxu-db <btxu-db@outlook.com>
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]container resources in CacheRuntimeClass are silently dropped

4 participants