Skip to content

[API]: feat: add Topology Policy for StormService - #1846

Open
googs1025 wants to merge 20 commits into
vllm-project:mainfrom
googs1025:exclusive_topology
Open

[API]: feat: add Topology Policy for StormService#1846
googs1025 wants to merge 20 commits into
vllm-project:mainfrom
googs1025:exclusive_topology

Conversation

@googs1025

@googs1025 googs1025 commented Dec 11, 2025

Copy link
Copy Markdown
Collaborator

Pull Request Description

This PR adds topologyPolicy to RoleSetSpec, which is also available through StormService.spec.template.spec. The policy lets operators request pod co-location by injecting Kubernetes pod affinity into both directly managed Pods and generated PodSet templates.

This is the generalized form of the earlier exclusiveTopology proposal in #1842. Instead of only supporting a hard per-RoleSet topology constraint, the API now supports multiple co-location scopes and both soft and hard scheduling behavior.

The policy has three fields:

  • scope: co-location granularity. Supported values are StormService, RoleSet, and Role.
  • key: Kubernetes topology label key used for co-location, such as kubernetes.io/hostname or topology.kubernetes.io/zone.
  • mode: scheduling strength. Preferred adds soft affinity and is the default; Required adds hard required affinity.

Example:

spec:
  template:
    spec:
      topologyPolicy:
        scope: RoleSet
        mode: Preferred
        key: kubernetes.io/hostname

With scope: RoleSet, pods within the same RoleSet prefer to schedule on the same topology domain. This covers the Prefill/Decode same-host colocation use case from #1290 while avoiding scheduling failures by default when a single host or zone cannot satisfy the request. Users can set mode: Required when strict co-location is needed.

For roles using podGroupSize > 1, the same affinity is applied to the generated PodSet template, so PodSet-managed pods receive the same topology behavior as directly managed pods.

Topology policy updates do not mutate affinity on already-created Pods because Pod affinity is immutable after Pod creation. Existing Pods and PodSet templates will pick up the new affinity after replacement or recreation.

Related Issues

Resolves: #1842
Related: #1290

Testing

  • go test -count=1 ./pkg/controller/roleset -run 'Test(GetTopologyMatchLabels|InjectTopologyAffinity|CreatePodSetForRole_TopologyPolicyDefaultsToPreferred|EmitTopologyPolicyPendingReplacementEvent)'
  • go test -count=1 ./pkg/controller/podset ./api/orchestration/v1alpha1
  • go test -count=1 ./test/integration/controller -run '^$'
  • git diff --check

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @googs1025, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the aibrix orchestration system by introducing a TopologyPolicy feature to the RoleSetSpec. This new policy enables fine-grained control over pod placement, allowing users to specify how pods should be co-located across different topology domains (e.g., hostnames, zones) at various scopes (StormService, RoleSet, or individual Role). The changes include API definition, automatic code generation for deepcopy and apply configurations, updates to CRD schemas, and the necessary controller logic to dynamically inject pod affinity rules, thereby improving scheduling flexibility and adherence to infrastructure topology.

Highlights

  • New API Field: TopologyPolicy: Introduced a new TopologyPolicy field within the RoleSetSpec to allow users to define co-location constraints for pods based on Kubernetes topology keys.
  • Topology Scope Definition: Defined TopologyScope enum with values StormService, RoleSet, and Role to specify the granularity at which co-location policies apply.
  • Automated Code Generation Updates: Generated deepcopy methods and apply configuration structures for the new TopologyPolicy type, ensuring proper handling within the Kubernetes API ecosystem.
  • CRD and Helm Chart Updates: Updated the Custom Resource Definition (CRD) schemas for RoleSet and StormService, as well as their corresponding Helm chart CRDs, to incorporate the new topologyPolicy field.
  • Controller Logic for Pod Affinity: Implemented controller logic to inject Kubernetes PodAffinity rules into generated Pods and PodTemplates based on the specified TopologyPolicy, enforcing co-location requirements during scheduling.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@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 a TopologyPolicy to control pod co-location for StormServices. The changes include adding the new API types, updating CRDs, and implementing the logic to inject pod affinity rules based on the policy.

