Skip to content

feat(cache): bind a CacheRuntime to a snapshot of its CacheRuntimeClass - #6239

Closed
cheyang wants to merge 29 commits into
masterfrom
cheyang-cacheruntime-resources-merge
Closed

cheyang wants to merge 29 commits into
masterfrom
cheyang-cacheruntime-resources-merge

Conversation

@cheyang

@cheyang cheyang commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

Ⅰ. Describe what this PR does

Stacked on #6177. Please review only the four commits after 5b16f186. 5b16f186 merges current master into #6177's head (94a82543) and resolves its conflicts with #6186, #6223 and #6162. This PR is not meant to merge on its own. Once both are approved, its commits go onto #6177's branch so that #6177 and this change land in one squash, and this PR is closed.

Since #6165, #6177 and #6186, the controller reads the CacheRuntimeClass live on every reconcile. Editing a class's template resources or image reaches the master and worker of every CacheRuntime that uses the class, in every namespace, within one requeue (90s by default). The class is cluster-scoped and the controller does not watch it.

The AdvancedStatefulSet controller that Fluid bundles (run in-process by cmd/cache/app/cache.go) only updates container images in place:

  • defaultCalculateInPlaceUpdateSpec accepts nothing but replace on containers[x].image;
  • NativeVerticalUpdate exists in the vendored code but is not wired up.

So a single class edit restarts the containers (image) or recreates the Pods (resources) of all those runtimes, and they lose their memory and emptyDir cache.

options, executionEntries and dataOperationSpecs are also read live, through the runtime config ConfigMap, the mountUFS/reportSummary commands and DataLoad. A class is a versioned bundle of these, so following only part of it live would hand new scripts and options to components still running the old images.

This PR binds a CacheRuntime, at its first Setup, to a snapshot of the whole class:

  • Where the snapshot lives: an immutable ConfigMap fluid-runtime-class-snapshot-<runtime>, controlled by the CacheRuntime. It is labelled with the runtime and annotated with the class name.
  • Who reads it:
  • Class edited or deleted: this only affects CacheRuntimes created afterwards. Existing runtimes keep reconciling even if their class is deleted.
  • runtimeClassName changed: the runtime stays bound to its snapshot and Fluid emits a RuntimeClassChanged Warning.
  • Snapshot cannot be decoded: the snapshot is reported (RuntimeClassSnapshotInvalid) and never overwritten. Deleting it re-binds the runtime.
  • Snapshot controlled by another UID: this happens when an earlier runtime of the same name was not yet garbage-collected. Such a snapshot is replaced.
  • Runtimes created before this change have no snapshot and are bound on their next reconcile. Binding is designed to change no running component:
    • The snapshot is taken from the current class, except for the master and worker template resources and image. Those are adopted from the workloads, without reading the class: the worker's tiered-store memory quota is taken back out, and keys the runtime overrides are left out.
    • If syncing the runtime spec on top of that adopted baseline would still change a component, the component is listed in the snapshot's cacheruntime.fluid.io/adoption-pending annotation.
    • For a listed component, only replicas is synced, with a RuntimeAdoptionPending Warning. This lasts until the user removes the entry, or the workload already runs what the runtime declares.
    • The pending set is returned from the snapshot read or created in the same reconcile. It is not read back, because the informer cache would not see it yet.
    • If Create hits AlreadyExists because of cache lag, it waits briefly for the existing snapshot instead of taking another.
  • Data operations run in the dataset controller and only read the snapshot. If it does not exist yet, they fall back to the live class until the runtime controller binds the runtime.
  • Claims: updateResources now compares claims as a set keyed by name. A reordered claim list no longer rolls the workload.

Ⅱ. Does this pull request fix one issue?

fixes #6213. It lands through #6177; this PR will be closed rather than merged.

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

