Skip to content

feat: add cron expression validation to manifest validator#2149

Merged
bjcoombs merged 2 commits intodevelopfrom
per-tenant-scheduling--10--cron-validation
Apr 6, 2026
Merged

feat: add cron expression validation to manifest validator#2149
bjcoombs merged 2 commits intodevelopfrom
per-tenant-scheduling--10--cron-validation

Conversation

@bjcoombs
Copy link
Copy Markdown
Collaborator

@bjcoombs bjcoombs commented Apr 6, 2026

Summary

  • Enhances validateScheduledTriggers to parse inline cron expressions from scheduled:<name>:<cron-expr> triggers using robfig/cron/v3 (already a dependency)
  • Enforces a 15-minute minimum interval between occurrences (error)
  • Caps scheduled sagas per manifest at 20 (error)
  • Warns on schedules that fire less frequently than once per 365 days

Changes Made

services/control-plane/internal/validator/domain_validator.go

  • Added maxScheduledTriggersPerTenant, minCronInterval, warnCronInterval constants
  • Added cronParser package-level var using cron.NewParser(Minute|Hour|Dom|Month|Dow) to match scheduler
  • Extracted parsScheduledTrigger to split name:cron-expr by first colon
  • Added validateCronExpression method covering syntax check + interval checks
  • Updated validateScheduledTriggers to invoke new validations and enforce the 20-schedule cap

Testing

All 9 new tests pass alongside the existing test suite:

  • TestValidateScheduledTriggers_ValidCronExpression - valid hourly cron passes
  • TestValidateScheduledTriggers_NoCronExpression_Valid - scheduled:<name> without cron is valid
  • TestValidateScheduledTriggers_InvalidCronSyntax - invalid cron expression errors with INVALID_CRON_EXPRESSION
  • TestValidateScheduledTriggers_IntervalTooShort - every-minute cron errors with CRON_INTERVAL_TOO_SHORT
  • TestValidateScheduledTriggers_IntervalExactly15Minutes_Valid - exactly 15-minute interval passes
  • TestValidateScheduledTriggers_TooManySchedules - 21 schedules errors with TOO_MANY_SCHEDULED_TRIGGERS
  • TestValidateScheduledTriggers_InfrequentScheduleWarns - annual cron warns with CRON_VERY_INFREQUENT
go test ./services/control-plane/internal/validator/... 
ok  github.com/meridianhub/meridian/services/control-plane/internal/validator  5.760s

Enhances validateScheduledTriggers to parse and validate inline cron
expressions from scheduled:<name>:<cron-expr> triggers. Enforces a
15-minute minimum interval, 20-schedule-per-manifest cap, and warns on
schedules that fire less frequently than once per 365 days.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 6, 2026

Warning

Rate limit exceeded

@bjcoombs has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 59 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 6 minutes and 59 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1a5b8f9b-76aa-4cc7-a129-2faaeaf98162

📥 Commits

Reviewing files that changed from the base of the PR and between b6f74d6 and a36812c.

📒 Files selected for processing (2)
  • services/control-plane/internal/validator/domain_validator.go
  • services/control-plane/internal/validator/domain_validator_test.go
📝 Walkthrough

Walkthrough

Enhanced validation for scheduled saga triggers in the control plane domain validator. Added checks for: tenant-wide scheduled trigger count limits, cron expression parsing and validity, minimum interval enforcement between cron occurrences, and warnings for infrequent schedules. Includes new helper function and validation method plus comprehensive test coverage.

Changes

Cohort / File(s) Summary
Scheduled Trigger Validation
services/control-plane/internal/validator/domain_validator.go
Added validateCronExpression() method to parse and validate cron syntax, compute occurrence intervals, enforce minimum interval thresholds, and emit warnings for infrequent schedules. Added parsScheduledTrigger() helper to parse trigger name and cron expression. Extended validateScheduledTriggers() to enforce tenant-wide scheduled trigger count limits via TOO_MANY_SCHEDULED_TRIGGERS error.
Scheduled Trigger Tests
services/control-plane/internal/validator/domain_validator_test.go
Added 7 test cases covering valid cron expressions, missing cron portions, invalid cron syntax, interval violations, maximum trigger limits, and infrequent schedule warnings. Tests verify correct error and warning emissions at appropriate validation paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main change: adding cron expression validation to the manifest validator. It is concise and captures the primary objective.
Description check ✅ Passed The description is comprehensive and directly related to the changeset. It explains the summary, changes made, and testing performed, all aligned with the cron validation implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch per-tenant-scheduling--10--cron-validation

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 6, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/control-plane/internal/validator/domain_validator.go (1)

