diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0008941 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,142 @@ +# gocron: Go Job Scheduling Library + +Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here. + +## Working Effectively + +### Bootstrap and Build Commands +- Install dependencies: `go mod tidy` +- Build the library: `go build -v ./...` +- Install required tools: + - `go install go.uber.org/mock/mockgen@latest` + - `export PATH=$PATH:$(go env GOPATH)/bin` (add to shell profile) +- Generate mocks: `make mocks` +- Format code: `make fmt` + +### Testing Commands +- Run all tests: `make test` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds. +- Run CI tests: `make test_ci` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds. +- Run with coverage: `make test_coverage` -- takes 50 seconds. NEVER CANCEL. Set timeout to 90+ seconds. +- Run specific tests: `go test -v -race -count=1 ./...` + +### Linting Commands +- Format verification: `grep "^func [a-zA-Z]" example_test.go | sort -c` +- Full linting: `make lint` -- MAY FAIL due to golangci-lint config compatibility issues. This is a known issue. +- Alternative basic linting: `go vet ./...` and `gofmt -d .` + +## Validation + +### Required Validation Steps +- ALWAYS run `make test` before submitting changes. Tests must pass. +- ALWAYS run `make fmt` to ensure proper formatting. +- ALWAYS run `make mocks` if you change interface definitions. +- ALWAYS verify examples still work by running them: `cd examples/elector && go run main.go` + +### Manual Testing Scenarios +Since this is a library, not an application, testing involves: +1. **Basic Scheduler Creation**: Verify you can create a scheduler with `gocron.NewScheduler()` +2. **Job Creation**: Verify you can create jobs with various `JobDefinition` types +3. **Scheduler Lifecycle**: Verify Start() and Shutdown() work correctly +4. **Example Validation**: Run examples in `examples/` directory to ensure functionality + +Example validation script: +```go +package main +import ( + "fmt" + "time" + "github.com/go-co-op/gocron/v2" +) +func main() { + s, err := gocron.NewScheduler() + if err != nil { panic(err) } + j, err := s.NewJob( + gocron.DurationJob(2*time.Second), + gocron.NewTask(func() { fmt.Println("Working!") }), + ) + if err != nil { panic(err) } + fmt.Printf("Job ID: %s\n", j.ID()) + s.Start() + time.Sleep(6 * time.Second) + s.Shutdown() +} +``` + +### CI Requirements +The CI will fail if: +- Tests don't pass (`make test_ci`) +- Function order in `example_test.go` is incorrect +- golangci-lint finds issues (though config compatibility varies) + +## Common Tasks + +### Repository Structure +``` +. +├── README.md # Main documentation +├── CONTRIBUTING.md # Contribution guidelines +├── SECURITY.md # Security policy +├── Makefile # Build automation +├── go.mod # Go module definition +├── .github/ # GitHub workflows and configs +├── .golangci.yaml # Linting configuration +├── examples/ # Usage examples +│ └── elector/ # Distributed elector example +├── mocks/ # Generated mock files +├── *.go # Library source files +└── *_test.go # Test files +``` + +### Key Source Files +- `scheduler.go` - Main scheduler implementation +- `job.go` - Job definitions and scheduling logic +- `executor.go` - Job execution engine +- `logger.go` - Logging interfaces and implementations +- `distributed.go` - Distributed scheduling support +- `monitor.go` - Job monitoring interfaces +- `util.go` - Utility functions +- `errors.go` - Error definitions + +### Dependencies and Versions +- Requires Go 1.23.0+ +- Key dependencies automatically managed via `go mod`: + - `github.com/google/uuid` - UUID generation + - `github.com/jonboulle/clockwork` - Time mocking for tests + - `github.com/robfig/cron/v3` - Cron expression parsing + - `github.com/stretchr/testify` - Testing utilities + - `go.uber.org/goleak` - Goroutine leak detection + +### Testing Patterns +- Uses table-driven tests following Go best practices +- Extensive use of goroutine leak detection (may be skipped in CI via TEST_ENV) +- Mock-based testing for interfaces +- Race condition detection enabled (`-race` flag) +- 93.8% test coverage expected + +### Build and Release +- No application to build - this is a library +- Version managed via Git tags (v2.x.x) +- Distribution via Go module system +- CI tests on Go 1.23 and 1.24 + +## Troubleshooting + +### Common Issues +1. **mockgen not found**: Install with `go install go.uber.org/mock/mockgen@latest` +2. **golangci-lint config errors**: Known compatibility issue - use `go vet` instead +3. **Test timeouts**: Tests can take 50+ seconds, always set adequate timeouts +4. **PATH issues**: Ensure `$(go env GOPATH)/bin` is in PATH +5. **Import errors in examples**: Run `go mod tidy` to resolve dependencies + +### Expected Timings +- `make test`: ~50 seconds +- `make test_coverage`: ~50 seconds +- `make test_ci`: ~50 seconds +- `go build`: ~5 seconds +- `make mocks`: ~2 seconds +- `make fmt`: <1 second + +### Known Limitations +- golangci-lint configuration may have compatibility issues with certain versions +- Some tests are skipped in CI environments (controlled by TEST_ENV variable) +- Examples directory has no tests but should be manually validated \ No newline at end of file diff --git a/executor.go b/executor.go index 37fccb4..594d5ea 100644 --- a/executor.go +++ b/executor.go @@ -29,7 +29,7 @@ type executor struct { // sends out jobs once completed jobsOutCompleted chan uuid.UUID // used to request jobs from the scheduler - jobOutRequest chan jobOutRequest + jobOutRequest chan *jobOutRequest // sends out job needs to update the next runs jobUpdateNextRuns chan uuid.UUID diff --git a/job.go b/job.go index 1a73915..4c4e1bd 100644 --- a/job.go +++ b/job.go @@ -1136,7 +1136,7 @@ type job struct { id uuid.UUID name string tags []string - jobOutRequest chan jobOutRequest + jobOutRequest chan *jobOutRequest runJobRequest chan runJobRequest } diff --git a/mocks/distributed.go b/mocks/distributed.go index 357dc2b..92649d3 100644 --- a/mocks/distributed.go +++ b/mocks/distributed.go @@ -5,6 +5,7 @@ // // mockgen -destination=mocks/distributed.go -package=gocronmocks . Elector,Locker,Lock // + // Package gocronmocks is a generated GoMock package. package gocronmocks @@ -12,7 +13,7 @@ import ( context "context" reflect "reflect" - gocron "github.com/go-co-op/gocron/v2" + v2 "github.com/go-co-op/gocron/v2" gomock "go.uber.org/mock/gomock" ) @@ -20,6 +21,7 @@ import ( type MockElector struct { ctrl *gomock.Controller recorder *MockElectorMockRecorder + isgomock struct{} } // MockElectorMockRecorder is the mock recorder for MockElector. @@ -57,6 +59,7 @@ func (mr *MockElectorMockRecorder) IsLeader(arg0 any) *gomock.Call { type MockLocker struct { ctrl *gomock.Controller recorder *MockLockerMockRecorder + isgomock struct{} } // MockLockerMockRecorder is the mock recorder for MockLocker. @@ -77,24 +80,25 @@ func (m *MockLocker) EXPECT() *MockLockerMockRecorder { } // Lock mocks base method. -func (m *MockLocker) Lock(arg0 context.Context, arg1 string) (gocron.Lock, error) { +func (m *MockLocker) Lock(ctx context.Context, key string) (v2.Lock, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Lock", arg0, arg1) - ret0, _ := ret[0].(gocron.Lock) + ret := m.ctrl.Call(m, "Lock", ctx, key) + ret0, _ := ret[0].(v2.Lock) ret1, _ := ret[1].(error) return ret0, ret1 } // Lock indicates an expected call of Lock. -func (mr *MockLockerMockRecorder) Lock(arg0, arg1 any) *gomock.Call { +func (mr *MockLockerMockRecorder) Lock(ctx, key any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockLocker)(nil).Lock), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockLocker)(nil).Lock), ctx, key) } // MockLock is a mock of Lock interface. type MockLock struct { ctrl *gomock.Controller recorder *MockLockMockRecorder + isgomock struct{} } // MockLockMockRecorder is the mock recorder for MockLock. @@ -115,15 +119,15 @@ func (m *MockLock) EXPECT() *MockLockMockRecorder { } // Unlock mocks base method. -func (m *MockLock) Unlock(arg0 context.Context) error { +func (m *MockLock) Unlock(ctx context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Unlock", arg0) + ret := m.ctrl.Call(m, "Unlock", ctx) ret0, _ := ret[0].(error) return ret0 } // Unlock indicates an expected call of Unlock. -func (mr *MockLockMockRecorder) Unlock(arg0 any) *gomock.Call { +func (mr *MockLockMockRecorder) Unlock(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockLock)(nil).Unlock), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockLock)(nil).Unlock), ctx) } diff --git a/mocks/job.go b/mocks/job.go index 0c233e4..6b49b46 100644 --- a/mocks/job.go +++ b/mocks/job.go @@ -5,6 +5,7 @@ // // mockgen -destination=mocks/job.go -package=gocronmocks . Job // + // Package gocronmocks is a generated GoMock package. package gocronmocks @@ -20,6 +21,7 @@ import ( type MockJob struct { ctrl *gomock.Controller recorder *MockJobMockRecorder + isgomock struct{} } // MockJobMockRecorder is the mock recorder for MockJob. diff --git a/mocks/logger.go b/mocks/logger.go index abc1b7a..f7291e3 100644 --- a/mocks/logger.go +++ b/mocks/logger.go @@ -5,6 +5,7 @@ // // mockgen -destination=mocks/logger.go -package=gocronmocks . Logger // + // Package gocronmocks is a generated GoMock package. package gocronmocks @@ -18,6 +19,7 @@ import ( type MockLogger struct { ctrl *gomock.Controller recorder *MockLoggerMockRecorder + isgomock struct{} } // MockLoggerMockRecorder is the mock recorder for MockLogger. @@ -38,69 +40,69 @@ func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { } // Debug mocks base method. -func (m *MockLogger) Debug(arg0 string, arg1 ...any) { +func (m *MockLogger) Debug(msg string, args ...any) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { + varargs := []any{msg} + for _, a := range args { varargs = append(varargs, a) } m.ctrl.Call(m, "Debug", varargs...) } // Debug indicates an expected call of Debug. -func (mr *MockLoggerMockRecorder) Debug(arg0 any, arg1 ...any) *gomock.Call { +func (mr *MockLoggerMockRecorder) Debug(msg any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) + varargs := append([]any{msg}, args...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) } // Error mocks base method. -func (m *MockLogger) Error(arg0 string, arg1 ...any) { +func (m *MockLogger) Error(msg string, args ...any) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { + varargs := []any{msg} + for _, a := range args { varargs = append(varargs, a) } m.ctrl.Call(m, "Error", varargs...) } // Error indicates an expected call of Error. -func (mr *MockLoggerMockRecorder) Error(arg0 any, arg1 ...any) *gomock.Call { +func (mr *MockLoggerMockRecorder) Error(msg any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) + varargs := append([]any{msg}, args...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) } // Info mocks base method. -func (m *MockLogger) Info(arg0 string, arg1 ...any) { +func (m *MockLogger) Info(msg string, args ...any) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { + varargs := []any{msg} + for _, a := range args { varargs = append(varargs, a) } m.ctrl.Call(m, "Info", varargs...) } // Info indicates an expected call of Info. -func (mr *MockLoggerMockRecorder) Info(arg0 any, arg1 ...any) *gomock.Call { +func (mr *MockLoggerMockRecorder) Info(msg any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) + varargs := append([]any{msg}, args...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) } // Warn mocks base method. -func (m *MockLogger) Warn(arg0 string, arg1 ...any) { +func (m *MockLogger) Warn(msg string, args ...any) { m.ctrl.T.Helper() - varargs := []any{arg0} - for _, a := range arg1 { + varargs := []any{msg} + for _, a := range args { varargs = append(varargs, a) } m.ctrl.Call(m, "Warn", varargs...) } // Warn indicates an expected call of Warn. -func (mr *MockLoggerMockRecorder) Warn(arg0 any, arg1 ...any) *gomock.Call { +func (mr *MockLoggerMockRecorder) Warn(msg any, args ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]any{arg0}, arg1...) + varargs := append([]any{msg}, args...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) } diff --git a/mocks/scheduler.go b/mocks/scheduler.go index 5fb4797..ce5eea4 100644 --- a/mocks/scheduler.go +++ b/mocks/scheduler.go @@ -5,13 +5,14 @@ // // mockgen -destination=mocks/scheduler.go -package=gocronmocks . Scheduler // + // Package gocronmocks is a generated GoMock package. package gocronmocks import ( reflect "reflect" - gocron "github.com/go-co-op/gocron/v2" + v2 "github.com/go-co-op/gocron/v2" uuid "github.com/google/uuid" gomock "go.uber.org/mock/gomock" ) @@ -20,6 +21,7 @@ import ( type MockScheduler struct { ctrl *gomock.Controller recorder *MockSchedulerMockRecorder + isgomock struct{} } // MockSchedulerMockRecorder is the mock recorder for MockScheduler. @@ -40,10 +42,10 @@ func (m *MockScheduler) EXPECT() *MockSchedulerMockRecorder { } // Jobs mocks base method. -func (m *MockScheduler) Jobs() []gocron.Job { +func (m *MockScheduler) Jobs() []v2.Job { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Jobs") - ret0, _ := ret[0].([]gocron.Job) + ret0, _ := ret[0].([]v2.Job) return ret0 } @@ -68,14 +70,14 @@ func (mr *MockSchedulerMockRecorder) JobsWaitingInQueue() *gomock.Call { } // NewJob mocks base method. -func (m *MockScheduler) NewJob(arg0 gocron.JobDefinition, arg1 gocron.Task, arg2 ...gocron.JobOption) (gocron.Job, error) { +func (m *MockScheduler) NewJob(arg0 v2.JobDefinition, arg1 v2.Task, arg2 ...v2.JobOption) (v2.Job, error) { m.ctrl.T.Helper() varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "NewJob", varargs...) - ret0, _ := ret[0].(gocron.Job) + ret0, _ := ret[0].(v2.Job) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -158,14 +160,14 @@ func (mr *MockSchedulerMockRecorder) StopJobs() *gomock.Call { } // Update mocks base method. -func (m *MockScheduler) Update(arg0 uuid.UUID, arg1 gocron.JobDefinition, arg2 gocron.Task, arg3 ...gocron.JobOption) (gocron.Job, error) { +func (m *MockScheduler) Update(arg0 uuid.UUID, arg1 v2.JobDefinition, arg2 v2.Task, arg3 ...v2.JobOption) (v2.Job, error) { m.ctrl.T.Helper() varargs := []any{arg0, arg1, arg2} for _, a := range arg3 { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "Update", varargs...) - ret0, _ := ret[0].(gocron.Job) + ret0, _ := ret[0].(v2.Job) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/scheduler.go b/scheduler.go index b395e63..3676627 100644 --- a/scheduler.go +++ b/scheduler.go @@ -90,7 +90,7 @@ type scheduler struct { // used to send all the jobs out when a request is made by the client allJobsOutRequest chan allJobsOutRequest // used to send a jobs out when a request is made by the client - jobOutRequestCh chan jobOutRequest + jobOutRequestCh chan *jobOutRequest // used to run a job on-demand when requested by the client runJobRequestCh chan runJobRequest // new jobs are received here @@ -140,7 +140,7 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) { jobsOutForRescheduling: make(chan uuid.UUID), jobUpdateNextRuns: make(chan uuid.UUID), jobsOutCompleted: make(chan uuid.UUID), - jobOutRequest: make(chan jobOutRequest, 1000), + jobOutRequest: make(chan *jobOutRequest, 100), done: make(chan error, 1), } @@ -159,7 +159,7 @@ func NewScheduler(options ...SchedulerOption) (Scheduler, error) { startedCh: make(chan struct{}), stopCh: make(chan struct{}), stopErrCh: make(chan error, 1), - jobOutRequestCh: make(chan jobOutRequest), + jobOutRequestCh: make(chan *jobOutRequest), runJobRequestCh: make(chan runJobRequest), allJobsOutRequest: make(chan allJobsOutRequest), } @@ -461,7 +461,7 @@ func (s *scheduler) selectExecJobsOutCompleted(id uuid.UUID) { s.jobs[id] = j } -func (s *scheduler) selectJobOutRequest(out jobOutRequest) { +func (s *scheduler) selectJobOutRequest(out *jobOutRequest) { if j, ok := s.jobs[out.id]; ok { select { case out.outChan <- j: diff --git a/util.go b/util.go index 2dd1c26..282463a 100644 --- a/util.go +++ b/util.go @@ -35,16 +35,16 @@ func callJobFuncWithParams(jobFunc any, params ...any) error { return nil } -func requestJob(id uuid.UUID, ch chan jobOutRequest) *internalJob { +func requestJob(id uuid.UUID, ch chan *jobOutRequest) *internalJob { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() return requestJobCtx(ctx, id, ch) } -func requestJobCtx(ctx context.Context, id uuid.UUID, ch chan jobOutRequest) *internalJob { +func requestJobCtx(ctx context.Context, id uuid.UUID, ch chan *jobOutRequest) *internalJob { resp := make(chan internalJob, 1) select { - case ch <- jobOutRequest{ + case ch <- &jobOutRequest{ id: id, outChan: resp, }: