Skip to content

Commit

Permalink
x/time/rate: add rate.Sometimes, which runs a function occasionally.
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
gaal authored and Sajmani committed Oct 20, 2022
1 parent f3bd1da commit 80b9fac
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 0 deletions.
67 changes: 67 additions & 0 deletions rate/sometimes.go
Original file line number Diff line number Diff line change
@@ -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++
}
94 changes: 94 additions & 0 deletions rate/sometimes_test.go
Original file line number Diff line number Diff line change
@@ -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() {})
}

0 comments on commit 80b9fac

Please sign in to comment.