Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `EventHooks` struct with `OnJobStart` and `OnJobComplete` callbacks, set via
`WithEventHooks` option.
- `ErrorFunc` type and `WithOnError` option for error-only callbacks.
- `Shutdown(ctx context.Context) error` for graceful scheduler draining without
cancelling contexts already handed to running jobs.
- `example_test.go` with runnable examples for pkg.go.dev.
- `context.Context` threading throughout the public API:
- `Job.Run(ctx context.Context) error` — jobs receive a cancellable context
Expand All @@ -33,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`ErrAlreadyRunning` if a scheduler is already active.
- `Stop(ctx context.Context) error` — cancels the scheduler, waits for
in-flight jobs bounded by `ctx`.
- `Shutdown(ctx context.Context) error` — stops future scheduling and waits
for in-flight jobs without cancelling their contexts.
- `Clock` interface (`Clock`, `Timer`) and `WithClock` option for deterministic
testing without `time.Sleep`.
- `ErrAlreadyRunning` sentinel error returned by `Run` when called twice.
Expand All @@ -50,6 +54,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `Entry.WrappedJob` is now unexported (`wrappedJob`).
- `FuncJob` signature: `func(context.Context) error` (was `func()`).
- Minimum Go version: **1.26**.
- `@every` rejects non-positive durations while still rounding accepted
sub-second intervals to one second.
- Malformed `TZ=` / `CRON_TZ=` prefixes now return parse errors instead of
panicking.
- Passing nil to `WithLocation`, `WithParser`, `WithLogger`, or `WithClock`
preserves the package defaults.

### Removed

Expand Down
19 changes: 18 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ Returns `cron.ErrAlreadyRunning` if a scheduler is already active.
`Stop` cancels the scheduler and waits for in-flight jobs, bounded by `ctx`. It
returns `ctx.Err()` if the deadline elapses before all jobs finish.

### Shutdown (graceful drain)

If you want to stop scheduling new work without cancelling contexts already
handed to running jobs, use:

```go
err := c.Shutdown(ctx)
```

## 4. Logger — `log/slog` replaces custom interface

The custom `Logger` interface, `PrintfLogger`, and `VerbosePrintfLogger` are
Expand Down Expand Up @@ -131,6 +140,13 @@ if errors.Is(err, cron.ErrPanic) {
}
```

`Recover` is not enabled by default. To preserve v3-style panic recovery,
install it explicitly:

```go
cron.New(cron.WithChain(cron.Recover(logger)))
```

## 9. Removed symbols

| Removed | Replacement |
Expand All @@ -149,7 +165,8 @@ if errors.Is(err, cron.ErrPanic) {

1. Update import path to `github.com/hyp3rd/cron/v4`.
2. Add `context.Context` parameter and `error` return to all `Job` implementations and `FuncJob` / `AddFunc` closures.
3. Pass a `context.Context` to `Start`, `Run`, and `Stop`.
3. Pass a `context.Context` to `Start`, `Run`, and `Stop`, and use
`Shutdown(ctx)` when you need graceful draining instead of cancellation.
4. Replace `Logger`/`PrintfLogger`/`VerbosePrintfLogger` with `*slog.Logger`.
5. Rename `NewParser` calls to `NewSpecParser`.
6. Replace `Entry.WrappedJob` usage with `Entry.Job`.
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,28 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

c.Start(ctx)
c.Start(context.Background())

<-ctx.Done()
c.Stop(context.Background())
c.Shutdown(context.Background())
}
Comment on lines 43 to 50
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The quick-start example creates a signal-aware ctx but then starts the scheduler with context.Background(). That means the scheduler/job contexts are not tied to the interrupt signal (so cancellation/deadlines on ctx won’t propagate to jobs), which undermines the “Context-aware lifecycle” claim right below. Consider passing ctx to Start (and choose Stop vs Shutdown based on whether you want cancellation vs graceful drain).

Copilot uses AI. Check for mistakes.
```

## Features

- **Standard 5-field cron expressions** (minute, hour, dom, month, dow) plus
optional seconds via `WithSeconds()`.
- **`context.Context` throughout** — `Start`, `Run`, `Stop`, and every `Job`
receive a context for cancellation and deadlines.
- **Context-aware lifecycle** — `Start`, `Run`, `Stop`, `Shutdown`, and every
`Job` participate in cancellation and deadlines.
- **`log/slog` logging** — structured, leveled logging out of the box. Default
level is `slog.LevelWarn` to keep the scheduler quiet.
- **`Clock` interface** — inject a fake clock via `WithClock` for deterministic,
zero-`time.Sleep` tests.
- **Job wrappers** — `Recover`, `SkipIfStillRunning`, `DelayIfStillRunning`,
`Timeout`, `MaxConcurrent`, `RetryOnError`, and custom `JobWrapper` chains.
- **Defensive configuration** — malformed `TZ=` / `CRON_TZ=` prefixes and
invalid `@every` intervals return parse errors; nil `With*` options keep
defaults.
- **Named entries** — `AddNamedFunc` / `AddNamedJob` attach human-readable
labels for logging and observability.
- **Event hooks** — `WithEventHooks` for `OnJobStart` / `OnJobComplete`
Expand Down Expand Up @@ -98,6 +101,10 @@ func main() {
@every 1h30m
```

`@every` accepts positive `time.ParseDuration` values. Durations smaller than a
second still round up to 1 second; `@every 0s` and negative durations are
rejected as configuration errors.

### Time zones

```go
Expand All @@ -106,8 +113,26 @@ cron.New(cron.WithLocation(time.UTC))
c.AddFunc("CRON_TZ=Asia/Tokyo 0 6 * * ?", myJob)
```

Malformed timezone prefixes such as `CRON_TZ=` or `CRON_TZ=UTC` without a
schedule body return parse errors instead of panicking.

## Lifecycle

Use `Start(ctx)` for a background scheduler and `Run(ctx)` for a blocking one.
The context passed to `Start` or `Run` is also the parent context for every job.

- `Stop(ctx)` cancels the scheduler and cancels contexts already handed to
running jobs before waiting for them to return.
- `Shutdown(ctx)` stops future scheduling and waits for running jobs to finish
without cancelling their contexts.
- If the `Start` / `Run` context is cancelled directly, both the scheduler and
job contexts are cancelled.

## Job wrappers / Chain

Panic recovery is available but not enabled by default. Install `Recover`
explicitly if you want panics turned into logged `ErrPanic` errors:

```go
c := cron.New(cron.WithChain(
cron.Recover(logger),
Expand Down Expand Up @@ -186,6 +211,11 @@ c := cron.New(cron.WithClock(fakeClock))

See `clock.go` for the interface definition.

## Option defaults

Passing `nil` to `WithLocation`, `WithParser`, `WithLogger`, or `WithClock`
keeps the package default instead of leaving the scheduler in an invalid state.

## Migration from robfig/cron/v3

See [MIGRATION.md](MIGRATION.md) for a step-by-step upgrade guide.
Expand Down
Loading
Loading