runtime_class_snapshot_test.go (new, 23 specs):

  • Binding:
    • the snapshot is immutable, controlled by the runtime and labelled;
    • the bound class keeps its value after the live class is edited (images, resources, options, executionEntries, dataOperationSpecs) or deleted;
    • binding fails when the class is missing;
    • a runtimeClassName change keeps the snapshot and warns.
  • Reading a snapshot:
    • a malformed snapshot is reported and left unchanged;
    • a snapshot left by an earlier runtime of the same name is replaced;
    • with a lagging cache, AlreadyExists waits for the existing snapshot.
  • Adoption:
    • defaults are adopted from the workloads, including the worker quota and the overridden key;
    • syncing on top of the adopted baseline does not patch the workloads;
    • a later runtime edit still applies on top of the adopted baseline;
    • a component whose workload differs is held: only replicas are synced, with a warning;
    • the held component is applied once its pending entry is removed;
    • the entry is released once the workload already matches;
    • components that do not exist yet are created from the class.
  • Cache lag in the binding reconcile: a full Sync behind a client that hides newly created ConfigMaps still holds the pending component (the case flagged in review).
  • Sync end to end: a class edit leaves the ASTS and the runtime config ConfigMap untouched.
  • adoptComponentBaseline: quota, master, quota not invertible, claims order, partial runtimeVersion, merge conflict.

sync_component_spec_test.go:

  • reordered claims are not reported as a change;
  • added, removed or replaced claims are.

curvine e2e (verify_runtime_class_edit_not_propagated) edits the class's worker template resources and options, forces a reconcile, and checks that:

  • the worker ASTS generation and resources are unchanged;
  • the runtime config is unchanged;
  • the class is restored afterwards.

Mutation checks (run locally, then reverted):

  • getBoundRuntimeClass reading the live class fails 14 of the 15 snapshot specs;
  • dropping the returned pending set fails the cache-lag spec;
  • failing on AlreadyExists fails the wait spec.

Ⅳ. Describe how to verify it

go build ./...                                                                # passed
go vet ./pkg/ddc/cache/... ./pkg/common/... ./pkg/controllers/...            # passed
FLUID_UNIT_TEST=true go test -gcflags="all=-N -l" ./pkg/ddc/cache/... ./pkg/common/...   # passed
GOTOOLCHAIN=go1.26.10 go run honnef.co/go/tools/cmd/staticcheck@2025.1.1 -checks 'all,-QF*,-ST*' ./pkg/ddc/cache/... ./pkg/common/...   # passed
bash -n test/gha-e2e/curvine/test.sh                                          # passed

The kind e2e was not run locally; CI runs it.

On a cluster:

  1. Create a CacheRuntime and wait until it is Ready.
  2. Run kubectl get cm fluid-runtime-class-snapshot-<name>.
  3. Edit the class (template resources or image, options) and annotate the runtime to force a reconcile.
  4. Check that the master and worker ASTS metadata.generation and the fluid-runtime-config-<name> ConfigMap do not change.

Ⅴ. Special notes for reviews

  • Why the whole class:
    • Pinning only resources and image leaves options, scripts and data operations live. A class upgrade would then mix versions inside a running runtime.
    • Following the whole class live rebuilds caches cluster-wide, as described above.
  • Why a ConfigMap:
    • It is namespaced next to the runtime, owned by it, immutable, and readable by both the runtime controller and the dataset controller. Both RBAC roles already cover ConfigMaps.
    • Annotations on the ASTS would only cover master and worker, and would be lost if the ASTS is recreated.
    • ControllerRevisions belong to the ASTS's history.
  • Adoption trade-offs:
  • Docs:
    • cacheruntime_spec_update.md (en/zh) now states that a resources change recreates the Pods and an image change restarts the containers.
    • The K8s >= 1.27 InPlacePodVerticalScaling and cgroupv1 caveats are dropped; they describe a resize Fluid does not perform.
    • A new section describes the snapshot.
    • The integration guide gets a short note for runtime authors.
  • Out of scope:
    • Wiring in-place vertical resize into the bundled ASTS, to be tracked in a follow-up issue.
    • An explicit opt-in to refresh a runtime's snapshot; for now, recreate the CacheRuntime.

btxu-db and others added 29 commits September 29, 2026 21:15
…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

Fixes #6173

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

The unit specs pin mergeResourceRequirements and desiredComponentResources
directly. These drive the whole sync against a fake client instead, so the
reported scenario is covered end to end: a worker whose CacheRuntimeClass
template declares a full set of requirements and whose CacheRuntime raises
only limits.memory keeps its CPU request, CPU limit and memory request.
Removing the merge fails it with the value the issue reports.

A second spec pins the nil handling. When neither side declares requests the
merged value must leave them nil rather than an empty map: Semantic.DeepEqual
separates the two, so an empty map would never compare equal to the workload
and the sync would patch on every reconcile. Dropping the empty-overlay guard
in mergeResourceList fails it.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The spec update guide described resources as the CacheRuntime value or, when
unset, the CacheRuntimeClass template value. They are a baseline and an
overlay now, so record that, and restate the limitation it carries: a key the
template declares can be overridden but no longer removed by omitting it.

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

mergeResourceList and mergeResourceClaims modified base in place and also
returned it, so a caller could not tell from the signature whether base was
left untouched, and had to use the returned value to pick up a list
allocated from nil or a slice grown by append.

Both now take a pointer and modify it in place without returning. The nil
case is handled inside: mergeResourceList allocates *base only when the
overlay declares something, keeping an untouched nil list nil, and
mergeResourceClaims relies on append growing a nil *base.
mergeResourceRequirements keeps its signature: it still deep-copies the
template baseline and returns the merged value, so its callers are
unchanged. Its comment is condensed to the overlay rule it implements.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The resources row pinned the replace-the-whole-struct behaviour as a known
bug of #6173. With the resources now overlaid on the CacheRuntimeClass
template key by key, it expects what that row said it should become:
requests {cpu 1, memory 2Gi} and limits {cpu 2, memory 8Gi} on both the
create and the update path.

Signed-off-by: btxu-db <btxu-db@outlook.com>
Drop the doubled blank line left where the rebase onto #6188 joined the
existing specs and the resources specs, which failed the gofmt check.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The comment on mergeResourceList, and the one on the "stop patching once
converged" spec, said an empty list would never compare equal to a
workload rendered with nil, so the sync would patch on every reconcile.
That is not so: updateResources compares with equality.Semantic, which
treats nil and empty maps as equal.

What the nil actually protects is the #6165 contract. When neither side
declares resources, desiredComponentResources returns nil and the sync
leaves the workload alone. Allocating empty lists there turns that into
a non-nil empty requirement, and the sync clears the resources of a
workload edited out of band. Dropping the guard fails the specs that pin
this, not the convergence spec. Say so in both comments.

Signed-off-by: btxu-db <btxu-db@outlook.com>
transformComponentPodTemplate and desiredComponentResources both resolve
a component's resources through mergeResourceRequirements, but each
path was only tested on its own. Nothing asserted that they agree, which
is what keeps the first reconcile after creation from patching the
workload it has just rendered.

Replay a table of template and CacheRuntime resources through both
paths. Each row checks that the two resolvers return the same value,
compared with equality.Semantic as updateResources does, and that a
workload rendered by the creation path is left untouched by
syncRuntimeSpec. The rows cover the nil/empty boundary on both sides
and claims.

