Skip to content

Commit a9839db

Browse files
authored
Commitment config loading refactored (#643)
1 parent c73117b commit a9839db

5 files changed

Lines changed: 83 additions & 26 deletions

File tree

cmd/main.go

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -490,25 +490,7 @@ func main() {
490490
monitor := reservations.NewMonitor(multiclusterClient)
491491
metrics.Registry.MustRegister(&monitor)
492492
commitmentsConfig := conf.GetConfigOrDie[commitments.Config]()
493-
commitmentsDefaults := commitments.DefaultConfig()
494-
if commitmentsConfig.RequeueIntervalActive == 0 {
495-
commitmentsConfig.RequeueIntervalActive = commitmentsDefaults.RequeueIntervalActive
496-
}
497-
if commitmentsConfig.RequeueIntervalRetry == 0 {
498-
commitmentsConfig.RequeueIntervalRetry = commitmentsDefaults.RequeueIntervalRetry
499-
}
500-
if commitmentsConfig.PipelineDefault == "" {
501-
commitmentsConfig.PipelineDefault = commitmentsDefaults.PipelineDefault
502-
}
503-
if commitmentsConfig.SchedulerURL == "" {
504-
commitmentsConfig.SchedulerURL = commitmentsDefaults.SchedulerURL
505-
}
506-
if commitmentsConfig.ChangeAPIWatchReservationsTimeout == 0 {
507-
commitmentsConfig.ChangeAPIWatchReservationsTimeout = commitmentsDefaults.ChangeAPIWatchReservationsTimeout
508-
}
509-
if commitmentsConfig.ChangeAPIWatchReservationsPollInterval == 0 {
510-
commitmentsConfig.ChangeAPIWatchReservationsPollInterval = commitmentsDefaults.ChangeAPIWatchReservationsPollInterval
511-
}
493+
commitmentsConfig.ApplyDefaults()
512494

513495
if err := (&commitments.CommitmentReservationController{
514496
Client: multiclusterClient,
@@ -676,10 +658,7 @@ func main() {
676658
must.Succeed(metrics.Registry.Register(syncerMonitor))
677659
syncer := commitments.NewSyncer(multiclusterClient, syncerMonitor)
678660
syncerConfig := conf.GetConfigOrDie[commitments.SyncerConfig]()
679-
syncerDefaults := commitments.DefaultSyncerConfig()
680-
if syncerConfig.SyncInterval == 0 {
681-
syncerConfig.SyncInterval = syncerDefaults.SyncInterval
682-
}
661+
syncerConfig.ApplyDefaults()
683662
if err := (&task.Runner{
684663
Client: multiclusterClient,
685664
Interval: syncerConfig.SyncInterval,

internal/scheduling/reservations/commitments/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ type Config struct {
5959
EnableReportCapacityAPI bool `json:"committedResourceEnableReportCapacityAPI"`
6060
}
6161

62+
// ApplyDefaults fills in any unset values with defaults.
63+
func (c *Config) ApplyDefaults() {
64+
defaults := DefaultConfig()
65+
if c.RequeueIntervalActive == 0 {
66+
c.RequeueIntervalActive = defaults.RequeueIntervalActive
67+
}
68+
if c.RequeueIntervalRetry == 0 {
69+
c.RequeueIntervalRetry = defaults.RequeueIntervalRetry
70+
}
71+
if c.PipelineDefault == "" {
72+
c.PipelineDefault = defaults.PipelineDefault
73+
}
74+
if c.SchedulerURL == "" {
75+
c.SchedulerURL = defaults.SchedulerURL
76+
}
77+
if c.ChangeAPIWatchReservationsTimeout == 0 {
78+
c.ChangeAPIWatchReservationsTimeout = defaults.ChangeAPIWatchReservationsTimeout
79+
}
80+
if c.ChangeAPIWatchReservationsPollInterval == 0 {
81+
c.ChangeAPIWatchReservationsPollInterval = defaults.ChangeAPIWatchReservationsPollInterval
82+
}
83+
// Note: EnableChangeCommitmentsAPI, EnableReportUsageAPI, EnableReportCapacityAPI
84+
// are booleans where false is a valid value, so we don't apply defaults for them
85+
}
86+
6287
func DefaultConfig() Config {
6388
return Config{
6489
RequeueIntervalActive: 5 * time.Minute,

internal/scheduling/reservations/commitments/syncer.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,24 @@ func DefaultSyncerConfig() SyncerConfig {
3636
}
3737
}
3838

39+
// ApplyDefaults fills in any unset values with defaults.
40+
func (c *SyncerConfig) ApplyDefaults() {
41+
defaults := DefaultSyncerConfig()
42+
if c.SyncInterval == 0 {
43+
c.SyncInterval = defaults.SyncInterval
44+
}
45+
// Note: KeystoneSecretRef and SSOSecretRef are not defaulted as they require explicit configuration
46+
}
47+
3948
type Syncer struct {
4049
// Client to fetch commitments from Limes
4150
CommitmentsClient
4251
// Kubernetes client for CRD operations
4352
client.Client
4453
// Monitor for metrics
4554
monitor *SyncerMonitor
55+
// SyncInterval is stored for logging purposes (actual interval managed by task.Runner)
56+
syncInterval time.Duration
4657
}
4758

4859
func NewSyncer(k8sClient client.Client, monitor *SyncerMonitor) *Syncer {
@@ -54,6 +65,7 @@ func NewSyncer(k8sClient client.Client, monitor *SyncerMonitor) *Syncer {
5465
}
5566

5667
func (s *Syncer) Init(ctx context.Context, config SyncerConfig) error {
68+
s.syncInterval = config.SyncInterval
5769
if err := s.CommitmentsClient.Init(ctx, s.Client, config); err != nil {
5870
return err
5971
}
@@ -191,7 +203,7 @@ func (s *Syncer) SyncReservations(ctx context.Context) error {
191203
ctx = WithNewGlobalRequestID(ctx)
192204
logger := LoggerFromContext(ctx).WithValues("component", "syncer", "runID", runID)
193205

194-
logger.Info("starting commitment sync with sync interval", "interval", DefaultSyncerConfig().SyncInterval)
206+
logger.Info("starting commitment sync")
195207

196208
// Record sync run
197209
if s.monitor != nil {

pkg/task/runner.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,26 @@ func (r *Runner) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result,
5353
return ctrl.Result{}, nil
5454
}
5555

56+
// MinInterval is the minimum allowed interval for task runners to prevent panics from time.NewTicker(0).
57+
const MinInterval = 1 * time.Millisecond
58+
5659
// Start starts the task runner, which will send events at the specified interval.
5760
func (r *Runner) Start(ctx context.Context) error {
5861
log := log.FromContext(ctx)
59-
log.Info("starting task runner", "name", r.Name, "interval", r.Interval)
6062

61-
ticker := time.NewTicker(r.Interval)
63+
// Safety: ensure interval is at least MinInterval to prevent tight loops or panics
64+
interval := r.Interval
65+
if interval < MinInterval {
66+
log.Info("task runner interval too low, using minimum",
67+
"name", r.Name,
68+
"configuredInterval", r.Interval,
69+
"minInterval", MinInterval)
70+
interval = MinInterval
71+
}
72+
73+
log.Info("starting task runner", "name", r.Name, "interval", interval)
74+
75+
ticker := time.NewTicker(interval)
6276
defer ticker.Stop()
6377
defer close(r.eventCh)
6478

pkg/task/runner_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,33 @@ func TestRunner_SetupWithManager_EventChannelCreation(t *testing.T) {
251251
}
252252
}
253253

254+
func TestRunner_Start_ZeroInterval_UsesMinimum(t *testing.T) {
255+
runner := &Runner{
256+
Name: "test-task",
257+
Interval: 0, // Zero interval should use MinInterval (1ms)
258+
eventCh: make(chan event.GenericEvent, 10),
259+
}
260+
261+
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
262+
defer cancel()
263+
264+
// Run Start in goroutine since it blocks until context is cancelled
265+
go func() {
266+
// This should NOT panic even with 0 interval
267+
if err := runner.Start(ctx); err != nil {
268+
t.Logf("Start returned error: %v (expected on context cancellation)", err)
269+
}
270+
}()
271+
272+
// Wait for initial event with generous timeout to avoid flakiness on slow machines
273+
select {
274+
case <-runner.eventCh:
275+
// Success - got initial event without panic
276+
case <-time.After(1 * time.Second):
277+
t.Error("Expected to receive initial event")
278+
}
279+
}
280+
254281
func TestRunner_EventStructure(t *testing.T) {
255282
runner := &Runner{
256283
Name: "test-task",

0 commit comments

Comments
 (0)