From c2757f68a640b082092ca51a862578b2f48715ee Mon Sep 17 00:00:00 2001 From: MaysWind Date: Tue, 13 Aug 2024 00:13:24 +0800 Subject: [PATCH] code refactor and add unit tests --- cmd/cron_jobs.go | 2 +- pkg/cron/cron_container.go | 2 +- pkg/cron/cron_container_test.go | 138 +++++++++++++++++++++++++++++++ pkg/cron/cron_job.go | 34 ++++---- pkg/cron/cron_job_period.go | 63 +++++++++++++++ pkg/cron/cron_job_test.go | 139 ++++++++++++++++++++++++++++++++ pkg/cron/cron_jobs.go | 7 +- 7 files changed, 364 insertions(+), 21 deletions(-) create mode 100644 pkg/cron/cron_container_test.go create mode 100644 pkg/cron/cron_job_period.go create mode 100644 pkg/cron/cron_job_test.go diff --git a/cmd/cron_jobs.go b/cmd/cron_jobs.go index e5dc1fcc..19a4d1fe 100644 --- a/cmd/cron_jobs.go +++ b/cmd/cron_jobs.go @@ -65,7 +65,7 @@ func listAllCronJobs(c *cli.Context) error { fmt.Printf("[Name] %s\n", cronJob.Name) fmt.Printf("[Description] %s\n", cronJob.Description) - fmt.Printf("[Interval] Every %s\n", cronJob.Interval) + fmt.Printf("[Interval] Every %s\n", cronJob.Period.GetInterval()) } return nil diff --git a/pkg/cron/cron_container.go b/pkg/cron/cron_container.go index df29dfc9..599e334c 100644 --- a/pkg/cron/cron_container.go +++ b/pkg/cron/cron_container.go @@ -83,7 +83,7 @@ func (c *CronJobSchedulerContainer) registerAllJobs(config *settings.Config) { func (c *CronJobSchedulerContainer) registerIntervalJob(job *CronJob) { gocronJob, err := c.scheduler.NewJob( - gocron.DurationJob(job.Interval), + job.Period.ToJobDefinition(), gocron.NewTask(job.doRun), gocron.WithName(job.Name), gocron.WithSingletonMode(gocron.LimitModeReschedule), diff --git a/pkg/cron/cron_container_test.go b/pkg/cron/cron_container_test.go new file mode 100644 index 00000000..ef98bbfe --- /dev/null +++ b/pkg/cron/cron_container_test.go @@ -0,0 +1,138 @@ +package cron + +import ( + "sync/atomic" + "testing" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" + + "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" + "github.com/mayswind/ezbookkeeping/pkg/settings" +) + +func TestCronJobSchedulerContainerRegisterIntervalJob(t *testing.T) { + var err error + + container := &CronJobSchedulerContainer{ + allJobsMap: make(map[string]*CronJob), + allGocronJobsMap: make(map[string]gocron.Job), + } + + container.scheduler, err = gocron.NewScheduler( + gocron.WithLocation(time.Local), + gocron.WithLogger(NewGocronLoggerAdapter()), + ) + assert.Nil(t, err) + + actualValue := false + job := &CronJob{ + Name: "TestRegisterIntervalJob", + Description: "The test cron job", + Period: CronJobIntervalPeriod{ + Interval: 1 * time.Second, + }, + Run: func() error { + actualValue = true + return nil + }, + } + + container.registerIntervalJob(job) + container.scheduler.Start() + + assert.Equal(t, 1, len(container.GetAllJobs())) + assert.Equal(t, job, container.GetAllJobs()[0]) + + time.Sleep(2 * time.Second) + assert.True(t, actualValue) + + err = container.scheduler.Shutdown() + assert.Nil(t, err) +} + +func TestCronJobSchedulerContainerSyncRunJobNow(t *testing.T) { + var err error + + container := &CronJobSchedulerContainer{ + allJobsMap: make(map[string]*CronJob), + allGocronJobsMap: make(map[string]gocron.Job), + } + + container.scheduler, err = gocron.NewScheduler( + gocron.WithLocation(time.Local), + gocron.WithLogger(NewGocronLoggerAdapter()), + ) + assert.Nil(t, err) + + actualValue := false + job := &CronJob{ + Name: "TestSyncRunJob", + Description: "The test cron job", + Period: CronJobIntervalPeriod{ + Interval: 24 * time.Hour, + }, + Run: func() error { + actualValue = true + return nil + }, + } + + container.registerIntervalJob(job) + + err = container.SyncRunJobNow("TestSyncRunJob") + assert.Nil(t, err) + assert.True(t, actualValue) +} + +func TestCronJobSchedulerContainerRepeatRun(t *testing.T) { + var err error + + checker, _ := duplicatechecker.NewInMemoryDuplicateChecker(&settings.Config{ + DuplicateSubmissionsIntervalDuration: 60 * time.Second, + InMemoryDuplicateCheckerCleanupIntervalDuration: 60 * time.Second, + }) + + duplicatechecker.Container.Current = checker + + container := &CronJobSchedulerContainer{ + allJobsMap: make(map[string]*CronJob), + allGocronJobsMap: make(map[string]gocron.Job), + } + + container.scheduler, err = gocron.NewScheduler( + gocron.WithLocation(time.Local), + gocron.WithLogger(NewGocronLoggerAdapter()), + ) + assert.Nil(t, err) + + var runCount atomic.Uint32 + runTime := time.Now().Add(time.Second) + job := &CronJob{ + Name: "TestRepeatRunJob", + Description: "The test cron job", + Period: CronJobFixedTimePeriod{ + Time: runTime, + }, + Run: func() error { + runCount.Add(1) + return nil + }, + } + + container.registerIntervalJob(job) + container.registerIntervalJob(job) + container.registerIntervalJob(job) + container.registerIntervalJob(job) + container.registerIntervalJob(job) + container.scheduler.Start() + + time.Sleep(10 * time.Second) + + assert.Nil(t, err) + assert.Equal(t, uint32(1), runCount.Load()) + + err = container.scheduler.Shutdown() + assert.Nil(t, err) +} diff --git a/pkg/cron/cron_job.go b/pkg/cron/cron_job.go index 8c0bc460..ed33fc06 100644 --- a/pkg/cron/cron_job.go +++ b/pkg/cron/cron_job.go @@ -9,33 +9,35 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/utils" ) +// CronJob represents the cron job instance type CronJob struct { Name string Description string - Interval time.Duration + Period CronJobPeriod Run func() error } func (c *CronJob) doRun() { start := time.Now() - localAddr, err := utils.GetLocalIPAddressesString() - if err != nil { - log.Warnf("[cron_job.doRun] job \"%s\" cannot get local ipv4 address, because %s", c.Name, err.Error()) - return + if duplicatechecker.Container.Current != nil { + localAddr, err := utils.GetLocalIPAddressesString() + + if err != nil { + log.Warnf("[cron_job.doRun] job \"%s\" cannot get local ipv4 address, because %s", c.Name, err.Error()) + return + } + + currentInfo := fmt.Sprintf("ip: %s, startTime: %d", localAddr, time.Now().Unix()) + found, runningInfo := duplicatechecker.Container.GetOrSetCronJobRunningInfo(c.Name, currentInfo, c.Period.GetInterval()) + + if found { + log.Warnf("[cron_job.doRun] job \"%s\" is already running (%s)", c.Name, runningInfo) + return + } } - currentInfo := fmt.Sprintf("ip: %s, startTime: %d", localAddr, time.Now().Unix()) - found, runningInfo := duplicatechecker.Container.GetOrSetCronJobRunningInfo(c.Name, currentInfo, c.Interval) - - if found { - log.Warnf("[cron_job.doRun] job \"%s\" is already running (%s)", c.Name, runningInfo) - return - } - - err = c.Run() - - duplicatechecker.Container.Current.RemoveCronJobRunningInfo(c.Name) + err := c.Run() now := time.Now() diff --git a/pkg/cron/cron_job_period.go b/pkg/cron/cron_job_period.go new file mode 100644 index 00000000..fa67be8a --- /dev/null +++ b/pkg/cron/cron_job_period.go @@ -0,0 +1,63 @@ +package cron + +import ( + "time" + + "github.com/go-co-op/gocron/v2" +) + +// CronJobPeriod represents the cron job period +type CronJobPeriod interface { + GetInterval() time.Duration + ToJobDefinition() gocron.JobDefinition +} + +// CronJobIntervalPeriod represents the period of execution at intervals +type CronJobIntervalPeriod struct { + Interval time.Duration +} + +// CronJobFixedHourPeriod represents the period of execution at fixed hour +type CronJobFixedHourPeriod struct { + Hour uint32 +} + +// CronJobFixedTimePeriod represents the period of execution at fixed time +type CronJobFixedTimePeriod struct { + Time time.Time +} + +// GetInterval returns the interval time of the period of CronJobIntervalPeriod +func (p CronJobIntervalPeriod) GetInterval() time.Duration { + return p.Interval +} + +// ToJobDefinition returns the gocron job definition of the period of CronJobIntervalPeriod +func (p CronJobIntervalPeriod) ToJobDefinition() gocron.JobDefinition { + return gocron.DurationJob(p.Interval) +} + +// GetInterval returns the interval time of the period of CronJobFixedHourPeriod +func (p CronJobFixedHourPeriod) GetInterval() time.Duration { + return 24 * time.Hour +} + +// ToJobDefinition returns the gocron job definition of the period of CronJobFixedHourPeriod +func (p CronJobFixedHourPeriod) ToJobDefinition() gocron.JobDefinition { + return gocron.DailyJob( + 1, + gocron.NewAtTimes( + gocron.NewAtTime(uint(p.Hour), 0, 0), + ), + ) +} + +// GetInterval returns the interval time of the period of CronJobFixedTimePeriod +func (p CronJobFixedTimePeriod) GetInterval() time.Duration { + return 0 +} + +// ToJobDefinition returns the gocron job definition of the period of CronJobFixedTimePeriod +func (p CronJobFixedTimePeriod) ToJobDefinition() gocron.JobDefinition { + return gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(p.Time)) +} diff --git a/pkg/cron/cron_job_test.go b/pkg/cron/cron_job_test.go new file mode 100644 index 00000000..0ce411fd --- /dev/null +++ b/pkg/cron/cron_job_test.go @@ -0,0 +1,139 @@ +package cron + +import ( + "testing" + "time" + + "github.com/go-co-op/gocron/v2" + "github.com/stretchr/testify/assert" +) + +func TestCronJobNextRunTimeWithIntervalPeriod(t *testing.T) { + scheduler, err := gocron.NewScheduler( + gocron.WithLocation(time.Local), + ) + assert.Nil(t, err) + + job := CronJob{ + Name: "TestCronJobWithIntervalPeriod", + Description: "The test cron job", + Period: CronJobIntervalPeriod{ + Interval: 2*time.Hour + 34*time.Minute + 56*time.Second, + }, + Run: func() error { + return nil + }, + } + + gocronJob, err := scheduler.NewJob( + job.Period.ToJobDefinition(), + gocron.NewTask(job.doRun), + gocron.WithName(job.Name), + gocron.WithSingletonMode(gocron.LimitModeReschedule), + ) + assert.Nil(t, err) + + scheduler.Start() + + currentTime := time.Now() + nextRunTime, err := gocronJob.NextRun() + assert.Nil(t, err) + + expectedNextTime := currentTime.Add(2 * time.Hour).Add(34 * time.Minute).Add(56 * time.Second) + + assert.Equal(t, expectedNextTime.Year(), nextRunTime.Year()) + assert.Equal(t, expectedNextTime.Month(), nextRunTime.Month()) + assert.Equal(t, expectedNextTime.Day(), nextRunTime.Day()) + assert.Equal(t, expectedNextTime.Hour(), nextRunTime.Hour()) + assert.Equal(t, expectedNextTime.Minute(), nextRunTime.Minute()) + assert.Equal(t, expectedNextTime.Second(), nextRunTime.Second()) + + err = scheduler.Shutdown() + assert.Nil(t, err) +} + +func TestCronJobNextRunTimeWithFixedHourPeriod(t *testing.T) { + scheduler, err := gocron.NewScheduler( + gocron.WithLocation(time.Local), + ) + assert.Nil(t, err) + + job := CronJob{ + Name: "TestCronJobWithFixedHourPeriod", + Description: "The test cron job", + Period: CronJobFixedHourPeriod{ + Hour: 0, + }, + Run: func() error { + return nil + }, + } + + gocronJob, err := scheduler.NewJob( + job.Period.ToJobDefinition(), + gocron.NewTask(job.doRun), + gocron.WithName(job.Name), + gocron.WithSingletonMode(gocron.LimitModeReschedule), + ) + assert.Nil(t, err) + + scheduler.Start() + + nextRunTime, err := gocronJob.NextRun() + assert.Nil(t, err) + + tomorrow := time.Now().AddDate(0, 0, 1) + + assert.Equal(t, tomorrow.Year(), nextRunTime.Year()) + assert.Equal(t, tomorrow.Month(), nextRunTime.Month()) + assert.Equal(t, tomorrow.Day(), nextRunTime.Day()) + assert.Equal(t, 0, nextRunTime.Hour()) + assert.Equal(t, 0, nextRunTime.Minute()) + assert.Equal(t, 0, nextRunTime.Second()) + + err = scheduler.Shutdown() + assert.Nil(t, err) +} + +func TestCronJobNextRunTimeWithFixedTimePeriod(t *testing.T) { + scheduler, err := gocron.NewScheduler( + gocron.WithLocation(time.Local), + ) + assert.Nil(t, err) + + expectedTime := time.Now().Add(123456789 * time.Second) + + job := CronJob{ + Name: "TestCronJobWithFixedTimePeriod", + Description: "The test cron job", + Period: CronJobFixedTimePeriod{ + Time: expectedTime, + }, + Run: func() error { + return nil + }, + } + + gocronJob, err := scheduler.NewJob( + job.Period.ToJobDefinition(), + gocron.NewTask(job.doRun), + gocron.WithName(job.Name), + gocron.WithSingletonMode(gocron.LimitModeReschedule), + ) + assert.Nil(t, err) + + scheduler.Start() + + nextRunTime, err := gocronJob.NextRun() + assert.Nil(t, err) + + assert.Equal(t, expectedTime.Year(), nextRunTime.Year()) + assert.Equal(t, expectedTime.Month(), nextRunTime.Month()) + assert.Equal(t, expectedTime.Day(), nextRunTime.Day()) + assert.Equal(t, expectedTime.Hour(), nextRunTime.Hour()) + assert.Equal(t, expectedTime.Minute(), nextRunTime.Minute()) + assert.Equal(t, expectedTime.Second(), nextRunTime.Second()) + + err = scheduler.Shutdown() + assert.Nil(t, err) +} diff --git a/pkg/cron/cron_jobs.go b/pkg/cron/cron_jobs.go index cc510883..ce41cf02 100644 --- a/pkg/cron/cron_jobs.go +++ b/pkg/cron/cron_jobs.go @@ -1,15 +1,16 @@ package cron import ( - "time" - "github.com/mayswind/ezbookkeeping/pkg/services" ) +// RemoveExpiredTokensJob represents the cron job which periodically remove expired user tokens from the database var RemoveExpiredTokensJob = &CronJob{ Name: "RemoveExpiredTokens", Description: "Periodically remove expired user tokens from the database.", - Interval: 24 * time.Hour, + Period: CronJobFixedHourPeriod{ + Hour: 0, + }, Run: func() error { return services.Tokens.DeleteAllExpiredTokens(nil) },