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() {})
+}