git commit -m "feat: add SchedulerStarted and SchedulerStopped monitoring"

This commit is contained in:
iyashjayesh 2025-10-26 01:10:05 +05:30
parent 11cf7cf8e2
commit 83f5f60b67
4 changed files with 206 additions and 1 deletions

View File

@ -59,6 +59,7 @@ var (
ErrStartTimeLaterThanEndTime = errors.New("gocron: WithStartDateTime: start must not be later than end")
ErrStopTimeEarlierThanStartTime = errors.New("gocron: WithStopDateTime: end must not be earlier than start")
ErrWithStopTimeoutZeroOrNegative = errors.New("gocron: WithStopTimeout: timeout must be greater than 0")
ErrSchedulerMonitorNil = errors.New("gocron: scheduler monitor cannot be nil")
)
// internal errors

View File

@ -7,6 +7,7 @@ import (
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@ -100,6 +101,11 @@ type scheduler struct {
removeJobCh chan uuid.UUID
// requests from the client to remove jobs by tags are received here
removeJobsByTagsCh chan []string
// scheduler monitor from which metrics can be collected
schedulerMonitor SchedulerMonitor
// mutex to protect access to the scheduler monitor
schedulerMonitorMu sync.RWMutex
}
type newJobIn struct {
@ -815,8 +821,13 @@ func (s *scheduler) RemoveJob(id uuid.UUID) error {
func (s *scheduler) Start() {
select {
case <-s.shutdownCtx.Done():
// Scheduler already shut down, don't notify
return
case s.startCh <- struct{}{}:
<-s.startedCh
<-s.startedCh // Wait for scheduler to actually start
// Scheduler has started
s.notifySchedulerStarted()
}
}
@ -849,6 +860,9 @@ func (s *scheduler) Shutdown() error {
select {
case err := <-s.stopErrCh:
t.Stop()
// notify monitor that scheduler stopped
s.notifySchedulerStopped()
return err
case <-t.C:
return ErrStopSchedulerTimedOut
@ -1054,3 +1068,34 @@ func WithMonitorStatus(monitor MonitorStatus) SchedulerOption {
return nil
}
}
// WithSchedulerMonitor sets a monitor that will be called with scheduler-level events.
func WithSchedulerMonitor(monitor SchedulerMonitor) SchedulerOption {
return func(s *scheduler) error {
if monitor == nil {
return ErrSchedulerMonitorNil
}
s.schedulerMonitorMu.Lock()
defer s.schedulerMonitorMu.Unlock()
s.schedulerMonitor = monitor
return nil
}
}
// notifySchedulerStarted notifies the monitor that scheduler has started
func (s *scheduler) notifySchedulerStarted() {
s.schedulerMonitorMu.RLock()
defer s.schedulerMonitorMu.RUnlock()
if s.schedulerMonitor != nil {
s.schedulerMonitor.SchedulerStarted()
}
}
// notifySchedulerStopped notifies the monitor that scheduler has stopped
func (s *scheduler) notifySchedulerStopped() {
s.schedulerMonitorMu.RLock()
defer s.schedulerMonitorMu.RUnlock()
if s.schedulerMonitor != nil {
s.schedulerMonitor.SchedulerStopped()
}
}

11
scheduler_monitor.go Normal file
View File

@ -0,0 +1,11 @@
package gocron
// SchedulerMonitor is called by the Scheduler to provide scheduler-level
// metrics and events.
type SchedulerMonitor interface {
// SchedulerStarted is called when Start() is invoked on the scheduler.
SchedulerStarted()
// SchedulerStopped is called when Shutdown() completes successfully.
SchedulerStopped()
}

148
scheduler_monitor_test.go Normal file
View File

@ -0,0 +1,148 @@
package gocron
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockSchedulerMonitor is a test implementation
type mockSchedulerMonitor struct {
mu sync.Mutex
schedulerStartedCount int
schedulerStoppedCount int
}
// SchedulerStarted increments the started count
func (m *mockSchedulerMonitor) SchedulerStarted() {
m.mu.Lock()
defer m.mu.Unlock()
m.schedulerStartedCount++
}
// SchedulerStopped increments the stopped count
func (m *mockSchedulerMonitor) SchedulerStopped() {
m.mu.Lock()
defer m.mu.Unlock()
m.schedulerStoppedCount++
}
// getSchedulerStartedCount returns the count of SchedulerStarted calls
func (m *mockSchedulerMonitor) getSchedulerStartedCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.schedulerStartedCount
}
// getSchedulerStoppedCount returns the count of SchedulerStopped calls
func (m *mockSchedulerMonitor) getSchedulerStoppedCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return m.schedulerStoppedCount
}
// Test that SchedulerStopped is called
func TestSchedulerMonitor_SchedulerStopped(t *testing.T) {
monitor := &mockSchedulerMonitor{}
s, err := NewScheduler(WithSchedulerMonitor(monitor))
require.NoError(t, err)
s.Start()
time.Sleep(10 * time.Millisecond)
err = s.Shutdown()
require.NoError(t, err)
// verify SchedulerStopped was called exactly once
assert.Equal(t, 1, monitor.getSchedulerStoppedCount())
}
// Test multiple start/stop cycles
func TestSchedulerMonitor_MultipleStartStopCycles(t *testing.T) {
monitor := &mockSchedulerMonitor{}
s, err := NewScheduler(WithSchedulerMonitor(monitor))
require.NoError(t, err)
// first cycle
s.Start()
time.Sleep(10 * time.Millisecond)
err = s.Shutdown()
require.NoError(t, err)
// second cycle
s.Start()
time.Sleep(10 * time.Millisecond)
err = s.Shutdown()
require.NoError(t, err)
// verifying counts
assert.Equal(t, 2, monitor.getSchedulerStartedCount())
assert.Equal(t, 2, monitor.getSchedulerStoppedCount())
}
// Test that start and stop are called in correct order
func TestSchedulerMonitor_StartStopOrder(t *testing.T) {
events := []string{}
var mu sync.Mutex
monitor := &orderTrackingMonitor{
onStart: func() {
mu.Lock()
events = append(events, "started")
mu.Unlock()
},
onStop: func() {
mu.Lock()
events = append(events, "stopped")
mu.Unlock()
},
}
s, err := NewScheduler(WithSchedulerMonitor(monitor))
require.NoError(t, err)
s.Start()
time.Sleep(10 * time.Millisecond)
s.Shutdown()
time.Sleep(10 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
assert.Equal(t, []string{"started", "stopped"}, events)
}
// Helper monitor for tracking order
type orderTrackingMonitor struct {
onStart func()
onStop func()
}
func (m *orderTrackingMonitor) SchedulerStarted() {
if m.onStart != nil {
m.onStart()
}
}
func (m *orderTrackingMonitor) SchedulerStopped() {
if m.onStop != nil {
m.onStop()
}
}
// Test that stopped is not called if scheduler wasn't started
func TestSchedulerMonitor_StoppedNotCalledIfNotStarted(t *testing.T) {
monitor := &mockSchedulerMonitor{}
s, err := NewScheduler(WithSchedulerMonitor(monitor))
require.NoError(t, err)
err = s.Shutdown()
if err != nil {
t.Logf("Shutdown returned error as expected: %v", err)
}
// Depending on implementation, this might return an error
// But stopped should not be called
assert.Equal(t, 0, monitor.getSchedulerStoppedCount())
}