Skip to content

Commit 0a799e5

Browse files
authored
feat(cron): add Shutdown for graceful drain and harden scheduler lifecycle (#5)
2 parents 7bc6513 + 25e437f commit 0a799e5

11 files changed

Lines changed: 649 additions & 116 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- `EventHooks` struct with `OnJobStart` and `OnJobComplete` callbacks, set via
2525
`WithEventHooks` option.
2626
- `ErrorFunc` type and `WithOnError` option for error-only callbacks.
27+
- `Shutdown(ctx context.Context) error` for graceful scheduler draining without
28+
cancelling contexts already handed to running jobs.
2729
- `example_test.go` with runnable examples for pkg.go.dev.
2830
- `context.Context` threading throughout the public API:
2931
- `Job.Run(ctx context.Context) error` — jobs receive a cancellable context
@@ -33,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3335
`ErrAlreadyRunning` if a scheduler is already active.
3436
- `Stop(ctx context.Context) error` — cancels the scheduler, waits for
3537
in-flight jobs bounded by `ctx`.
38+
- `Shutdown(ctx context.Context) error` — stops future scheduling and waits
39+
for in-flight jobs without cancelling their contexts.
3640
- `Clock` interface (`Clock`, `Timer`) and `WithClock` option for deterministic
3741
testing without `time.Sleep`.
3842
- `ErrAlreadyRunning` sentinel error returned by `Run` when called twice.
@@ -50,6 +54,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5054
- `Entry.WrappedJob` is now unexported (`wrappedJob`).
5155
- `FuncJob` signature: `func(context.Context) error` (was `func()`).
5256
- Minimum Go version: **1.26**.
57+
- `@every` rejects non-positive durations while still rounding accepted
58+
sub-second intervals to one second.
59+
- Malformed `TZ=` / `CRON_TZ=` prefixes now return parse errors instead of
60+
panicking.
61+
- Passing nil to `WithLocation`, `WithParser`, `WithLogger`, or `WithClock`
62+
preserves the package defaults.
5363

5464
### Removed
5565

MIGRATION.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ Returns `cron.ErrAlreadyRunning` if a scheduler is already active.
7575
`Stop` cancels the scheduler and waits for in-flight jobs, bounded by `ctx`. It
7676
returns `ctx.Err()` if the deadline elapses before all jobs finish.
7777

78+
### Shutdown (graceful drain)
79+
80+
If you want to stop scheduling new work without cancelling contexts already
81+
handed to running jobs, use:
82+
83+
```go
84+
err := c.Shutdown(ctx)
85+
```
86+
7887
## 4. Logger — `log/slog` replaces custom interface
7988

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

143+
`Recover` is not enabled by default. To preserve v3-style panic recovery,
144+
install it explicitly:
145+
146+
```go
147+
cron.New(cron.WithChain(cron.Recover(logger)))
148+
```
149+
134150
## 9. Removed symbols
135151

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

150166
1. Update import path to `github.com/hyp3rd/cron/v4`.
151167
2. Add `context.Context` parameter and `error` return to all `Job` implementations and `FuncJob` / `AddFunc` closures.
152-
3. Pass a `context.Context` to `Start`, `Run`, and `Stop`.
168+
3. Pass a `context.Context` to `Start`, `Run`, and `Stop`, and use
169+
`Shutdown(ctx)` when you need graceful draining instead of cancellation.
153170
4. Replace `Logger`/`PrintfLogger`/`VerbosePrintfLogger` with `*slog.Logger`.
154171
5. Rename `NewParser` calls to `NewSpecParser`.
155172
6. Replace `Entry.WrappedJob` usage with `Entry.Job`.

README.md

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,28 @@ func main() {
4343
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
4444
defer stop()
4545

46-
c.Start(ctx)
46+
c.Start(context.Background())
4747

4848
<-ctx.Done()
49-
c.Stop(context.Background())
49+
c.Shutdown(context.Background())
5050
}
5151
```
5252

5353
## Features
5454

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

104+
`@every` accepts positive `time.ParseDuration` values. Durations smaller than a
105+
second still round up to 1 second; `@every 0s` and negative durations are
106+
rejected as configuration errors.
107+
101108
### Time zones
102109

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

116+
Malformed timezone prefixes such as `CRON_TZ=` or `CRON_TZ=UTC` without a
117+
schedule body return parse errors instead of panicking.
118+
119+
## Lifecycle
120+
121+
Use `Start(ctx)` for a background scheduler and `Run(ctx)` for a blocking one.
122+
The context passed to `Start` or `Run` is also the parent context for every job.
123+
124+
- `Stop(ctx)` cancels the scheduler and cancels contexts already handed to
125+
running jobs before waiting for them to return.
126+
- `Shutdown(ctx)` stops future scheduling and waits for running jobs to finish
127+
without cancelling their contexts.
128+
- If the `Start` / `Run` context is cancelled directly, both the scheduler and
129+
job contexts are cancelled.
130+
109131
## Job wrappers / Chain
110132

133+
Panic recovery is available but not enabled by default. Install `Recover`
134+
explicitly if you want panics turned into logged `ErrPanic` errors:
135+
111136
```go
112137
c := cron.New(cron.WithChain(
113138
cron.Recover(logger),
@@ -186,6 +211,11 @@ c := cron.New(cron.WithClock(fakeClock))
186211

187212
See `clock.go` for the interface definition.
188213

214+
## Option defaults
215+
216+
Passing `nil` to `WithLocation`, `WithParser`, `WithLogger`, or `WithClock`
217+
keeps the package default instead of leaving the scheduler in an invalid state.
218+
189219
## Migration from robfig/cron/v3
190220

191221
See [MIGRATION.md](MIGRATION.md) for a step-by-step upgrade guide.

0 commit comments

Comments
 (0)