From 0f11912c145608b82d32ace28cec4b914c39985b Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Tue, 7 Jan 2025 19:08:02 +0530 Subject: [PATCH 1/9] added supoort for cron-tz --- pkg/gofr/cron.go | 15 ++++++++++++++- pkg/gofr/cron_test.go | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/gofr/cron.go b/pkg/gofr/cron.go index c849b9792..f35953d19 100644 --- a/pkg/gofr/cron.go +++ b/pkg/gofr/cron.go @@ -34,6 +34,7 @@ type CronFunc func(ctx *Context) type Crontab struct { // contains unexported fields ticker *time.Ticker + location *time.Location jobs []*job container *container.Container @@ -62,15 +63,21 @@ type tick struct { } // NewCron initializes and returns new cron tab. -func NewCron(cntnr *container.Container) *Crontab { +func NewCron(cntnr *container.Container, options ...func(*Crontab)) *Crontab { c := &Crontab{ ticker: time.NewTicker(time.Second), + location: time.Local, container: cntnr, jobs: make([]*job, 0), } + for _, option := range options { + option(c) + } + go func() { for t := range c.ticker.C { + t = t.In(c.location) c.runScheduled(t) } }() @@ -78,6 +85,12 @@ func NewCron(cntnr *container.Container) *Crontab { return c } +func WithTimezone(location *time.Location) func(*Crontab) { + return func(c *Crontab) { + c.location = location + } +} + // this will compile the regex once instead of compiling it each time when it is being called. var ( matchSpaces = regexp.MustCompile(`\s+`) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index e0e237287..1c53ed352 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -606,3 +606,25 @@ func TestCron_parsePart(t *testing.T) { }) } } + +func TestCron_WithTimezone(t *testing.T) { + tests := []struct { + name string + option func(*Crontab) + expected *time.Location + }{ + { + name: "Factory with timezone option", + option: WithTimezone(time.UTC), + expected: time.UTC, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := NewCron(nil, test.option) + got.ticker.Stop() + assert.Equal(t, test.expected, got.location) + }) + } +} From 531f44ef1ed2d5fd79862ceb1a87c6d4215b8183 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Tue, 7 Jan 2025 19:16:14 +0530 Subject: [PATCH 2/9] reduced lines of code --- pkg/gofr/cron.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/pkg/gofr/cron.go b/pkg/gofr/cron.go index f35953d19..754dc89d1 100644 --- a/pkg/gofr/cron.go +++ b/pkg/gofr/cron.go @@ -42,12 +42,7 @@ type Crontab struct { } type job struct { - sec map[int]struct{} - min map[int]struct{} - hour map[int]struct{} - day map[int]struct{} - month map[int]struct{} - dayOfWeek map[int]struct{} + sec, min, hour, day, month, dayOfWeek map[int]struct{} name string fn CronFunc @@ -86,9 +81,7 @@ func NewCron(cntnr *container.Container, options ...func(*Crontab)) *Crontab { } func WithTimezone(location *time.Location) func(*Crontab) { - return func(c *Crontab) { - c.location = location - } + return func(c *Crontab) { c.location = location } } // this will compile the regex once instead of compiling it each time when it is being called. From 9c09651af9fedb664d1c3c4dc1568c92fa63e2df Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Sun, 12 Jan 2025 17:35:49 +0530 Subject: [PATCH 3/9] added documentation and updated AddCronJob method to accept crontab options --- docs/advanced-guide/using-cron/page.md | 29 ++++++++++++++++++++++++++ pkg/gofr/cron.go | 1 + pkg/gofr/gofr.go | 4 ++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/advanced-guide/using-cron/page.md b/docs/advanced-guide/using-cron/page.md index 8eaec44c3..e0290ae9c 100644 --- a/docs/advanced-guide/using-cron/page.md +++ b/docs/advanced-guide/using-cron/page.md @@ -74,4 +74,33 @@ func main() { } ``` +Gofr also allows to specify timezone against which the schedule will be evaluated. + +### Example + +```go +package main + +import ( + "time" + + "gofr.dev/pkg/gofr" +) + +func main() { + app := gofr.New() + + // tzdata will be taking care of DST + estTimezone, _ := time.LoadLocation("America/New_York") + + // Run the cron job every day at 7AM EST + app.AddCronJob("0 7 * * *", "", func(ctx *gofr.Context) { + ctx.Logger.Infof("current time is %v", time.Now()) + }, gofr.WithTimezone(estTimezone)) + + app.Run() +} +``` + + > #### Check out the example on how to add cron jobs in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-cron-jobs/main.go) diff --git a/pkg/gofr/cron.go b/pkg/gofr/cron.go index 754dc89d1..67cd4f5df 100644 --- a/pkg/gofr/cron.go +++ b/pkg/gofr/cron.go @@ -80,6 +80,7 @@ func NewCron(cntnr *container.Container, options ...func(*Crontab)) *Crontab { return c } +// WithTimezone is a functional option for NewCron to specify timezone against which the cron schedule will be evaluated, func WithTimezone(location *time.Location) func(*Crontab) { return func(c *Crontab) { c.location = location } } diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 43c653d16..091021859 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -691,9 +691,9 @@ func (a *App) UseMiddlewareWithContainer(middlewareHandler func(c *container.Con // AddCronJob registers a cron job to the cron table. // The cron expression can be either a 5-part or 6-part format. The 6-part format includes an // optional second field (in beginning) and others being minute, hour, day, month and day of week respectively. -func (a *App) AddCronJob(schedule, jobName string, job CronFunc) { +func (a *App) AddCronJob(schedule, jobName string, job CronFunc, options ...func(*Crontab)) { if a.cron == nil { - a.cron = NewCron(a.container) + a.cron = NewCron(a.container, options...) } if err := a.cron.AddJob(schedule, jobName, job); err != nil { From eff6b9da639326525c2deb776ed4a377360b3682 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Mon, 13 Jan 2025 02:45:37 +0530 Subject: [PATCH 4/9] added suggested test cases --- pkg/gofr/cron_test.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index 1c53ed352..73d12fc09 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -608,21 +608,32 @@ func TestCron_parsePart(t *testing.T) { } func TestCron_WithTimezone(t *testing.T) { + est, _ := time.LoadLocation("America/New_York") + tests := []struct { name string - option func(*Crontab) + options []func(*Crontab) expected *time.Location }{ { - name: "Factory with timezone option", - option: WithTimezone(time.UTC), + name: "Factory without options", + expected: time.Local, + }, + { + name: "Factory with UTC timezone option", + options: []func(crontab *Crontab){WithTimezone(time.UTC)}, expected: time.UTC, }, + { + name: "Factory with EST/EDT timezone option", + options: []func(crontab *Crontab){WithTimezone(est)}, + expected: est, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := NewCron(nil, test.option) + got := NewCron(nil, test.options...) got.ticker.Stop() assert.Equal(t, test.expected, got.location) }) From 6a6fef2e1070c109f059e880d44b48fd0cfde270 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Mon, 13 Jan 2025 14:06:45 +0530 Subject: [PATCH 5/9] resolved review comment --- pkg/gofr/cron_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index 73d12fc09..dcaa7d617 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -608,7 +608,8 @@ func TestCron_parsePart(t *testing.T) { } func TestCron_WithTimezone(t *testing.T) { - est, _ := time.LoadLocation("America/New_York") + est, err := time.LoadLocation("America/New_York") + require.NoError(t, err, "computing prerequisite timezone") tests := []struct { name string From ba81c115e63065e903283d3ea64e2ecedcdef4a9 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Mon, 13 Jan 2025 16:37:20 +0530 Subject: [PATCH 6/9] fixed lint err --- pkg/gofr/cron.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/cron.go b/pkg/gofr/cron.go index 67cd4f5df..fc8a8bf56 100644 --- a/pkg/gofr/cron.go +++ b/pkg/gofr/cron.go @@ -80,7 +80,7 @@ func NewCron(cntnr *container.Container, options ...func(*Crontab)) *Crontab { return c } -// WithTimezone is a functional option for NewCron to specify timezone against which the cron schedule will be evaluated, +// WithTimezone is a functional option for NewCron to specify timezone against which the cron schedule will be evaluated. func WithTimezone(location *time.Location) func(*Crontab) { return func(c *Crontab) { c.location = location } } From d9e746f72792786f069241bec5e51e0067505bb6 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Thu, 16 Jan 2025 00:28:32 +0530 Subject: [PATCH 7/9] added newCronT for ticker cleanup --- pkg/gofr/cron_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index dcaa7d617..ed5269744 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr/container" "gofr.dev/pkg/gofr/testutil" ) @@ -203,7 +204,7 @@ func TestCronTab_AddJob(t *testing.T) { }, } - c := NewCron(nil) + c := newCronT(t, nil) for _, tc := range testCases { err := c.AddJob(tc.schedule, "test-job", fn) @@ -225,7 +226,7 @@ func TestCronTab_runScheduled(t *testing.T) { // can make container nil as we are not testing the internal working of // dependency function as it is user defined - c := NewCron(nil) + c := newCronT(t, nil) // Populate the job array for cron table c.jobs = []*job{j} @@ -634,9 +635,18 @@ func TestCron_WithTimezone(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := NewCron(nil, test.options...) - got.ticker.Stop() + got := newCronT(t, nil, test.options...) assert.Equal(t, test.expected, got.location) }) } } + +func newCronT(t *testing.T, cntnr *container.Container, options ...func(*Crontab)) *Crontab { + c := NewCron(cntnr, options...) + + t.Cleanup(func() { + c.ticker.Stop() + }) + + return c +} From 745f8438baff2ece6a4a624a63ebd87d392e0987 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Thu, 16 Jan 2025 00:28:32 +0530 Subject: [PATCH 8/9] added newCronT for ticker cleanup Co-authored-by: ccoVeille <3875889+ccoVeille@users.noreply.github.com> --- pkg/gofr/cron_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index dcaa7d617..ed5269744 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gofr.dev/pkg/gofr/container" "gofr.dev/pkg/gofr/testutil" ) @@ -203,7 +204,7 @@ func TestCronTab_AddJob(t *testing.T) { }, } - c := NewCron(nil) + c := newCronT(t, nil) for _, tc := range testCases { err := c.AddJob(tc.schedule, "test-job", fn) @@ -225,7 +226,7 @@ func TestCronTab_runScheduled(t *testing.T) { // can make container nil as we are not testing the internal working of // dependency function as it is user defined - c := NewCron(nil) + c := newCronT(t, nil) // Populate the job array for cron table c.jobs = []*job{j} @@ -634,9 +635,18 @@ func TestCron_WithTimezone(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := NewCron(nil, test.options...) - got.ticker.Stop() + got := newCronT(t, nil, test.options...) assert.Equal(t, test.expected, got.location) }) } } + +func newCronT(t *testing.T, cntnr *container.Container, options ...func(*Crontab)) *Crontab { + c := NewCron(cntnr, options...) + + t.Cleanup(func() { + c.ticker.Stop() + }) + + return c +} From d9741dd8283e42c3eb5d1be0dbbc0c176d7a7639 Mon Sep 17 00:00:00 2001 From: JayGoyal96 Date: Thu, 16 Jan 2025 11:44:17 +0530 Subject: [PATCH 9/9] fixed lint err --- pkg/gofr/cron_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/gofr/cron_test.go b/pkg/gofr/cron_test.go index ed5269744..9a2ddea8d 100644 --- a/pkg/gofr/cron_test.go +++ b/pkg/gofr/cron_test.go @@ -642,6 +642,8 @@ func TestCron_WithTimezone(t *testing.T) { } func newCronT(t *testing.T, cntnr *container.Container, options ...func(*Crontab)) *Crontab { + t.Helper() + c := NewCron(cntnr, options...) t.Cleanup(func() {