The implementation is mostly solid, but I've identified a few areas for improvement:

  • There is a typo in a new API constant that should be corrected.
  • The validation for the new TopologyPolicy could be strengthened to provide better feedback to users on misconfiguration.
  • There is some code duplication in the new utility functions that can be refactored for better maintainability.

Overall, this is a good addition. Please see my detailed comments for suggestions.

Comment thread api/orchestration/v1alpha1/roleset_types.go Outdated
Comment on lines +152 to +158
// Key is the Kubernetes topology label key to enforce co-location on.
// Common values include:
// - "kubernetes.io/hostname" (node-level)
// - "topology.kubernetes.io/zone" (zone-level)
// Required when Scope is set.
// +optional
Key string `json:"key,omitempty"`

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 comment for Key on line 156 states that it's "Required when Scope is set", but this is not enforced by any validation, and the field is marked as +optional. While the controller logic handles this by ignoring the policy if either Key or Scope is empty, it would be better for user experience to enforce this relationship via validation. Consider adding a validating webhook to enforce that Key is present if Scope is set. Without it, a user might specify a Scope without a Key and the policy would be silently ignored, which can be hard to debug.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

add TODO to solve in next pr

Comment thread pkg/controller/roleset/utils.go Outdated
@googs1025
googs1025 force-pushed the exclusive_topology branch 2 times, most recently from 2183d34 to 0681d7b Compare December 12, 2025 01:50
@googs1025

googs1025 commented Dec 12, 2025

Copy link
Copy Markdown
Collaborator Author

local test:

case 1:

root@iZ6we78ovpjfbxlipufqliZ:~# kubectl get node -owide --show-labels
NAME                     STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION     CONTAINER-RUNTIME    LABELS
cluster1-control-plane   Ready    control-plane   22h   v1.32.2   172.18.0.3    <none>        Debian GNU/Linux 12 (bookworm)   6.8.0-88-generic   containerd://2.0.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster1-control-plane,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers=
cluster1-worker          Ready    <none>          22h   v1.32.2   172.18.0.5    <none>        Debian GNU/Linux 12 (bookworm)   6.8.0-88-generic   containerd://2.0.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,gateway=true,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster1-worker,kubernetes.io/os=linux,topology.kubernetes.io/zone=zone-a
cluster1-worker2         Ready    <none>          22h   v1.32.2   172.18.0.4    <none>        Debian GNU/Linux 12 (bookworm)   6.8.0-88-generic   containerd://2.0.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster1-worker2,kubernetes.io/os=linux,topology.kubernetes.io/zone=zone-a
cluster1-worker3         Ready    <none>          22h   v1.32.2   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   6.8.0-88-generic   containerd://2.0.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster1-worker3,kubernetes.io/os=linux,topology.kubernetes.io/zone=zone-b
cluster1-worker4         Ready    <none>          22h   v1.32.2   172.18.0.6    <none>        Debian GNU/Linux 12 (bookworm)   6.8.0-88-generic   containerd://2.0.3   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=cluster1-worker4,kubernetes.io/os=linux,topology.kubernetes.io/zone=zone-b
root@iZ6we78ovpjfbxlipufqliZ:~# vi aa.yaml
root@iZ6we78ovpjfbxlipufqliZ:~# vi aa.yaml
root@iZ6we78ovpjfbxlipufqliZ:~# kubectl apply -f aa.yaml
stormservice.orchestration.aibrix.ai/pd-inference created
root@iZ6we78ovpjfbxlipufqliZ:~# kubectl get pods -owide
NAME                                            READY   STATUS    RESTARTS   AGE   IP          NODE               NOMINATED NODE   READINESS GATES
pd-inference-roleset-7zrrb-decode-6794f4-0      1/1     Running   0          8s    10.6.2.48   cluster1-worker    <none>           <none>
pd-inference-roleset-7zrrb-decode-6794f4-1      1/1     Running   0          8s    10.6.4.84   cluster1-worker2   <none>           <none>
pd-inference-roleset-7zrrb-decode-6794f4-2      1/1     Running   0          8s    10.6.4.85   cluster1-worker2   <none>           <none>
pd-inference-roleset-7zrrb-decode-6794f4-3      1/1     Running   0          8s    10.6.2.47   cluster1-worker    <none>           <none>
pd-inference-roleset-7zrrb-prefill-648798-0-0   1/1     Running   0          8s    10.6.3.52   cluster1-worker3   <none>           <none>
pd-inference-roleset-7zrrb-prefill-648798-0-1   1/1     Running   0          7s    10.6.1.29   cluster1-worker4   <none>           <none>
pd-inference-roleset-7zrrb-prefill-648798-1-0   1/1     Running   0          7s    10.6.3.53   cluster1-worker3   <none>           <none>
pd-inference-roleset-7zrrb-prefill-648798-1-1   1/1     Running   0          7s    10.6.1.30   cluster1-worker4   <none>           <none>
pd-inference-roleset-fjf5b-decode-6794f4-0      1/1     Running   0          8s    10.6.2.49   cluster1-worker    <none>    
...       
apiVersion: orchestration.aibrix.ai/v1alpha1
kind: StormService
metadata:
  name: pd-inference
