x/time/rate: add rate.Sometimes, which runs a function occasionally.
Modeled after sync.Once; intended to provide simple throttling
akin to the C++ log functions LOG_FIRST_N, LOG_EVERY_N, and
LOG_EVERY_N_SEC.
Originally authored by sameer@golang.org.
Fixes golang/go#54237
Change-Id: I7c6266cc780eb6dad30d310485de492f790dbcdb
Reviewed-on: https://go-review.googlesource.com/c/time/+/421915
Reviewed-by: Sameer Ajmani <sameer@golang.org>
diff --git a/rate/sometimes.go b/rate/sometimes.go
new file mode 100644
index 0000000..6ba99dd
--- /dev/null
+++ b/rate/sometimes.go
@@ -0,0 +1,67 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package rate
+
+import (
+ "sync"
+ "time"
+)
+
+// Sometimes will perform an action occasionally. The First, Every, and
+// Interval fields govern the behavior of Do, which performs the action.
+// A zero Sometimes value will perform an action exactly once.
+//
+// # Example: logging with rate limiting
+//
+// var sometimes = rate.Sometimes{First: 3, Interval: 10*time.Second}
+// func Spammy() {
+// sometimes.Do(func() { log.Info("here I am!") })
+// }
+type Sometimes struct {
+ First int // if non-zero, the first N calls to Do will run f.
+ Every int // if non-zero, every Nth call to Do will run f.
+ Interval time.Duration // if non-zero and Interval has elapsed since f's last run, Do will run f.
+
+ mu sync.Mutex
+ count int // number of Do calls
+ last time.Time // last time f was run
+}
+
+// Do runs the function f as allowed by First, Every, and Interval.
+//
+// The model is a union (not intersection) of filters. The first call to Do
+// always runs f. Subsequent calls to Do run f if allowed by First or Every or
+// Interval.
+//
+// A non-zero First:N causes the first N Do(f) calls to run f.
+//
+// A non-zero Every:M causes every Mth Do(f) call, starting with the first, to
+// run f.
+//
+// A non-zero Interval causes Do(f) to run f if Interval has elapsed since
+// Do last ran f.
+//
+// Specifying multiple filters produces the union of these execution streams.
+// For example, specifying both First:N and Every:M causes the first N Do(f)
+// calls and every Mth Do(f) call, starting with the first, to run f. See
+// Examples for more.
+//
+// If Do is called multiple times simultaneously, the calls will block and run
+// serially. Therefore, Do is intended for lightweight operations.
+//
+// Because a call to Do may block until f returns, if f causes Do to be called,
+// it will deadlock.
+func (s *Sometimes) Do(f func()) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ if s.count == 0 ||
+ (s.First > 0 && s.count < s.First) ||
+ (s.Every > 0 && s.count%s.Every == 0) ||
+ (s.Interval > 0 && time.Since(s.last) >= s.Interval) {
+ f()
+ s.last = time.Now()
+ }
+ s.count++
+}
diff --git a/rate/sometimes_test.go b/rate/sometimes_test.go
new file mode 100644
index 0000000..6781770
--- /dev/null
+++ b/rate/sometimes_test.go
@@ -0,0 +1,94 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package rate_test
+
+import (
+ "fmt"
+ "math"
+ "testing"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+func ExampleSometimes_once() {
+ // The zero value of Sometimes behaves like sync.Once, though less efficiently.
+ var s rate.Sometimes
+ s.Do(func() { fmt.Println("1") })
+ s.Do(func() { fmt.Println("2") })
+ s.Do(func() { fmt.Println("3") })
+ // Output:
+ // 1
+}
+
+func ExampleSometimes_first() {
+ s := rate.Sometimes{First: 2}
+ s.Do(func() { fmt.Println("1") })
+ s.Do(func() { fmt.Println("2") })
+ s.Do(func() { fmt.Println("3") })
+ // Output:
+ // 1
+ // 2
+}
+
+func ExampleSometimes_every() {
+ s := rate.Sometimes{Every: 2}
+ s.Do(func() { fmt.Println("1") })
+ s.Do(func() { fmt.Println("2") })
+ s.Do(func() { fmt.Println("3") })
+ // Output:
+ // 1
+ // 3
+}
+
+func ExampleSometimes_interval() {
+ s := rate.Sometimes{Interval: 1 * time.Second}
+ s.Do(func() { fmt.Println("1") })
+ s.Do(func() { fmt.Println("2") })
+ time.Sleep(1 * time.Second)
+ s.Do(func() { fmt.Println("3") })
+ // Output:
+ // 1
+ // 3
+}
+
+func ExampleSometimes_mix() {
+ s := rate.Sometimes{
+ First: 2,
+ Every: 2,
+ Interval: 2 * time.Second,
+ }
+ s.Do(func() { fmt.Println("1 (First:2)") })
+ s.Do(func() { fmt.Println("2 (First:2)") })
+ s.Do(func() { fmt.Println("3 (Every:2)") })
+ time.Sleep(2 * time.Second)
+ s.Do(func() { fmt.Println("4 (Interval)") })
+ s.Do(func() { fmt.Println("5 (Every:2)") })
+ s.Do(func() { fmt.Println("6") })
+ // Output:
+ // 1 (First:2)
+ // 2 (First:2)
+ // 3 (Every:2)
+ // 4 (Interval)
+ // 5 (Every:2)
+}
+
+func TestSometimesZero(t *testing.T) {
+ s := rate.Sometimes{Interval: 0}
+ s.Do(func() {})
+ s.Do(func() {})
+}
+
+func TestSometimesMax(t *testing.T) {
+ s := rate.Sometimes{Interval: math.MaxInt64}
+ s.Do(func() {})
+ s.Do(func() {})
+}
+
+func TestSometimesNegative(t *testing.T) {
+ s := rate.Sometimes{Interval: -1}
+ s.Do(func() {})
+ s.Do(func() {})
+}