Skip to content

feat: cron-based MonoVertex autoscaling spec#3558

Merged
vigith merged 6 commits into
numaproj:mainfrom
devsarvesh92:feat/cron-monovertex-autoscaling
Jul 25, 2026
Merged

feat: cron-based MonoVertex autoscaling spec#3558
vigith merged 6 commits into
numaproj:mainfrom
devsarvesh92:feat/cron-monovertex-autoscaling

Conversation

@devsarvesh92

@devsarvesh92 devsarvesh92 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What this PR does / why we need it

Adds cron-window-based autoscaling bounds for MonoVertex.

Users can configure recurring windows with minimum and maximum replica
counts. While a window is active:

  • Replicas outside the configured bounds move gradually toward the nearest
    bound.
  • Existing scale-up and scale-down cooldowns remain respected.
  • Reactive metric-based autoscaling continues within the active bounds.
  • Cron bounds are applied before daemon metrics are queried, allowing
    scheduled scale-up from zero.

When cron is omitted, existing autoscaling behavior is unchanged.

Example:

scale:
  min: 0
  max: 50
  cron:
    timezone: America/Los_Angeles
    schedules:
      - start: "0 2 * * * *"
        end: "0 3 * * * *"
        min: 1
        max: 5

Related issues

Fixes #3546

Parent issue: #3544

Testing

  • make test
  • make pre-push -B
  • go vet ./pkg/apis/numaflow/v1alpha1 ./pkg/reconciler/monovertex/scaling ./pkg/reconciler/validator
  • git diff --check
  • Local kind integration:
    • Verified active cron scale-up patches spec.replicas before metrics.
    • Verified gradual replica changes honor replicasPerScaleUp.
    • Verified the controller reconciles patched replicas and creates worker pods.
    • Verified required timezone is rejected by the CRD schema.
    • Verified incompatible start/end calendar fields are rejected by the webhook.

Special notes for reviewers

  • Cron configuration is optional.
  • When cron is configured, timezone, schedules, min, and max are required.
  • Schedule bounds must remain within the parent scale bounds.
  • Overlapping schedule detection is deferred; the first active schedule wins.
  • Scale is shared, so generated Pipeline and Vertex CRDs also contain the
    cron schema. Pipeline runtime support is outside this sub-issue.
  • CRD, OpenAPI, protobuf, install manifest, and API documentation changes are
    generated.

Contributors

learncoder4848 and others added 2 commits July 23, 2026 19:49
Define scheduled scale bounds and reject invalid cron windows before
resources are reconciled.

Co-authored-by: Sarvesh Sawant <[email protected]>
Signed-off-by: Shantanu Vashishtha <[email protected]>
Signed-off-by: Sarvesh Sawant <[email protected]>
Apply scheduled replica bounds before daemon metrics are available while
preserving gradual, metric-driven scaling within active windows.

Co-authored-by: Shantanu Vashishtha <[email protected]>
Signed-off-by: Sarvesh Sawant <[email protected]>
Signed-off-by: Shantanu Vashishtha <[email protected]>
@devsarvesh92
devsarvesh92 marked this pull request as ready for review July 23, 2026 14:52
@vigith vigith changed the title feat: support cron-based MonoVertex autoscaling. Fixes #3546 feat: cron-based MonoVertex autoscaling spec Jul 23, 2026
Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated
Comment on lines +28 to +29
Timezone string `json:"timezone" protobuf:"bytes,1,opt,name=timezone"`
Schedules []CronSchedule `json:"schedules" protobuf:"bytes,2,rep,name=schedules"`

@vaibhavtiwari33 vaibhavtiwari33 Jul 23, 2026

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.

nit: Could we document the cron format being used. Standard vs extended

I see it documented below

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated
Comment on lines +33 to +34
// Start of the cron window. Linux cron format (Minute Hour Dom Month Dow).
Start string `json:"start" protobuf:"bytes,1,opt,name=start"`

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.

nit: For sideinputs we're using extended format (6 fields). This might create confusion between users about which one to use.
ref: #3054

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated

// IsActiveAt reports whether time t falls within this cron window.
func (cs CronSchedule) IsActiveAt(t time.Time) bool {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)

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.