spec:
  replicas: 3
  stateful: true
  selector:
    matchLabels:
      app: pd-inference
  template:
    metadata:
      labels:
        app: pd-inference
    spec:
      topologyPolicy:
        scope: Role
        key: topology.kubernetes.io/zone
      roles:
        - name: prefill
          replicas: 2
          podGroupSize: 2
          stateful: true
          template:
            metadata:
              labels:
                role: prefill
            spec:
              containers:
                - name: prefill
                  image: nginx:alpine
        - name: decode
          replicas: 4
          stateful: true
          template:
            metadata:
              labels:
                role: decode
            spec:
              containers:
                - name: decode
                  image: nginx:alpine

case 2:

root@iZ6we78ovpjfbxlipufqliZ:~# cat aa.yaml
apiVersion: orchestration.aibrix.ai/v1alpha1
kind: StormService
metadata:
  name: pd-inference
spec:
  replicas: 3
  stateful: true
  selector:
    matchLabels:
      app: pd-inference
  template:
    metadata:
      labels:
        app: pd-inference
    spec:
      topologyPolicy:
        scope: Role
        key: topology.kubernetes.io/zone
      roles:
        - name: prefill
          replicas: 2
          podGroupSize: 2
          stateful: true
          template:
            metadata:
              labels:
                role: prefill
            spec:
              containers:
                - name: prefill
                  image: nginx:alpine
        - name: decode
          replicas: 4
          stateful: true
          template:
            metadata:
              labels:
                role: decode
            spec:
              containers:
                - name: decode
                  image: nginx:alpine
root@iZ6we78ovpjfbxlipufqliZ:~#
root@iZ6we78ovpjfbxlipufqliZ:~# kubectl apply -f aa.yaml
stormservice.orchestration.aibrix.ai/pd-inference created
root@iZ6we78ovpjfbxlipufqliZ:~# kubectl get pods -owide
NAME                                            READY   STATUS    RESTARTS   AGE   IP          NODE               NOMINATED NODE   READINESS GATES
pd-inference-roleset-8qw9t-decode-6794f4-0      1/1     Running   0          8s    10.6.3.56   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-decode-6794f4-1      1/1     Running   0          8s    10.6.3.59   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-decode-6794f4-2      1/1     Running   0          8s    10.6.3.57   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-decode-6794f4-3      1/1     Running   0          8s    10.6.3.58   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-prefill-648798-0-0   1/1     Running   0          8s    10.6.3.63   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-prefill-648798-0-1   1/1     Running   0          8s    10.6.3.60   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-prefill-648798-1-0   1/1     Running   0          8s    10.6.3.61   cluster1-worker3   <none>           <none>
pd-inference-roleset-8qw9t-prefill-648798-1-1   1/1     Running   0          8s    10.6.3.62   cluster1-worker3   <none>           <none>
pd-inference-roleset-bgqhg-decode-6794f4-0      1/1     Running   0          9s    10.6.2.54   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-decode-6794f4-1      1/1     Running   0          9s    10.6.2.51   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-decode-6794f4-2      1/1     Running   0          9s    10.6.2.53   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-decode-6794f4-3      1/1     Running   0          9s    10.6.2.52   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-prefill-648798-0-0   1/1     Running   0          9s    10.6.2.55   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-prefill-648798-0-1   1/1     Running   0          9s    10.6.2.56   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-prefill-648798-1-0   1/1     Running   0          8s    10.6.2.57   cluster1-worker    <none>           <none>
pd-inference-roleset-bgqhg-prefill-648798-1-1   1/1     Running   0          8s    10.6.2.58   cluster1-worker    <none>           <none>
pd-inference-roleset-dfwfs-decode-6794f4-0      1/1     Running   0          8s    10.6.1.36   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-decode-6794f4-1      1/1     Running   0          8s    10.6.1.34   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-decode-6794f4-2      1/1     Running   0          8s    10.6.1.33   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-decode-6794f4-3      1/1     Running   0          8s    10.6.1.35   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-prefill-648798-0-0   1/1     Running   0          8s    10.6.1.37   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-prefill-648798-0-1   1/1     Running   0          8s    10.6.1.39   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-prefill-648798-1-0   1/1     Running   0          8s    10.6.1.38   cluster1-worker4   <none>           <none>
pd-inference-roleset-dfwfs-prefill-648798-1-1   1/1     Running   0          8s    10.6.1.40   cluster1-worker4   <none>           <none>

