Skip to content

feat(v4): add named entries, event hooks, new job wrappers, and schedule inspection#3

Merged
hyp3rd merged 3 commits intomainfrom
feat/v4
Apr 10, 2026
Merged

feat(v4): add named entries, event hooks, new job wrappers, and schedule inspection#3
hyp3rd merged 3 commits intomainfrom
feat/v4

Conversation

@hyp3rd
Copy link
Copy Markdown
Owner

@hyp3rd hyp3rd commented Apr 10, 2026

  • 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

…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
Copilot AI review requested due to automatic review settings April 10, 2026 08:20
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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: NextN and SpecSchedule.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, and RetryOnError (+ 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.

Comment thread chain.go
Comment on lines +126 to +132
func MaxConcurrent(limit int, logger *slog.Logger) JobWrapper {
return func(job Job) Job {
sem := make(chan struct{}, limit)

for range limit {
sem <- struct{}{}
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread chain.go
Comment on lines +180 to +187
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
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread cron.go
Comment on lines 533 to +544
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)
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread option.go
Comment on lines +56 to +84
// 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
}
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
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.
@hyp3rd hyp3rd merged commit 635850f into main Apr 10, 2026
8 checks passed
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.

2 participants