Repository: go-co-op/gocron
Pull request #136 (2021-03-05 05:53:38)

allow enforcing uniqueness across tags
### What does this do?
- allows enforcing tags to be unique across the scheduler
- there isn't a good way I can see to support forcing jobs tagged via the job.Tag() method to be unique as they have no knowledge of the scheduler. I considered a global tags map, but that would then be unique across all schedulers that the user may define. Limiting the scope to a single scheduler seems better, albeit with the limitation that the scheduler.Tag() method must be used. 

### Which issue(s) does this PR fix/relate to?
<!--- Put `Resolves #XXX` here to auto-close the issue that your PR fixes (if such) --->
#131 

### List any changes that modify/break current functionality


### Have you included tests for your changes?
👍 

### Did you document any new/modified functionality?

- [x] Updated `example_test.go`
- [ ] Updated `README.md`

### Notes



Patch:
diff --git a/gocron.go b/gocron.go
index 66b2176..4ab0c59 100644
--- a/gocron.go
+++ b/gocron.go
@@ -24,9 +24,9 @@ var (
 	ErrInvalidInterval               = errors.New(".Every() interval must be greater than 0")
 	ErrInvalidIntervalType           = errors.New(".Every() interval must be int, time.Duration, or string")
 	ErrInvalidIntervalUnitsSelection = errors.New("an .Every() duration interval cannot be used with units (e.g. .Seconds())")
-
-	ErrAtTimeNotSupported  = errors.New("the At() not supported for time unit")
-	ErrWeekdayNotSupported = errors.New("weekday is not supported for time unit")
+	ErrAtTimeNotSupported            = errors.New("the At() not supported for time unit")
+	ErrWeekdayNotSupported           = errors.New("weekday is not supported for time unit")
+	ErrTagsUnique                    = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) }
 )
 
 func wrapOrError(toWrap error, err error) error {
diff --git a/scheduler.go b/scheduler.go
index a0dbcc4..79ed56c 100644
--- a/scheduler.go
+++ b/scheduler.go
@@ -26,6 +26,8 @@ type Scheduler struct {
 
 	time     timeWrapper // wrapper around time.Time
 	executor *executor   // executes jobs passed via chan
+
+	tags map[string]struct{} // for storing tags when unique tags is set
 }
 
 // NewScheduler creates a new Scheduler
@@ -564,6 +566,17 @@ func (s *Scheduler) At(t string) *Scheduler {
 // Tag will add a tag when creating a job.
 func (s *Scheduler) Tag(t ...string) *Scheduler {
 	job := s.getCurrentJob()
+
+	if s.tags != nil {
+		for _, tag := range t {
+			if _, ok := s.tags[tag]; ok {
+				job.err = wrapOrError(job.err, ErrTagsUnique(tag))
+				return s
+			}
+			s.tags[tag] = struct{}{}
+		}
+	}
+
 	job.tags = t
 	return s
 }
@@ -713,3 +726,11 @@ func (s *Scheduler) getCurrentJob() *Job {
 func (s *Scheduler) now() time.Time {
 	return s.time.Now(s.Location())
 }
+
+// TagsUnique forces job tags to be unique across the scheduler
+// when adding tags with (s *Scheduler) Tag().
+// This does not enforce uniqueness on tags added via
+// (j *Job) Tag()
+func (s *Scheduler) TagsUnique() {
+	s.tags = make(map[string]struct{})
+}


Test patch:
diff --git a/example_test.go b/example_test.go
index 74b68e5..d7d4180 100644
--- a/example_test.go
+++ b/example_test.go
@@ -525,6 +525,18 @@ func ExampleScheduler_Tag() {
 	// [tag]
 }
 
+func ExampleScheduler_TagsUnique() {
+	s := gocron.NewScheduler(time.UTC)
+	s.TagsUnique()
+
+	_, _ = s.Every(1).Week().Tag("foo").Do(task)
+	_, err := s.Every(1).Week().Tag("foo").Do(task)
+
+	fmt.Println(err)
+	// Output:
+	// a non-unique tag was set on the job: foo
+}
+
 func ExampleScheduler_TaskPresent() {
 	s := gocron.NewScheduler(time.UTC)
 
diff --git a/scheduler_test.go b/scheduler_test.go
index 1fb0849..b2d7db7 100644
--- a/scheduler_test.go
+++ b/scheduler_test.go
@@ -783,35 +783,6 @@ func TestRunJobsWithLimit(t *testing.T) {
 		assert.Equal(t, 1, counter)
 	})
 
-	// this test isn't designed well and the functionality isn't working
-	//t.Run("change limit", func(t *testing.T) {
-	//	semaphore := make(chan bool)
-	//
-	//	s := NewScheduler(time.UTC)
-	//
-	//	j, err := s.Every(1).Second().Do(func() {
-	//		semaphore <- true
-	//	})
-	//	require.NoError(t, err)
-	//	j.LimitRunsTo(1)
-	//
-	//	s.StartAsync()
-	//	time.Sleep(1 * time.Second)
-	//
-	//	j.LimitRunsTo(2)
-	//	time.Sleep(1 * time.Second)
-	//
-	//	var counter int
-	//	select {
-	//	case <-time.After(2 * time.Second):
-	//		assert.Equal(t, 2, counter)
-	//		// test passed
-	//	case <-semaphore:
-	//		counter++
-	//		require.LessOrEqual(t, counter, 2)
-	//	}
-	//})
-
 	t.Run("remove after last run", func(t *testing.T) {
 		semaphore := make(chan bool)
 
@@ -1087,3 +1058,30 @@ func TestScheduler_RemoveAfterLastRun(t *testing.T) {
 		}
 	})
 }
+
+func TestScheduler_TagsUnique(t *testing.T) {
+	const (
+		foo = "foo"
+		bar = "bar"
+		baz = "baz"
+	)
+
+	s := NewScheduler(time.UTC)
+	s.TagsUnique()
+
+	j, err := s.Every("1s").Tag(foo, bar).Do(func() {})
+	require.NoError(t, err)
+
+	// uniqueness not enforced on jobs tagged with job.Tag()
+	// thus tagging the job here is allowed
+	j.Tag(baz)
+	_, err = s.Every("1s").Tag(baz).Do(func() {})
+	require.NoError(t, err)
+
+	_, err = s.Every("1s").Tag(foo).Do(func() {})
+	assert.EqualError(t, err, ErrTagsUnique(foo).Error())
+
+	_, err = s.Every("1s").Tag(bar).Do(func() {})
+	assert.EqualError(t, err, ErrTagsUnique(bar).Error())
+
+}