@googs1025
googs1025 marked this pull request as ready for review December 12, 2025 15:28
Signed-off-by: CYJiang <googs1025@gmail.com>
@googs1025

Copy link
Copy Markdown
Collaborator Author

@Jeffwan /PTAL

@googs1025
googs1025 force-pushed the exclusive_topology branch from 5548953 to 3f78ff0 Compare June 15, 2026 11:15
Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>

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

Adds a new topologyPolicy field to the StormService/RoleSet API surface and wires controller logic to inject pod-affinity terms so pods can be co-located on a chosen Kubernetes topology domain (e.g., hostname/zone), with unit and integration coverage.

Changes:

  • Introduce TopologyPolicy (scope/mode/key) in the orchestration API and regenerate deepcopy/applyconfiguration helpers.
  • Inject pod affinity into both direct Pods and PodSet templates when RoleSetSpec.TopologyPolicy is set.
  • Add unit tests and an integration test validating default (Preferred) topology affinity injection.

Reviewed changes

Copilot reviewed 7 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/integration/controller/roleset_test.go Integration test ensuring affinity injection for both Pod and PodSet roles.
pkg/controller/roleset/utils.go Implements topology-policy label selection and affinity injection logic.
pkg/controller/roleset/utils_test.go Unit tests for match-label selection and affinity injection behavior.
pkg/controller/roleset/podset_rollsyncer.go Injects topology affinity into PodSet templates during creation.
pkg/client/applyconfiguration/utils.go Registers applyconfiguration mapping for TopologyPolicy.
pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go Generated applyconfiguration type for TopologyPolicy.
pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go Adds TopologyPolicy to RoleSetSpec applyconfiguration.
dist/chart/crds/orchestration.aibrix.ai_stormservices.yaml Helm CRD update to expose topologyPolicy in StormService template spec.
dist/chart/crds/orchestration.aibrix.ai_rolesets.yaml Helm CRD update to expose topologyPolicy in RoleSet spec.
config/crd/orchestration/orchestration.aibrix.ai_stormservices.yaml Source CRD update for topologyPolicy in StormService template spec.
config/crd/orchestration/orchestration.aibrix.ai_rolesets.yaml Source CRD update for topologyPolicy in RoleSet spec.
api/orchestration/v1alpha1/zz_generated.deepcopy.go Generated deepcopy updates for TopologyPolicy and RoleSetSpec.
api/orchestration/v1alpha1/roleset_types.go Defines TopologyPolicy, enums, and adds it to RoleSetSpec.
Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file

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

Comment on lines +269 to +278
case orchestrationv1alpha1.TopologyPolicyRequired:
// avoid duplicate terms
for _, term := range spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution {
if term.TopologyKey == topologyKey &&
reflect.DeepEqual(term.LabelSelector.MatchLabels, matchLabels) {
return
}
}
spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution =
append(spec.Affinity.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution, affinityTerm)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5386063 by guarding nil LabelSelector before comparing existing required affinity terms. Added a regression test covering existing required terms with nil selectors.

Comment on lines +284 to +292
for _, term := range spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution {
if term.Weight == weightedTerm.Weight &&
term.PodAffinityTerm.TopologyKey == topologyKey &&
reflect.DeepEqual(term.PodAffinityTerm.LabelSelector.MatchLabels, matchLabels) {
return
}
}
spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution =
append(spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, weightedTerm)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5386063 by guarding nil PodAffinityTerm.LabelSelector before comparing existing preferred affinity terms. Added a regression test covering existing preferred terms with nil selectors.