398-414: ⚠️ Potential issue | 🟡 Minor

Reject an explicit empty cron suffix.

Line 399 collapses scheduled:<name> and scheduled:<name>: into the same (name, "") shape, and Line 412 treats both as “no inline cron provided.” That silently accepts a malformed trigger instead of surfacing a validation error. Preserve whether the second separator was present and fail when it is present but empty.

💡 One way to preserve that distinction
-		name, cronExpr := parsScheduledTrigger(remainder)
+		name, cronExpr, hasCron := parseScheduledTrigger(remainder)

 		if firstIdx, exists := seen[name]; exists {
 			addError(result, ValidationError{
 				Severity: SeverityError,
 				Path:     fmt.Sprintf("sagas[%d].trigger", i),
@@
-		if cronExpr != "" {
+		if hasCron {
+			if cronExpr == "" {
+				addError(result, ValidationError{
+					Severity:     SeverityError,
+					Path:         fmt.Sprintf("sagas[%d].trigger", i),
+					Code:         "INVALID_CRON_EXPRESSION",
+					Message:      "cron expression cannot be empty",
+					ResourceType: "saga",
+					ResourceID:   saga.GetName(),
+				})
+				continue
+			}
 			v.validateCronExpression(cronExpr, fmt.Sprintf("sagas[%d].trigger", i), saga.GetName(), result)
 		}
 	}
 }
 
-// parsScheduledTrigger splits "name" or "name:cron expr" into (name, cronExpr).
+// parseScheduledTrigger splits "name" or "name:cron expr" into (name, cronExpr, hasCron).
 // The cron expression may contain spaces, so only the first colon is used as separator.
-func parsScheduledTrigger(remainder string) (name, cronExpr string) {
+func parseScheduledTrigger(remainder string) (name, cronExpr string, hasCron bool) {
 	idx := strings.Index(remainder, ":")
 	if idx < 0 {
-		return remainder, ""
+		return remainder, "", false
 	}
-	return remainder[:idx], remainder[idx+1:]
+	return remainder[:idx], remainder[idx+1:], true
 }

Also applies to: 427-434

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/control-plane/internal/validator/domain_validator.go` around lines
398 - 414, parsScheduledTrigger currently collapses "scheduled:name" and
"scheduled:name:" into the same (name, "") so a trailing colon (explicit empty
cron) is silently accepted; update the validation after calling
parsScheduledTrigger to detect when the trigger had a separator but an empty
cron (you can change parsScheduledTrigger to return a third bool like
separatorPresent or have it return nil vs "" differently), and when
separatorPresent && cronExpr == "" call addError with a ValidationError
(SeverityError, Path fmt.Sprintf("sagas[%d].trigger", i), Code like
"EMPTY_SCHEDULED_CRON", and a helpful Message) instead of calling
validateCronExpression; keep the existing duplicate-name check using seen and
only call validateCronExpression when cronExpr != "" and not explicitly empty.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@services/control-plane/internal/validator/domain_validator.go`:
- Around line 452-467: The current check uses a single sampled gap from now
(using schedule.Next) which can miss shorter intervals; change the logic in
domain_validator.go so that instead of computing only t1 := schedule.Next(now)
and t2 := schedule.Next(t1), you pick a fixed anchor (e.g., a constant UTC time)
and iterate schedule.Next repeatedly to collect many consecutive occurrences,
compute the minimum difference between any adjacent occurrences, and compare
that minimum to minCronInterval; update the code paths that call
addError/ValidationError (using expr, sagaName, path, result) to report when
this computed minimum is < minCronInterval.

---

Outside diff comments:
In `@services/control-plane/internal/validator/domain_validator.go`:
- Around line 398-414: parsScheduledTrigger currently collapses "scheduled:name"
and "scheduled:name:" into the same (name, "") so a trailing colon (explicit
empty cron) is silently accepted; update the validation after calling
parsScheduledTrigger to detect when the trigger had a separator but an empty
cron (you can change parsScheduledTrigger to return a third bool like
separatorPresent or have it return nil vs "" differently), and when
separatorPresent && cronExpr == "" call addError with a ValidationError
(SeverityError, Path fmt.Sprintf("sagas[%d].trigger", i), Code like
"EMPTY_SCHEDULED_CRON", and a helpful Message) instead of calling
validateCronExpression; keep the existing duplicate-name check using seen and
only call validateCronExpression when cronExpr != "" and not explicitly empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65ad5fa1-abd2-4a6b-9d98-83c697cacde7

📥 Commits

Reviewing files that changed from the base of the PR and between 31519f4 and b6f74d6.

📒 Files selected for processing (2)
  • services/control-plane/internal/validator/domain_validator.go
  • services/control-plane/internal/validator/domain_validator_test.go

Comment thread services/control-plane/internal/validator/domain_validator.go Outdated
@claude
Copy link
Copy Markdown

claude Bot commented Apr 6, 2026

Claude Code Review

Commit: a36812c | CI: running

Summary

Clean, well-scoped addition of cron expression validation to the manifest validator. The second commit addressed all prior review feedback: interval sampling now uses a fixed anchor with 10 consecutive samples (catching non-uniform gaps), and the function name typo is fixed. Test coverage is thorough — 7 new test cases covering valid expressions, invalid syntax, interval floors, the cap limit, empty-cron edge case, and the infrequent-schedule warning. No domain-level concerns.

Risk Assessment

Area Level Detail
Blast radius Low Validation-only change — rejects invalid manifests at submit time, no runtime behavior change
Rollback Safe Reverting removes the new validation rules; no data or schema impact
Scale Low Validation runs at deploy/submit time, not on hot path
Cross-system Low Uses same 5-field cron parser as the scheduler (cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
Migration N/A No schema changes

Findings

No open findings.

Previously Flagged

Severity Location Description Status
Improvement domain_validator.go:453 Interval check sampled one pair from time.Now() — could miss true minimum for non-uniform cron specs Resolved in a36812c (fixed anchor + 10 samples)
Note domain_validator.go:425 Function name typo parsScheduledTrigger Resolved in a36812c
Note domain_validator.go:21 Constant named "perTenant" but validates per manifest Acknowledged — naming is accurate enough given 1:1 manifest-tenant relationship

Bot Review Notes

  • CodeRabbit flagged the interval sampling issue (single pair from time.Now()). Resolved in a36812c — now uses a fixed UTC anchor (2001-01-01) and samples 10 consecutive occurrences to find the true minimum. CodeRabbit marked it as addressed. No unresolved bot threads remain.

- Rename parsScheduledTrigger -> parseScheduledTrigger (typo)
- Return hasCron bool to distinguish scheduled:name from scheduled:name:
  so an explicit empty cron suffix is rejected with INVALID_CRON_EXPRESSION
- Use fixed anchor and 10-sample minimum for interval check instead of
  sampling only 2 occurrences from now, avoiding time-dependent results
@bjcoombs bjcoombs dismissed coderabbitai[bot]’s stale review April 6, 2026 11:18

Addressed both issues: empty cron suffix now errors, interval check uses fixed anchor with 10-sample minimum

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

All prior findings resolved. Cron validation logic is correct: fixed anchor, 10-sample minimum interval detection, proper error codes, and comprehensive test coverage. No domain-level concerns — see summary comment for details.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@bjcoombs bjcoombs merged commit e658521 into develop Apr 6, 2026
43 checks passed
@bjcoombs bjcoombs deleted the per-tenant-scheduling--10--cron-validation branch April 6, 2026 12:07
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.

1 participant