Conversation
…ule inspection - Add `Entry.Name` field with `AddNamedFunc`, `AddNamedJob`, and `ScheduleNamed` methods for attaching human-readable labels to entries (logs, hooks, debugging) - Add `Timeout(time.Duration)` job wrapper — cancels the job context after a deadline - Add `MaxConcurrent(int, *slog.Logger)` job wrapper — generalizes `SkipIfStillRunning` to allow up to N concurrent invocations - Add `RetryOnError(int, time.Duration)` job wrapper — retries on failure with configurable backoff, respecting context cancellation - Add `EventHooks` struct with `OnJobStart` / `OnJobComplete` callbacks via `WithEventHooks` option for lifecycle observability (tracing, metrics) - Add `ErrorFunc` type and `WithOnError` option for error-only alerting callbacks - Add `NextN(Schedule, time.Time, int) []time.Time` to preview future fire times - Add `SpecSchedule.String()` to round-trip a parsed schedule back to a cron expression - Refactor `Schedule` / `AddJob` into internal `scheduleEntry` / `parseAndSchedule` helpers to reduce duplication - Update `startJob` to fire lifecycle hooks and route errors to `onError` callback - Include entry name in all structured log messages for improved observability - Add `example_test.go` with runnable pkg.go.dev examples for all new public APIs - Add depguard linter rules to `.golangci.yaml` - Remove Codacy security scan workflow - Update CHANGELOG and README with full feature documentation
There was a problem hiding this comment.
Pull request overview
This PR expands the cron/v4 public API to improve observability and ergonomics around scheduled jobs (naming, lifecycle hooks, error callbacks), adds new job wrappers (timeout, concurrency limiting, retries), and introduces helpers for inspecting schedules.
Changes:
- Add schedule inspection helpers:
NextNandSpecSchedule.String()(+ tests/examples). - Add job execution observability: entry naming, lifecycle hooks, and error-only callbacks; include entry name in logs.
- Add new job wrappers:
Timeout,MaxConcurrent, andRetryOnError(+ tests/examples/docs).
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec.go | Add SpecSchedule.String() to render schedules back to cron expressions. |
| spec_test.go | Add tests for SpecSchedule.String() and round-tripping via parse/Next(). |
| schedule.go | Introduce NextN helper for previewing future activation times. |
| schedule_test.go | Add unit tests for NextN (counts, unsatisfiable schedules, edge counts). |
| cron.go | Add Entry.Name, named scheduling APIs, lifecycle hooks/error callback wiring, and richer logs. |
| option.go | Add ErrorFunc, WithOnError, EventHooks, and WithEventHooks options. |
| chain.go | Add wrappers: MaxConcurrent, Timeout, RetryOnError. |
| chain_test.go | Add unit tests for the new wrappers. |
| example_test.go | Add pkg.go.dev runnable examples for new APIs and wrappers. |
| doc.go | Document new wrappers, named entries, hooks, and schedule inspection APIs. |
| README.md | Update feature list and add usage docs for new wrappers/hooks/inspection. |
| CHANGELOG.md | Document newly added APIs and behavior. |
| .golangci.yaml | Add depguard linter rules. |
| .github/workflows/codacy.yml | Remove Codacy security scan workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func MaxConcurrent(limit int, logger *slog.Logger) JobWrapper { | ||
| return func(job Job) Job { | ||
| sem := make(chan struct{}, limit) | ||
|
|
||
| for range limit { | ||
| sem <- struct{}{} | ||
| } |
There was a problem hiding this comment.
MaxConcurrent panics if limit is negative because make(chan struct{}, limit) requires a non-negative capacity. Consider guarding/clamping limit (e.g., treat limit < 1 as 1, or return a wrapper that always skips) so callers can't crash the scheduler by passing a bad limit.
| func executeWithRetry(ctx context.Context, job Job, maxRetries int, backoff time.Duration) error { | ||
| var err error | ||
|
|
||
| for attempt := range maxRetries + 1 { | ||
| err = job.Run(ctx) | ||
| if err == nil { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
RetryOnError with a negative maxRetries currently results in the wrapped job never running (the for attempt := range maxRetries + 1 loop does zero iterations) and returns nil. It would be safer to clamp maxRetries to 0 (meaning “run once, no retries”) so invalid input can’t silently skip execution.
| c.jobWaiter.Go(func() { | ||
| err := j.Run(ctx) | ||
| if c.hooks.OnJobStart != nil { | ||
| c.hooks.OnJobStart(entry.ID, entry.Name) | ||
| } | ||
|
|
||
| start := time.Now() | ||
|
|
||
| err := entry.wrappedJob.Run(ctx) | ||
|
|
||
| if c.hooks.OnJobComplete != nil { | ||
| c.hooks.OnJobComplete(entry.ID, entry.Name, time.Since(start), err) | ||
| } |
There was a problem hiding this comment.
EventHooks and onError callbacks are invoked outside the job wrapper chain. If any callback panics, the panic will crash the process even if the job is wrapped with Recover. Consider protecting these callbacks with a deferred recover (and logging) so observability hooks can’t bring down the scheduler.
| // ErrorFunc is called after a job returns a non-nil error. It fires in the | ||
| // job's goroutine, so it must be safe for concurrent use. | ||
| type ErrorFunc func(id EntryID, name string, err error) | ||
|
|
||
| // WithOnError registers a callback invoked whenever a job returns a non-nil | ||
| // error. The callback receives the entry's ID, name, and the error. | ||
| func WithOnError(fn ErrorFunc) Option { | ||
| return func(c *Cron) { | ||
| c.onError = fn | ||
| } | ||
| } | ||
|
|
||
| // EventHooks contains optional callbacks for job lifecycle events. All | ||
| // callbacks fire in the job's goroutine and must be safe for concurrent use. | ||
| type EventHooks struct { | ||
| // OnJobStart is called just before a job begins execution. | ||
| OnJobStart func(id EntryID, name string) | ||
|
|
||
| // OnJobComplete is called after a job finishes, with the duration and | ||
| // the error (or nil). | ||
| OnJobComplete func(id EntryID, name string, elapsed time.Duration, err error) | ||
| } | ||
|
|
||
| // WithEventHooks registers lifecycle callbacks for job execution events. | ||
| func WithEventHooks(hooks EventHooks) Option { | ||
| return func(c *Cron) { | ||
| c.hooks = hooks | ||
| } | ||
| } |
There was a problem hiding this comment.
WithOnError / WithEventHooks introduce new observable behavior (callbacks firing on job start/complete and error), but there are no unit tests asserting these callbacks are invoked with the expected ID/name/err values. Since option.go already has tests, consider adding tests that start a Cron with a fake clock and verify hook/onError invocation for success and failure cases.
Wrap event-hook invocations (OnJobStart, OnJobComplete, OnError) in a new safeCallHook helper that uses defer/recover, preventing panicking callbacks from crashing the scheduler. Extract executeJob from startJob for clearer separation of concerns. Clamp MaxConcurrent limit to ≥1 and RetryOnError maxRetries to ≥0 to guard against invalid arguments. Add comprehensive tests for event hooks, error callback behaviour, and hook-panic resilience. Introduce mustAddNamedFunc test helper. Add GitHub Sponsors funding configuration.
Entry.Namefield withAddNamedFunc,AddNamedJob, andScheduleNamedmethods for attaching human-readable labels to entries (logs, hooks, debugging)Timeout(time.Duration)job wrapper — cancels the job context after a deadlineMaxConcurrent(int, *slog.Logger)job wrapper — generalizesSkipIfStillRunningto allow up to N concurrent invocationsRetryOnError(int, time.Duration)job wrapper — retries on failure with configurable backoff, respecting context cancellationEventHooksstruct withOnJobStart/OnJobCompletecallbacks viaWithEventHooksoption for lifecycle observability (tracing, metrics)ErrorFunctype andWithOnErroroption for error-only alerting callbacksNextN(Schedule, time.Time, int) []time.Timeto preview future fire timesSpecSchedule.String()to round-trip a parsed schedule back to a cron expressionSchedule/AddJobinto internalscheduleEntry/parseAndSchedulehelpers to reduce duplicationstartJobto fire lifecycle hooks and route errors toonErrorcallbackexample_test.gowith runnable pkg.go.dev examples for all new public APIs.golangci.yaml