Comment on lines +148 to +154
// TopologyPolicy specifies how Pods are co-located based on Kubernetes topology keys.
type TopologyPolicy struct {
// Scope defines the granularity of co-location.
// Valid values are:
// - "StormService": All Pods in the entire StormService share the same topology value.
// - "RoleSet": All Pods within each RoleSet share the same topology value (different RoleSets may be on different domains).
// - "Role": All Pods of the same role (across all RoleSets) share the same topology value.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Updated the PR description with the topologyPolicy API fields, Preferred/Required scheduling semantics, an example, related issues, and the verification commands.

Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>
@varungup90

Copy link
Copy Markdown
Collaborator

📐 API Design

  • No Pattern Validation on Key:
    Using +kubebuilder:validation:MinLength=1 prevents empty strings but fails to reject invalid Kubernetes label key formats.

Recommendation: Consider adding a +kubebuilder:validation:Pattern marker to enforce valid label structures.

  • Undocumented Update Behavior:
    When TopologyPolicy is updated on a live RoleSet, existing pods retain the old affinity because pods are immutable once created; only new or replaced pods will pick up the changes.

Recommendation: Document this behavior explicitly in the API comments to prevent user confusion.


⚙️ Controller Logic

  • Redundant Validation at Call Sites:
    Both podset_rollsyncer.go and utils.go check tp.Key != "" and tp.Scope != "" before calling the inject functions. However, the inject functions already handle these cases internally via hasTopologyKey() and getTopologyMatchLabels(). Additionally, CRD validation enforces both as required fields.

Recommendation: Remove the outer guards as they are dead code and centralize the logic in one canonical location.

  • Misleading Scope for hasTopologyKey:
    The function only checks tp.Key, but its name implies it validates the entire topology policy.

Recommendation: Rename the function to validateTopologyKey or consolidate scope validation into it.

  • Hardcoded Weight ($100$):
    The preferred affinity weight is fixed and not configurable. If users have existing pod affinity preferences at different weights (e.g., from Volcano scheduler configurations), this will interact silently.

Recommendation: At a minimum, document this chosen default value.


🧪 Tests

  • Missing Required Mode Coverage:
    The integration tests only cover Preferred mode. They are missing a test case that explicitly asserts RequiredDuringSchedulingIgnoredDuringExecution when Required mode is selected.
  • Incomplete Scope Coverage:
    Integration tests only cover the RoleSet scope. There is currently no end-to-end coverage for the StormService and Role scopes.
  • No Update Scenario Tests:
    There are no test cases evaluating lifecycle state changes.

Missing Cases: What happens when TopologyPolicy is patched from nil $\rightarrow$ set, or from Preferred $\rightarrow$ Required on a running RoleSet?

Here is a clean, structured formatting of your additional code review feedback:


⚙️ Refactoring & Code Cleanup

  • Duplicate Injection Functions:
    injectTopologyAffinityForPod and injectTopologyAffinityForPodTemplate are nearly identical. Both execute the exact same sequence (hasTopologyKey $\rightarrow$ getTopologyMatchLabels $\rightarrow$ injectTopologyAffinityToPodSpec) and differ only by their input types (*v1.Pod vs *v1.PodTemplateSpec). In both cases, they simply extract the underlying .Spec to pass it down.

Recommendation: Consolidate these into a single helper function that accepts a *v1.PodSpec directly. This eliminates duplication and simplifies the calling pattern:

func injectTopologyAffinity(spec *v1.PodSpec, roleSet ..., tp ...) { ... }

// Callers:
injectTopologyAffinity(&pod.Spec, ...)
injectTopologyAffinity(&template.Spec, ...)

🔍 Dead Code & Optimization

  • Unreachable Default Mode Guard:
    Inside injectTopologyAffinityToPodSpec (around line 411), the if mode == "" guard that defaults the policy to Preferred is unreachable in practice. Because +kubebuilder:default=Preferred is defined at the API level, the field is guaranteed to be set before the object ever reaches the controller logic.

Recommendation: It is completely fine to keep this as a defensive safety net, but consider adding a brief comment so future maintainers know it isn't actively exercised by normal API paths.

@googs1025
googs1025 force-pushed the exclusive_topology branch from 13b8b55 to adb5ee0 Compare June 17, 2026 06:40
# Conflicts:
#	pkg/controller/roleset/utils.go
@varungup90

Copy link
Copy Markdown
Collaborator

@googs1025 let me know once it is ready to review.

@googs1025

Copy link
Copy Markdown
Collaborator Author

@googs1025 let me know once it is ready to review.

yes, ready for review @varungup90 @Jeffwan

@varungup90

Copy link
Copy Markdown
Collaborator

Thanks for the work on this feature! The core logic looks solid, but I've found a few areas that need attention, particularly regarding CRD synchronization and observability.

Below is my feedback broken down into actionable issues and a few non-blocking observations.

🚨 Actionable Issues

1. CRD drift between config/crd and dist/chart/crds (Bug)
The config/crd YAMLs include proper validation for the key field:

key:
  maxLength: 317
  pattern: ^([a-z0-9]...
  type: string

However, the dist/chart/crds YAMLs are missing both the maxLength: 317 and the pattern: constraints. Clusters deployed via Helm will silently accept topology keys that the config-deployed CRDs would reject. These files need to be kept in sync.

2. Missing observability when a topology policy is silently skipped
When a policy update is applied to a running RoleSet, existing pods are unaffected (since pod affinity is immutable post-creation). While the struct comment properly documents this behavior, there is no Kubernetes Event emitted when the policy changes but existing pods cannot be updated. This creates a silent gap—an operator could change mode: Preferred to mode: Required and see no indication that existing pods still use the old (or no) affinity.

  • Suggestion: Please fire a warning Event on the RoleSet in this scenario so operators are aware the change hasn't taken effect on running pods.

3. Integration test couples policy change with scale-up
In the "applies topology policy updates only to newly created pods" test (roleset_test.go:1074), each policy change is paired with a Replicas bump to force new pod creation. This makes it difficult to separate "policy updated" from "scale-up triggered" as the underlying mechanism. If a future reconciler change alters how scaling is triggered, this test could silently test the wrong thing.

  • Suggestion: It would be cleaner to force pod recreation explicitly (e.g., delete the pod and let the controller recreate it) rather than relying on the scale-up side effect.

💡 Observations & Nits (Non-blocking)

  • validateTopologyKey is redundant with CRD enforcement: The validateTopologyKey function (utils.go:524) guards against an empty Key, but the CRD already enforces MinLength=1 and Required. This is perfectly fine as a "belt-and-suspenders" check for pre-existing or externally mutated objects, but it would be helpful to note that rationale in a brief code comment so future maintainers know why the check exists.
  • Label map comparison: Using reflect.DeepEqual on map[string]string in the dedup checks (utils.go:434, utils.go:449) works fine. However, a more idiomatic choice in the Kubernetes ecosystem is apiequality.Semantic.DeepEqual from k8s.io/apimachinery/pkg/api/equality. Just a minor nit!
  • Missing unit test coverage: TestCreatePodSetForRole_TopologyPolicyDefaultsToPreferred doesn't exercise the case where StormServiceNameLabelKey is absent. While the integration tests cover getTopologyMatchLabels indirectly, adding an explicit unit test for this path in createPodSetForRole would make the test suite more robust.

Signed-off-by: CYJiang <googs1025@gmail.com>
@googs1025

Copy link
Copy Markdown
Collaborator Author

@Jeffwan /PTAL

Preserve topology affinity injection and integration coverage while incorporating the RoleSet in-place update events, tests, and helpers added on main.

Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>
Resolve RoleSet topology policy conflicts with pod in-place update events and integration coverage.

Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>
Restore auto-merged RoleSet API, CRD, and controller updates from main while retaining topology policy changes and the two conflict resolutions.

Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>
@googs1025

Copy link
Copy Markdown
Collaborator Author

cc @Jeffwan /PTAL

Copilot AI review requested due to automatic review settings July 22, 2026 08:39

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

Copilot reviewed 10 out of 16 changed files in this pull request and generated 2 comments.

Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file

Comment on lines +80 to +87
r.EventRecorder.Eventf(
roleSet,
corev1.EventTypeWarning,
TopologyPolicyPendingPodReplacementEventType,
"TopologyPolicy changes do not update %s or %s; they will use the new affinity after replacement or recreation.",
formatTopologyPolicyEventCount(outdatedPods, "active Pod"),
formatTopologyPolicyEventCount(outdatedPodSets, "PodSet template"),
)

assert.NoError(t, reconciler.emitTopologyPolicyPendingReplacementEvent(ctx, roleSet))

event := <-recorder.Events
Signed-off-by: CYJiang <googs1025@gmail.com>
Copilot AI review requested due to automatic review settings July 22, 2026 14:11

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

Copilot reviewed 10 out of 16 changed files in this pull request and generated no new comments.

Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file

Copilot AI review requested due to automatic review settings July 22, 2026 15:15

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

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

Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file

Comment thread pkg/controller/roleset/topology_policy_event_test.go
Signed-off-by: CYJiang <86391540+googs1025@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 22, 2026 15:26
@googs1025
googs1025 force-pushed the exclusive_topology branch from a9d58c9 to 46e7a9d Compare July 22, 2026 15:26
@googs1025

googs1025 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

@Jeffwan Could you please take a look at this Topology Policy feature when you have time? It is important for controlling prefill/decode and RoleSet placement across topology domains, and I have added local validation and samples to make the usage clear.

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

Copilot reviewed 15 out of 21 changed files in this pull request and generated 2 comments.

Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file

Comment on lines +1179 to +1184
func waitForTopologyRolePods(namespace, roleSetName, roleName string, expected int) []corev1.Pod {
var pods []corev1.Pod
gomega.Eventually(func(g gomega.Gomega) {
podList := &corev1.PodList{}
g.Expect(k8sClient.List(ctx, podList,
client.InNamespace(namespace),
Comment on lines +38 to +49
var outdatedPods int
var outdatedPodSets int
for i := range roleSet.Spec.Roles {
role := &roleSet.Spec.Roles[i]
matchLabels, ok := getTopologyMatchLabels(roleSet, role.Name, roleSet.Spec.TopologyPolicy)
if !ok || !validateTopologyKey(roleSet, roleSet.Spec.TopologyPolicy) {
continue
}

pods, err := getRolePods(ctx, r.Client, roleSet.Namespace, roleSet.Name, role.Name)
if err != nil {
return err
Copilot AI review requested due to automatic review settings July 29, 2026 04:32

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

Copilot reviewed 15 out of 21 changed files in this pull request and generated no new comments.

Files not reviewed (4)
  • api/orchestration/v1alpha1/zz_generated.deepcopy.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/rolesetspec.go: Generated file
  • pkg/client/applyconfiguration/orchestration/v1alpha1/topologypolicy.go: Generated file
  • pkg/client/applyconfiguration/utils.go: Generated file
Comments suppressed due to low confidence (2)

pkg/controller/roleset/topology_policy_event.go:45

  • validateTopologyKey is called inside the per-role loop, so an empty TopologyPolicy.Key will log the same warning once per role on every reconcile. Since the key is RoleSet-wide (not per-role), validate once up-front and return early to avoid log spam and wasted work.
	var outdatedPods int
	var outdatedPodSets int
	for i := range roleSet.Spec.Roles {
		role := &roleSet.Spec.Roles[i]
		matchLabels, ok := getTopologyMatchLabels(roleSet, role.Name, roleSet.Spec.TopologyPolicy)

pkg/controller/roleset/roleset_controller.go:149

  • Calling emitTopologyPolicyPendingReplacementEvent from Reconcile adds per-role List calls for Pods and (optionally) PodSets on every reconcile when spec.topologyPolicy is set. syncPods already lists the same role Pods/PodSets during reconciliation (e.g., rolesyncer.go calls getRolePods), so this can noticeably increase API-server load as role/replica counts grow. Consider gating this to only run when TopologyPolicy changes (persist an observed hash/value on the RoleSet), or emit the event from within the existing syncers where pod/podset lists are already available.
	if err := r.emitTopologyPolicyPendingReplacementEvent(ctx, roleSet); err != nil {
		managedErrors = append(managedErrors, fmt.Errorf("emit topology policy event error %v", err))
	}

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.

[RFC]: Support Exclusive Topology in StormService for Pod Colocation

4 participants