Skip to content

Commit 4f00409

Browse files
slin1237fanyang-real
authored andcommitted
[Core] MC: multi-cluster controller config
Load MultiClusterConfig (workload-cluster transport tunables, placement timing/GC, and endpoint routing) from the inferenceservice-config ConfigMap, with per-key duration accessors that degrade gracefully when a key is omitted so each consumer applies its own in-package default. (cherry picked from commit 457580b)
1 parent d889c1f commit 4f00409

3 files changed

Lines changed: 409 additions & 0 deletions

File tree

pkg/controller/v1beta1/controllerconfig/configmap.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ func NewBenchmarkJobConfig(clientset kubernetes.Interface) (*BenchmarkJobConfig,
355355
return benchmarkJobConfig, nil
356356
}
357357

358+
// getInferenceServiceConfigMap fetches the inferenceservice-config ConfigMap
359+
// from the OME namespace.
360+
func getInferenceServiceConfigMap(clientset kubernetes.Interface) (*v1.ConfigMap, error) {
361+
return clientset.CoreV1().ConfigMaps(constants.OMENamespace).Get(context.TODO(), constants.InferenceServiceConfigMapName, metav1.GetOptions{})
362+
}
363+
358364
// NewOmeAgentConfig loads the omeAgent block from the inferenceservice-config
359365
// ConfigMap in the OME namespace. A missing block yields a zero-value config
360366
// (not an error) so the PVC path can surface PVCConfigMissing via status
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package controllerconfig
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
v1 "k8s.io/api/core/v1"
8+
"k8s.io/client-go/kubernetes"
9+
)
10+
11+
// MultiClusterConfigName is the inferenceservice-config ConfigMap key holding
12+
// the multi-cluster tuning block.
13+
const MultiClusterConfigName = "multicluster"
14+
15+
// +kubebuilder:object:generate=false
16+
// MultiClusterConfig holds operator-level tuning for the multi-cluster fan-out
17+
// layer (the WorkloadCluster connection/transport, the placement controller,
18+
// and the global endpoint publisher). It is loaded once at manager startup, from
19+
// the inferenceservice-config ConfigMap, only when multi-cluster is enabled.
20+
//
21+
// Topology, role, identity, and security stay manager flags, not config: they
22+
// decide which controllers run and the manager's identity, so they are
23+
// deploy-time decisions (a restart), not hot-tunable config.
24+
//
25+
// Every field degrades gracefully when omitted. Durations are stored as strings
26+
// and parsed by the *Duration() accessors, which yield 0 on an empty or
27+
// unparsable value; a zero handed to the workloadcluster/placement options
28+
// makes those packages apply their OWN in-package default. So the default for
29+
// each knob stays single-sourced in the package that owns it — never duplicated
30+
// as a literal here — and an absent "multicluster" block reproduces the
31+
// built-in behavior exactly.
32+
type MultiClusterConfig struct {
33+
WorkloadCluster WorkloadClusterConfig `json:"workloadCluster,omitempty"`
34+
Placement PlacementConfig `json:"placement,omitempty"`
35+
Endpoint EndpointConfig `json:"endpoint,omitempty"`
36+
}
37+
38+
// +kubebuilder:object:generate=false
39+
// WorkloadClusterConfig tunes the remote-cluster connection and transport layer
40+
// and the cross-cluster status watch funnel.
41+
type WorkloadClusterConfig struct {
42+
// ClientQPS / ClientBurst are the steady-state request rate and burst to each
43+
// remote workload-cluster apiserver. Zero leaves the client-go default.
44+
ClientQPS float64 `json:"clientQPS,omitempty"`
45+
ClientBurst int `json:"clientBurst,omitempty"`
46+
// PerCallTimeout bounds one remote request. Empty leaves no client-level timeout.
47+
PerCallTimeout string `json:"perCallTimeout,omitempty"`
48+
// CacheEnabled serves cross-cluster derived-InferenceService reads from a
49+
// per-cluster informer cache instead of live apiserver reads, and is the
50+
// prerequisite event source for the status watch funnel. False (the default)
51+
// keeps reads live and the funnel off.
52+
CacheEnabled bool `json:"cacheEnabled,omitempty"`
53+
// HealthInterval is the re-probe cadence for an otherwise-idle WorkloadCluster.
54+
HealthInterval string `json:"healthInterval,omitempty"`
55+
// ConnectionGrace is how long a previously reachable cluster tolerates transient
56+
// probe failures before it is flipped to Ready=False and disconnected.
57+
ConnectionGrace string `json:"connectionGrace,omitempty"`
58+
// EventsBatchPeriod debounces a rotated-kubeconfig Secret's burst of key updates
59+
// into one reconcile.
60+
EventsBatchPeriod string `json:"eventsBatchPeriod,omitempty"`
61+
// EstablishInitial / EstablishMax bound a single remote watch-establish attempt
62+
// (the timeout grows from initial to max). ReconnectRetryMax caps the
63+
// inter-attempt backoff for an unreachable cluster.
64+
EstablishInitial string `json:"establishInitial,omitempty"`
65+
EstablishMax string `json:"establishMax,omitempty"`
66+
ReconnectRetryMax string `json:"reconnectRetryMax,omitempty"`
67+
// FunnelResyncInterval is how often the status funnel reconciles its per-cluster
68+
// watch set against the connected clusters (how fast a newly-connected cluster
69+
// is noticed, not the status latency). FunnelBufferSize is the depth of its
70+
// buffered event channel; a full channel drops events (the safety requeue
71+
// recovers).
72+
FunnelResyncInterval string `json:"funnelResyncInterval,omitempty"`
73+
FunnelBufferSize int `json:"funnelBufferSize,omitempty"`
74+
}
75+
76+
// +kubebuilder:object:generate=false
77+
// PlacementConfig tunes the fan-out placement controller, its status
78+
// convergence, and its orphan GC.
79+
type PlacementConfig struct {
80+
// RequeueInterval is the status-refresh poll cadence. With the cache/funnel off
81+
// it also paces the cross-cluster status re-read.
82+
RequeueInterval string `json:"requeueInterval,omitempty"`
83+
// GCInterval is the orphan-sweep cadence for the placement GC runnable.
84+
GCInterval string `json:"gcInterval,omitempty"`
85+
// MaxConcurrentReconciles caps placement reconciles in parallel (distinct
86+
// ISVCs). Zero falls back to controller-runtime's single worker.
87+
MaxConcurrentReconciles int `json:"maxConcurrentReconciles,omitempty"`
88+
// FanoutTimeout is the per-cluster deadline bounding a single fan-out apply, so
89+
// one slow remote cannot block placement to healthy peers.
90+
FanoutTimeout string `json:"fanoutTimeout,omitempty"`
91+
// WinnerLostGrace is the grace window held before re-placing when the sticky
92+
// winner's derived is absent on a still-connected winner. Empty re-places
93+
// immediately.
94+
WinnerLostGrace string `json:"winnerLostGrace,omitempty"`
95+
// StatusBatchPeriod debounces a burst of cross-cluster derived-status events for
96+
// one ISVC into a single placement reconcile.
97+
StatusBatchPeriod string `json:"statusBatchPeriod,omitempty"`
98+
// StatusSafetyRequeue is the steady-state re-read backstop when the funnel is on
99+
// (events drive freshness; this only recovers a missed event).
100+
StatusSafetyRequeue string `json:"statusSafetyRequeue,omitempty"`
101+
// DispatcherMode is the fan-out breadth policy: "AllAtOnce" clones onto every
102+
// matched candidate at once; "Incremental" probes candidates in batches. Empty
103+
// or unrecognized means AllAtOnce.
104+
DispatcherMode string `json:"dispatcherMode,omitempty"`
105+
// DispatcherStepSize is the candidates the Incremental dispatcher adds per round.
106+
// Non-positive advances by one.
107+
DispatcherStepSize int `json:"dispatcherStepSize,omitempty"`
108+
// DispatcherRoundTimeout is how long an Incremental round waits for a nominated
109+
// cluster to win before adding the next batch. Non-positive enforces no dwell.
110+
DispatcherRoundTimeout string `json:"dispatcherRoundTimeout,omitempty"`
111+
}
112+
113+
// +kubebuilder:object:generate=false
114+
// EndpointConfig tunes the global endpoint publisher (a Gateway API HTTPRoute to
115+
// the placement winner). The host template, gateway, and namespace are
116+
// deployment-identity values with no in-code default: empty disables publishing
117+
// (a no-op), never a baked-in gateway or host.
118+
type EndpointConfig struct {
119+
// GlobalHostTemplate is the text/template for the global host an ISVC publishes
120+
// to the winner. Empty means only ISVCs carrying the global-host annotation
121+
// publish.
122+
GlobalHostTemplate string `json:"globalHostTemplate,omitempty"`
123+
// GlobalGateway is the "namespace/name" of the global-traffic Gateway the
124+
// published HTTPRoute attaches to. Empty makes the publisher a no-op.
125+
GlobalGateway string `json:"globalGateway,omitempty"`
126+
// RouteNamespace is the namespace for the published HTTPRoute and backing
127+
// Service. Empty uses the ISVC's own namespace.
128+
RouteNamespace string `json:"routeNamespace,omitempty"`
129+
// BackendPort is the port on the winner cluster's ingress the global host
130+
// forwards to.
131+
BackendPort int `json:"backendPort,omitempty"`
132+
}
133+
134+
// NewMultiClusterConfig loads the "multicluster" block from the
135+
// inferenceservice-config ConfigMap. It is read once at manager startup (the
136+
// multi-cluster wiring is built before any reconcile), so there is no
137+
// ConfigCache-backed variant. An absent block yields a zero-valued config and
138+
// every consumer applies its own in-package default.
139+
func NewMultiClusterConfig(clientset kubernetes.Interface) (*MultiClusterConfig, error) {
140+
configMap, err := getInferenceServiceConfigMap(clientset)
141+
if err != nil {
142+
return nil, err
143+
}
144+
return parseMultiClusterConfig(configMap)
145+
}
146+
147+
func parseMultiClusterConfig(configMap *v1.ConfigMap) (*MultiClusterConfig, error) {
148+
cfg := &MultiClusterConfig{}
149+
if err := getComponentConfig(MultiClusterConfigName, configMap, cfg); err != nil {
150+
return nil, fmt.Errorf("unable to parse multicluster config json: %w", err)
151+
}
152+
return cfg, nil
153+
}
154+
155+
// PerCallTimeoutDuration returns the parsed PerCallTimeout (0 if absent/unparsable).
156+
func (c WorkloadClusterConfig) PerCallTimeoutDuration() time.Duration {
157+
return parseDurationOrZero(c.PerCallTimeout)
158+
}
159+
160+
// HealthIntervalDuration returns the parsed HealthInterval (0 if absent/unparsable).
161+
func (c WorkloadClusterConfig) HealthIntervalDuration() time.Duration {
162+
return parseDurationOrZero(c.HealthInterval)
163+
}
164+
165+
// ConnectionGraceDuration returns the parsed ConnectionGrace (0 if absent/unparsable).
166+
func (c WorkloadClusterConfig) ConnectionGraceDuration() time.Duration {
167+
return parseDurationOrZero(c.ConnectionGrace)
168+
}
169+
170+
// EventsBatchPeriodDuration returns the parsed EventsBatchPeriod (0 if absent/unparsable).
171+
func (c WorkloadClusterConfig) EventsBatchPeriodDuration() time.Duration {
172+
return parseDurationOrZero(c.EventsBatchPeriod)
173+
}
174+
175+
// EstablishInitialDuration returns the parsed EstablishInitial (0 if absent/unparsable).
176+
func (c WorkloadClusterConfig) EstablishInitialDuration() time.Duration {
177+
return parseDurationOrZero(c.EstablishInitial)
178+
}
179+
180+
// EstablishMaxDuration returns the parsed EstablishMax (0 if absent/unparsable).
181+
func (c WorkloadClusterConfig) EstablishMaxDuration() time.Duration {
182+
return parseDurationOrZero(c.EstablishMax)
183+
}
184+
185+
// ReconnectRetryMaxDuration returns the parsed ReconnectRetryMax (0 if absent/unparsable).
186+
func (c WorkloadClusterConfig) ReconnectRetryMaxDuration() time.Duration {
187+
return parseDurationOrZero(c.ReconnectRetryMax)
188+
}
189+
190+
// FunnelResyncIntervalDuration returns the parsed FunnelResyncInterval (0 if absent/unparsable).
191+
func (c WorkloadClusterConfig) FunnelResyncIntervalDuration() time.Duration {
192+
return parseDurationOrZero(c.FunnelResyncInterval)
193+
}
194+
195+
// RequeueIntervalDuration returns the parsed RequeueInterval (0 if absent/unparsable).
196+
func (c PlacementConfig) RequeueIntervalDuration() time.Duration {
197+
return parseDurationOrZero(c.RequeueInterval)
198+
}
199+
200+
// GCIntervalDuration returns the parsed GCInterval (0 if absent/unparsable).
201+
func (c PlacementConfig) GCIntervalDuration() time.Duration {
202+
return parseDurationOrZero(c.GCInterval)
203+
}
204+
205+
// FanoutTimeoutDuration returns the parsed FanoutTimeout (0 if absent/unparsable).
206+
func (c PlacementConfig) FanoutTimeoutDuration() time.Duration {
207+
return parseDurationOrZero(c.FanoutTimeout)
208+
}
209+
210+
// WinnerLostGraceDuration returns the parsed WinnerLostGrace (0 if absent/unparsable).
211+
func (c PlacementConfig) WinnerLostGraceDuration() time.Duration {
212+
return parseDurationOrZero(c.WinnerLostGrace)
213+
}
214+
215+
// StatusBatchPeriodDuration returns the parsed StatusBatchPeriod (0 if absent/unparsable).
216+
func (c PlacementConfig) StatusBatchPeriodDuration() time.Duration {
217+
return parseDurationOrZero(c.StatusBatchPeriod)
218+
}
219+
220+
// StatusSafetyRequeueDuration returns the parsed StatusSafetyRequeue (0 if absent/unparsable).
221+
func (c PlacementConfig) StatusSafetyRequeueDuration() time.Duration {
222+
return parseDurationOrZero(c.StatusSafetyRequeue)
223+
}
224+
225+
// DispatcherRoundTimeoutDuration returns the parsed DispatcherRoundTimeout (0 if absent/unparsable).
226+
func (c PlacementConfig) DispatcherRoundTimeoutDuration() time.Duration {
227+
return parseDurationOrZero(c.DispatcherRoundTimeout)
228+
}
229+
230+
// parseDurationOrZero parses s, returning 0 when it is empty, malformed, or
231+
// non-positive. Callers hand the zero to a workloadcluster/placement option,
232+
// which then applies its own in-package default — so the fallback stays
233+
// single-sourced in the consuming package, not duplicated here.
234+
func parseDurationOrZero(s string) time.Duration {
235+
if d, err := time.ParseDuration(s); err == nil && d > 0 {
236+
return d
237+
}
238+
return 0
239+
}

0 commit comments

Comments
 (0)