Suggested change
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated
// CronScheduling defines cron-based autoscaling overrides.
type CronScheduling struct {
// Timezone for interpreting cron expressions. IANA Time Zone Database format.
Timezone string `json:"timezone" protobuf:"bytes,1,opt,name=timezone"`

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.

nit: Can this be made optional with default being UTC?

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated
for i := range s.Cron.Schedules {
if s.Cron.Schedules[i].IsActiveAt(at) {
return &s.Cron.Schedules[i], true
}

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.

We should document that we honor the first matching schedule in case when there exist multiple overlapping schedules.

@vaibhavtiwari33

Copy link
Copy Markdown
Contributor

Could we also please update the autoscaling doc (specifically the mvtx section)

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated

// IsActiveAt reports whether time t falls within this cron window.
func (cs CronSchedule) IsActiveAt(t time.Time) bool {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

parsed schedules should be pre-computed, it is computationally heavy and performance will be impacted since it is in hot code path of reconciliation.

Comment thread pkg/apis/numaflow/v1alpha1/scale.go Outdated
func (s Scale) GetEffectiveScaleBounds() (int32, int32, bool) {
minR := s.GetMinReplicas()
maxR := s.GetMaxReplicas()
sched, active := s.GetActiveCronSchedule(time.Now())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this can make it tough to unit-test, can we pass time.Now() as an arg.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

 227 +  if v.Scale.Cron != nil {                                                                                                                                                              
 228 +    return fmt.Errorf("vertex %q: cron autoscaling is not supported for pipeline vertices", v.Name)                                                                                     
 229 +  } 

we need to add this pipeline else it will be silently ignored.

Comment on lines +98 to +103
if scale.Min != nil && *scale.Min < 0 {
return fmt.Errorf("scale.min must not be negative")
}
if scale.Max != nil && *scale.Max < 0 {
return fmt.Errorf("scale.max must not be negative")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The cronMin >= parentMin and cronMax <= parentMax is concerning. This blocks the common pattern where parentMin=1 (always keep at least one replica running) but during a quiet window you want the cron to scale down to 0. If the intent of cron overrides is to override the parent bounds, this constraint should be relaxed.

Comment on lines +185 to +191
if cronActive {
if current < minReplicas {
return s.scaleUp(ctx, monoVtx, current, minReplicas, secondsSinceLastScale, scaleUpCooldown)
}
if current > maxReplicas {
return s.scaleDown(ctx, monoVtx, current, maxReplicas, secondsSinceLastScale, scaleDownCooldown)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

log.Infof("Cron window active [min=%d, max=%d], current=%d is outside bounds; scaling.", minReplicas, maxReplicas, current)

Precompute cron schedules in the autoscaler and align validation with
six-field expressions and UTC defaults.
Reject cron configuration on unsupported Pipeline vertices and document
schedule precedence.

Co-authored-by: Shantanu Vashishtha <[email protected]>
Signed-off-by: Sarvesh Sawant <[email protected]>
Signed-off-by: Shantanu Vashishtha <[email protected]>
@learncoder4848
learncoder4848 force-pushed the feat/cron-monovertex-autoscaling branch from d527eba to e2a0a35 Compare July 24, 2026 16:39
@learncoder4848

Copy link
Copy Markdown
Contributor

Thanks for the thorough reviews, @vaibhavtiwari33 and @vigith! The latest commit addresses all the comments. Summary below.

@vigith

  • Pre-compute parsed cron schedules (hot path perf): Schedules are now parsed once via CronScheduling.Compile() and cached in the scaler (cronScheduleCache, keyed by UID/Generation), so no cron parsing happens on the reconciliation hot path. The cache invalidates automatically on spec changes (Generation) and object recreation (UID).
  • Pass time.Now() for testability: Bound evaluation now flows through effectiveScaleBoundsAt(scale, parsed, at) with the time passed in explicitly, so it's unit-testable without wall-clock dependence.
  • Reject cron on pipeline vertices (silent-ignore): Added validation in validateVertex — pipeline vertices with scale.cron are now rejected with cron autoscaling is not supported for pipeline vertices instead of being silently ignored.
  • Relax cronMin >= parentMin / cronMax <= parentMax: Removed these containment checks so cron windows can override the parent bounds (e.g. parentMin=1 scaling down to 0 during a quiet window).
  • Log when cron forces scaling outside bounds: Added the info log when an active cron window scales because current replicas are outside [min, max].

@vaibhavtiwari33

  • Cron format documentation / consistency with side inputs: Switched to the six-field extended format (second minute hour dom month dow), matching the side-input convention, and documented it. (ref Sideinput not working in v1.6: ERROR numaflow: Schedule("*/1 * * * *\n ^\n") #3054)
  • Timezone optional, default UTC: timezone is now optional and defaults to UTC via CronScheduling.GetTimezone(); validation and CRDs updated accordingly.
  • Document first-match on overlapping schedules: Documented that schedules are evaluated in order and the first active schedule takes precedence, both in the API godoc and the autoscaling reference doc.
  • Update autoscaling docs (mvtx section): Updated docs/user-guide/reference/autoscaling.md with the cron field, examples, format, timezone default, and precedence behavior.

@codecov

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.02098% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.57%. Comparing base (7b02ed9) to head (9108a87).

Files with missing lines Patch % Lines
pkg/reconciler/monovertex/scaling/scaling.go 71.42% 15 Missing and 9 partials ⚠️
pkg/reconciler/validator/mvtx_validate.go 88.67% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3558      +/-   ##
==========================================
- Coverage   83.58%   83.57%   -0.01%     
==========================================
  Files         311      311              
  Lines       83097    83220     +123     
==========================================
+ Hits        69455    69550      +95     
- Misses      13046    13055       +9     
- Partials      596      615      +19     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vigith vigith left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

thank you, LGTM

@learncoder4848

Copy link
Copy Markdown
Contributor

thank you, LGTM

@vigith Can you approve the awaiting workflows so that we can merge the changes ?

@vigith
vigith enabled auto-merge (squash) July 25, 2026 16:29
targetProcessingSeconds: 20 # Optional, defaults to 20.
replicasPerScaleUp: 2 # Optional, defaults to 2.
replicasPerScaleDown: 2 # Optional, defaults to 2.
cron: # Optional; supported only for MonoVertex.

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.

nit:

Suggested change
cron: # Optional; supported only for MonoVertex.
cron: # Optional; currently only supported for MonoVertex.

Since, I believe there exists an issue to add this functionality for pipeline as well

@learncoder4848

learncoder4848 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

@vigith can you re-trigger/approve this [test / E2E Tests (jetstream, e2e) (pull_request)] , don't know what is this

@vigith
vigith merged commit 05660f7 into numaproj:main Jul 25, 2026
41 of 42 checks passed
@vigith

vigith commented Jul 25, 2026

Copy link
Copy Markdown
Member

@vigith can you re-trigger/approve this [test / E2E Tests (jetstream, e2e) (pull_request)] , don't know what is this

it is successful now, Github Runners are not very stable

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.

Cron-based autoscaling: MonoVertex controller changes

4 participants