Skip to content

Commit d45e189

Browse files
committed
docs: add comprehensive migration guide from robfig/cron
Create docs/MIGRATION.md with detailed migration documentation: - Quick start section with import path and go.mod changes - Go version requirements comparison - Behavioral differences with before/after examples: - TZ= parsing panic fixes (robfig#554, robfig#555) - Entry.Run() chain behavior fix (robfig#551) - DST spring-forward handling (robfig#541) - Step range validation (robfig#543) - Input length limits - Type changes (EntryID: int -> uint64) - New features overview (FakeClock, StopAndWait, Timeout, heap scheduling) - Migration checklist - Testing recommendations with code examples - Troubleshooting section Also update README.md to link to the comprehensive migration guide. Closes #73
1 parent 31a88ed commit d45e189

2 files changed

Lines changed: 362 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import cron "github.com/netresearch/go-cron"
4545

4646
The API is 100% compatible with robfig/cron v3.
4747

48+
> [!TIP]
49+
> See [docs/MIGRATION.md](docs/MIGRATION.md) for a comprehensive migration guide including behavioral differences, type changes, and troubleshooting.
50+
4851
## Quick Start
4952

5053
```go

docs/MIGRATION.md

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
# Migrating from robfig/cron
2+
3+
This guide helps you migrate from [robfig/cron](https://github.com/robfig/cron) v3 to [netresearch/go-cron](https://github.com/netresearch/go-cron).
4+
5+
## Quick Start
6+
7+
For most users, migration requires only two changes:
8+
9+
### 1. Update import path
10+
11+
```go
12+
// Before
13+
import "github.com/robfig/cron/v3"
14+
15+
// After
16+
import cron "github.com/netresearch/go-cron"
17+
```
18+
19+
### 2. Update go.mod
20+
21+
```bash
22+
go get github.com/netresearch/go-cron@latest
23+
```
24+
25+
The API is 100% compatible with robfig/cron v3 — no code changes required for typical use cases.
26+
27+
## Go Version Requirements
28+
29+
| Library | Minimum Go Version |
30+
|---------|-------------------|
31+
| robfig/cron v3 | Go 1.13 |
32+
| netresearch/go-cron | Go 1.25 |
33+
34+
## Behavioral Differences
35+
36+
While the API is compatible, there are intentional behavioral changes that fix bugs or improve reliability. These changes may affect your application if you were (knowingly or unknowingly) depending on the original behavior.
37+
38+
### Bug Fixes That Change Behavior
39+
40+
| Issue | robfig/cron v3 | netresearch/go-cron |
41+
|-------|----------------|---------------------|
42+
| **TZ= parsing panics** | Crashes on empty or malformed timezone | Returns descriptive error |
43+
| **Entry.Run() bypasses chains** | `entry.Run()` calls job directly | `entry.Run()` honors chain wrappers |
44+
| **DST spring-forward** | Jobs silently skipped | Jobs run immediately after transition (ISC behavior) |
45+
| **NewParser with no fields** | Panics | Returns error |
46+
47+
#### TZ= Panic Fixes (#554, #555)
48+
49+
**Before (robfig/cron):**
50+
```go
51+
// These would panic:
52+
c.AddFunc("TZ= 0 6 * * *", myFunc) // Empty timezone
53+
c.AddFunc("CRON_TZ=Invalid/Zone", myFunc) // Invalid timezone
54+
c.AddFunc("TZ=America/New_York", myFunc) // Timezone only, no schedule
55+
```
56+
57+
**After (netresearch/go-cron):**
58+
```go
59+
// These return errors instead of panicking:
60+
_, err := c.AddFunc("TZ= 0 6 * * *", myFunc)
61+
// err: "empty time zone specification"
62+
63+
_, err := c.AddFunc("CRON_TZ=Invalid/Zone 0 6 * * *", myFunc)
64+
// err: "unknown time zone Invalid/Zone"
65+
66+
_, err := c.AddFunc("TZ=America/New_York", myFunc)
67+
// err: "empty schedule specification"
68+
```
69+
70+
#### Entry.Run() Chain Behavior (#551)
71+
72+
**Before (robfig/cron):**
73+
```go
74+
c := cron.New(cron.WithChain(
75+
cron.SkipIfStillRunning(logger),
76+
cron.Recover(logger),
77+
))
78+
id, _ := c.AddFunc("* * * * *", myFunc)
79+
80+
entry := c.Entry(id)
81+
entry.Job.Run() // Bypasses chain — no skip check, no panic recovery!
82+
```
83+
84+
**After (netresearch/go-cron):**
85+
```go
86+
entry := c.Entry(id)
87+
entry.Run() // Honors chain wrappers — skip check and panic recovery applied
88+
```
89+
90+
Use `entry.Run()` instead of `entry.Job.Run()` to ensure chain decorators are respected.
91+
92+
#### DST Spring-Forward Handling (#541)
93+
94+
**Before (robfig/cron):**
95+
```
96+
Schedule: "0 30 2 * * *" (2:30 AM)
97+
DST transition: 2:00 AM → 3:00 AM
98+
Result: Job SKIPPED (time 2:30 never exists)
99+
```
100+
101+
**After (netresearch/go-cron):**
102+
```
103+
Schedule: "0 30 2 * * *" (2:30 AM)
104+
DST transition: 2:00 AM → 3:00 AM
105+
Result: Job runs at 3:00 AM (immediately after transition)
106+
```
107+
108+
This follows ISC cron behavior used by most Unix systems.
109+
110+
### Validation Improvements
111+
112+
These changes make the library stricter about invalid input:
113+
114+
#### Step Range Validation (#543)
115+
116+
**Before (robfig/cron):**
117+
```go
118+
// Accepted but semantically incorrect:
119+
c.AddFunc("*/60 * * * *", myFunc) // Step 60 in 0-59 range
120+
```
121+
122+
**After (netresearch/go-cron):**
123+
```go
124+
_, err := c.AddFunc("*/60 * * * *", myFunc)
125+
// err: "step (60) must be less than range size (60)"
126+
```
127+
128+
#### Minimum @every Duration
129+
130+
**Before (robfig/cron):**
131+
```go
132+
c.AddFunc("@every 100ms", myFunc) // Allowed, but could overwhelm system
133+
```
134+
135+
**After (netresearch/go-cron):**
136+
```go
137+
_, err := c.AddFunc("@every 100ms", myFunc)
138+
// err: "@every duration must be at least 1 second"
139+
```
140+
141+
#### Input Length Limits
142+
143+
**Before (robfig/cron):**
144+
```go
145+
// No limit on spec length — potential DoS vector
146+
c.AddFunc(veryLongString, myFunc)
147+
```
148+
149+
**After (netresearch/go-cron):**
150+
```go
151+
// Specs limited to 1024 characters
152+
_, err := c.AddFunc(veryLongString, myFunc)
153+
// err: "spec exceeds maximum length of 1024 characters"
154+
```
155+
156+
## Type Changes
157+
158+
### EntryID: int → uint64
159+
160+
The `EntryID` type changed from `int` to `uint64` for larger capacity and overflow safety.
161+
162+
**Before (robfig/cron):**
163+
```go
164+
var id int = int(c.Schedule(schedule, job))
165+
```
166+
167+
**After (netresearch/go-cron):**
168+
```go
169+
// Option 1: Use the type directly (recommended)
170+
var id cron.EntryID = c.Schedule(schedule, job)
171+
172+
// Option 2: Use uint64
173+
var id uint64 = uint64(c.Schedule(schedule, job))
174+
```
175+
176+
**Impact:** If you're storing EntryID in an `int` variable, update to `cron.EntryID` or `uint64`.
177+
178+
## New Features
179+
180+
These features are additions that don't affect existing code:
181+
182+
### Deterministic Testing with FakeClock
183+
184+
```go
185+
import "github.com/netresearch/go-cron"
186+
187+
// Create a fake clock for testing
188+
clock := cron.NewFakeClock(time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC))
189+
c := cron.New(cron.WithClock(clock))
190+
191+
c.AddFunc("0 * * * *", myFunc)
192+
c.Start()
193+
194+
// Advance time in tests
195+
clock.Advance(time.Hour)
196+
// Job executes at 1:00
197+
```
198+
199+
### StopAndWait() Convenience Method
200+
201+
```go
202+
// Before: Manual wait pattern
203+
ctx := c.Stop()
204+
<-ctx.Done()
205+
206+
// After: Convenience method
207+
c.StopAndWait()
208+
```
209+
210+
### Timeout Wrapper
211+
212+
```go
213+
c := cron.New(cron.WithChain(
214+
cron.Timeout(logger, 30*time.Second),
215+
cron.Recover(logger),
216+
))
217+
```
218+
219+
**Note:** The Timeout wrapper uses an "abandonment model" — the wrapper returns after timeout, but the job goroutine continues running. See [doc.go](https://pkg.go.dev/github.com/netresearch/go-cron#hdr-Timeout_Wrapper_Caveats) for details.
220+
221+
### Heap-Based Scheduling (Performance)
222+
223+
The scheduler now uses a min-heap instead of sorted slice:
224+
225+
| Operation | robfig/cron | netresearch/go-cron |
226+
|-----------|-------------|---------------------|
227+
| Insert entry | O(n log n) | O(log n) |
228+
| Remove entry | O(n) | O(log n) |
229+
| Get next entry | O(1) | O(1) |
230+
231+
No code changes required — this is an internal optimization.
232+
233+
### slog Adapter
234+
235+
```go
236+
import "log/slog"
237+
238+
c := cron.New(cron.WithLogger(
239+
cron.SlogLogger(slog.Default()),
240+
))
241+
```
242+
243+
## Migration Checklist
244+
245+
- [ ] Update import path to `github.com/netresearch/go-cron`
246+
- [ ] Update go.mod with `go get github.com/netresearch/go-cron@latest`
247+
- [ ] Verify Go version is 1.25+
248+
- [ ] Review timezone handling for empty/invalid timezone cases
249+
- [ ] Update `entry.Job.Run()` calls to `entry.Run()` if chain behavior is expected
250+
- [ ] Review cron expressions for step validation (`*/60` style patterns)
251+
- [ ] Update any code storing `EntryID` as `int` to use `cron.EntryID`
252+
- [ ] Test DST transitions if your application runs DST-sensitive schedules
253+
- [ ] Run existing tests to verify compatibility
254+
255+
## Testing Your Migration
256+
257+
### 1. Run Existing Tests
258+
259+
```bash
260+
go test ./...
261+
```
262+
263+
### 2. Verify Timezone Handling
264+
265+
If you use timezone features, test edge cases:
266+
267+
```go
268+
func TestTimezoneEdgeCases(t *testing.T) {
269+
c := cron.New()
270+
271+
// These should return errors, not panic
272+
_, err := c.AddFunc("TZ= 0 6 * * *", func() {})
273+
if err == nil {
274+
t.Error("expected error for empty timezone")
275+
}
276+
277+
_, err = c.AddFunc("CRON_TZ=Invalid/Zone 0 6 * * *", func() {})
278+
if err == nil {
279+
t.Error("expected error for invalid timezone")
280+
}
281+
}
282+
```
283+
284+
### 3. Test DST Transitions
285+
286+
If DST behavior matters to your application:
287+
288+
```go
289+
func TestDSTBehavior(t *testing.T) {
290+
loc, _ := time.LoadLocation("America/New_York")
291+
292+
// Create schedule for 2:30 AM
293+
schedule, _ := cron.ParseStandard("30 2 * * *")
294+
295+
// Time just before spring-forward transition
296+
before := time.Date(2024, 3, 10, 1, 59, 0, 0, loc)
297+
298+
next := schedule.Next(before)
299+
300+
// Should be 3:00 AM (immediately after transition), not next day
301+
if next.Hour() != 3 || next.Day() != 10 {
302+
t.Errorf("expected 3:00 AM same day, got %v", next)
303+
}
304+
}
305+
```
306+
307+
### 4. Benchmark Performance (Optional)
308+
309+
If performance is critical:
310+
311+
```bash
312+
go test -bench=. -benchmem
313+
```
314+
315+
## Troubleshooting
316+
317+
### "unknown time zone" errors
318+
319+
Ensure timezone names are valid IANA timezone identifiers:
320+
321+
```go
322+
// Wrong
323+
c.AddFunc("TZ=EST 0 6 * * *", myFunc)
324+
325+
// Correct
326+
c.AddFunc("TZ=America/New_York 0 6 * * *", myFunc)
327+
```
328+
329+
### Step validation errors
330+
331+
Review expressions using `/` step syntax:
332+
333+
```go
334+
// Invalid: step equals or exceeds range
335+
"*/60 * * * *" // Error: 60 >= 60
336+
"0-10/15 * * * *" // Error: 15 > 10
337+
338+
// Valid alternatives
339+
"* * * * *" // Every minute
340+
"0-10/5 * * * *" // Every 5 minutes within 0-10
341+
```
342+
343+
### Jobs running at unexpected times during DST
344+
345+
The new ISC-compatible behavior runs skipped-hour jobs immediately. If you prefer the old behavior (skip the job), schedule outside DST transition hours:
346+
347+
```go
348+
// Avoid 1-3 AM for DST-sensitive jobs
349+
c.AddFunc("0 4 * * *", myFunc) // 4:00 AM — safe
350+
351+
// Or use UTC
352+
c.AddFunc("CRON_TZ=UTC 0 6 * * *", myFunc)
353+
```
354+
355+
## Getting Help
356+
357+
- [GitHub Issues](https://github.com/netresearch/go-cron/issues)
358+
- [API Reference](https://pkg.go.dev/github.com/netresearch/go-cron)
359+
- [CHANGELOG](../CHANGELOG.md)

0 commit comments

Comments
 (0)