Reverting either path alone to the wholesale replacement fails the
partial-overlay, added-key, empty-list and claims rows.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The curvine suite covered create, bind, IO and scaling, but none of the
in-place resources update line (#6165, #6167, #6173) ran against a real
cluster.

Give the worker template a full set of requirements, then patch only
limits.memory on the CacheRuntime. The AdvancedStatefulSet must take
the new value and keep the three keys the CacheRuntime did not name.
Wait for the update to roll out, in place or by recreation depending on
the cluster, and check that the worker pod carries the merged values
before the scaling case runs.

Signed-off-by: btxu-db <btxu-db@outlook.com>
Three consequences of the resources overlay that operators need to
know:

- Editing resources in a CacheRuntimeClass template now reaches every
  CacheRuntime using that class, on its next reconcile, for the keys it
  does not override. Removing all of the template's resources while the
  CacheRuntime sets none leaves the workload's values in place.
- After upgrading, a CacheRuntime that set only part of resources gets
  the template's other requirements back on the first reconcile, which
  updates the affected pods once.
- claims can only reference pod-level resourceClaims the template
  declares. Any other name now makes pod creation fail validation;
  earlier releases ignored a claims-only CacheRuntime.

List claims among the supported sub-fields, as the resolution order
already mentions them.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The specs for a CacheRuntime that names only part of the template's
resources drove the worker alone. The master resolves its resources
through the same desiredComponentResources, so add the same case for
it: a full template, limits.memory raised on the CacheRuntime, and the
other three keys expected to survive the sync.

Generalise the worker-only workload helper to take the workload name so
both components share it.

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

desiredComponentResources returned nil when neither side declared any
resources, which told the sync to leave the workload untouched. Since the
merge onto the template baseline already keeps the template's values, that
nil only served to block removals: a value set on the CacheRuntime over an
empty template could not be dropped by removing it, and removing the last
key from the template did not reach the workloads either.

Always return the merged result, so a key neither side declares is cleared.
This implements the rules agreed in the #6165 review: a template key can be
overridden but not removed, and a value the template does not declare can
be set and later removed. An empty list such as limits: {} names no key and
reads the same as leaving it out.

Signed-off-by: btxu-db <btxu-db@outlook.com>
Merging the CacheRuntime onto the CacheRuntimeClass template per key can
turn two valid inputs into an invalid pair, such as a template
requests.cpu: 1 with a CacheRuntime limits.cpu: 500m. The ASTS pod
template is not validated, so the controller patched it silently and the
API server only rejected the Pods it created.

Check the merged first container on both paths. On creation, transform
returns an error naming the conflicting keys, so setup fails. On sync,
the component's resources are left as they are and a
RuntimeResourcesConflict Warning event is emitted on the CacheRuntime,
while image and replicas still sync.

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

Kubernetes requires huge pages and extended resources to set a request
equal to their limit, and a limit whenever a request is set. A merged
pair such as a template pinning nvidia.com/gpu to 1 plus a CacheRuntime
raising only the limit to 2 passed validateResourceRequirements and was
only rejected once a Pod was created, while sync patched it onto the
workload. Validate those resources too, using a local mirror of
IsOvercommitAllowed since that helper package is not vendored.

Signed-off-by: btxu-db <btxu-db@outlook.com>
CacheRuntime has not shipped in a release yet, so there is no earlier
behavior to upgrade from. Remove the upgrade note and the remark about
earlier releases ignoring claims, and keep the one fact the note added:
a template edit updates the master and worker Pods, recreating them
where the cluster cannot resize them in place.

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

The sync path already names the conflicting keys in a
RuntimeResourcesConflict Warning event, but the creation path only
returned an error that surfaced under the generic setup failure reason.
Emit the same reason when the master, worker or client is rejected on
creation, and cover all three components, the client included.

Signed-off-by: btxu-db <btxu-db@outlook.com>
getRuntimeClass already rejects such a class, so no production caller
reaches the dereference; the guard keeps a direct caller from panicking
and, like the transform path, syncs no component.

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

The creation path adds the tiered store memory quota to the merged
resources and the sync path charges it back from the workload. Add a
Memory-medium emptyDir row and a process memory row, and have the
resolve check charge the quota the way syncRuntimeSpec does.

Signed-off-by: btxu-db <btxu-db@outlook.com>
Assert the Warning type alongside the reason, and that each rejected
creation emits the event once.

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

The rows compared the two paths against each other, so a quota that both
paths dropped would still agree. Pin the quota and the resulting memory
request and limit the creation path must render for each row.

Signed-off-by: btxu-db <btxu-db@outlook.com>
The sync patches container resources only. A CacheRuntimeClass edit that
adds a pod-level resourceClaims entry and the matching container claim
would carry the container claim into existing workloads without the
pod-level entry, and with DynamicResourceAllocation enabled the API server
would then reject every replacement Pod, stalling the rollout with nothing
on the CacheRuntime.

Leave out of the synced resources any claim the workload's pod spec does
not declare and its container does not already carry, emit a
RuntimeResourcesConflict Warning event naming it, and still sync the rest.
Skipping the whole resources instead would hold back a requests or limits
edit on clusters without DRA, where the API server ignores claims anyway;
keeping claims the container already carries avoids rolling a workload
whose Pods would not change. Document the limitation in both languages.

Signed-off-by: btxu-db <btxu-db@outlook.com>
Signed-off-by: btxu-db <btxu-db@outlook.com>
Resolve the conflicts with #6186, #6223 and #6162. All five are additive:

- constants.go: keep both RuntimeResourcesConflict and RuntimeVersionIncomplete.
- transform_common.go(+_test): keep the resource merge/validation helpers and the
  runtime version completion helpers side by side.
- sync.go: the master block reads the pod spec for validDesiredResources and
  completes the version against the template image, mirroring the worker block.
- curvine test.sh: keep update_worker_resources_and_verify ahead of the scale
  checks rewritten for the scale subresource.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: cheyang <cheyang@163.com>
updateResources compared the container resources with Semantic.DeepEqual, which
is order sensitive for claims. A reordered claim list carries no change for the
Pod, but rolled the workload, and for an AdvancedStatefulSet any pod template
change other than the image recreates the Pods. Compare claims by name instead.

Also correct the SyncComponentSpec comment: the bundled AdvancedStatefulSet
controller only updates container images in place; resources changes recreate
the Pods.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: cheyang <cheyang@163.com>
The CacheRuntimeClass was read live on every reconcile and every data operation.
Since #6165, #6177 and #6186, edits to a class template's resources and image
reached the master and worker of every CacheRuntime using the class, in every
namespace, within one requeue. The bundled AdvancedStatefulSet controller
recreates Pods for any resources change and restarts containers for an image
change, so a single class edit rebuilt the caches of all those runtimes. Class
options, execution entries and data operation specs were read live as well, so
a class upgrade would hand new scripts and options to components still running
the old images.

A CacheRuntime is now bound, at its first Setup, to a snapshot of its class,
stored in an immutable ConfigMap fluid-runtime-class-snapshot-<runtime> owned
by the runtime. Setup, Sync, the runtime config, UFS updates and DataLoad read
the class only from the snapshot. Editing or deleting a class therefore only
affects the CacheRuntimes created afterwards.

A runtime created before snapshots existed is bound on its next reconcile. Its
snapshot is taken from the current class, except for the master and worker
resources and image, which are adopted from what their workloads run (the
worker's tiered store quota taken back out, the keys the runtime overrides left
out), so binding changes no running component. A component whose workload would
still change is listed in the adoption-pending annotation of the snapshot: only
its replicas are synced, with a RuntimeAdoptionPending Warning event, until the
entry is removed or the workload already runs what the runtime declares.

The pending set is returned from the snapshot read or created in the reconcile
itself rather than read back, since the informer cache does not see a snapshot
created moments ago; a create racing the cache waits for the existing snapshot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: cheyang <cheyang@163.com>
…ects

Describe that a CacheRuntime is bound to its CacheRuntimeClass as it was at
setup, how runtimes created by an earlier release are bound and when their
adoption is held, and what a runtimeVersion or resources update does to the
running components: the bundled AdvancedStatefulSet controller restarts the
containers for an image change and recreates the Pods for a resources change.
Drop the K8s >= 1.27 InPlacePodVerticalScaling and cgroupv1 caveats, which
describe an in-place resize Fluid does not perform.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: cheyang <cheyang@163.com>
… runtime

Edit the curvine class's worker template resources and options after the
runtime is up, force a reconcile, and check that the worker AdvancedStatefulSet
generation and resources and the runtime config are unchanged. The class is
restored afterwards.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: cheyang <cheyang@163.com>
@sonarqubecloud

Copy link
Copy Markdown

@codecov

codecov Bot commented Nov 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.80122% with 70 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.94%. Comparing base (a01c038) to head (3d5593f).

Files with missing lines Patch % Lines
pkg/ddc/cache/engine/runtime_class_snapshot.go 82.05% 36 Missing and 18 partials ⚠️
pkg/ddc/cache/engine/sync.go 79.36% 9 Missing and 4 partials ⚠️
pkg/common/cacheruntime.go 0.00% 2 Missing ⚠️
pkg/ddc/cache/engine/cm.go 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6239      +/-   ##
==========================================
+ Coverage   65.75%   65.94%   +0.19%     
==========================================
  Files         486      488       +2     
  Lines       34459    34901     +442     
==========================================
+ Hits        22660    23017     +357     
- Misses      10044    10112      +68     
- Partials     1755     1772      +17     

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURES] CacheRuntime: pin the CacheRuntimeClass template at creation; resources = pinned baseline + per-key overrides

2 participants