Skip to content

Instantly share code, notes, and snippets.

@clarkmcc
Last active July 25, 2020 15:01
Show Gist options
  • Save clarkmcc/2c40db4bb7b3aea412d78c8a490f030e to your computer and use it in GitHub Desktop.
Save clarkmcc/2c40db4bb7b3aea412d78c8a490f030e to your computer and use it in GitHub Desktop.
A thread-safe counter that can optionally be expired. The expiration works that way a rate bucket works, where the counter is reset repeatedly every time the counter has expired. This counter is useful for questions like, "how often did x happen over the course of y?"."
package threadsafe
import (
"sync"
"time"
)
// Handy for comparisons
var ZeroTime = time.Time{}
// ExpiringCounter is a counter whose value returns to 0 after the expiration
// time is passed. This counter is also thread safe so all operations can be
// executed concurrently.
type ExpiringCounter struct {
// The underlying counter value itself
counter int
// The time that the counter was started at
start time.Time
// The time that the counter expires at
expiration time.Time
// The duration to wait before expiration
duration time.Duration
lock sync.Mutex
}
// NewCounter returns a new ExpiringCounter
func NewCounter() *ExpiringCounter {
return &ExpiringCounter{start: time.Time{}, expiration: time.Time{}}
}
// WithExpiration sets the expiration for this counter
func (c *ExpiringCounter) WithExpiration(duration time.Duration) *ExpiringCounter {
c.lock.Lock()
defer c.lock.Unlock()
c.setExpiration(duration)
return c
}
// Value returns the current value of the counter unless the counter
// has expired, in which case the counter is reset, and returned.
func (c *ExpiringCounter) Value() int {
c.lock.Lock()
defer c.lock.Unlock()
c.checkIsExpired()
return c.counter
}
// Inc increments the counter by one
func (c *ExpiringCounter) Inc() {
c.lock.Lock()
defer c.lock.Unlock()
c.checkIsExpired()
c.counter++
}
// Reset resets the counter
func (c *ExpiringCounter) Reset() {
c.lock.Lock()
defer c.lock.Unlock()
c.counter = 0
}
// checkIsExpired checks if the counter is expired, if so it resets
// the counter to 0
func (c *ExpiringCounter) checkIsExpired() {
if c.IsExpired(c.duration) {
c.setExpiration(c.duration)
c.counter = 0
}
}
// IsExpired returns true if the provided expiration duration is after
// the start time. If the counter does not have an expiration, this method
// will always return false
func (c *ExpiringCounter) IsExpired(expiration time.Duration) bool {
if c.start == ZeroTime || c.expiration == ZeroTime {
return false
}
return time.Now().After(c.start.Add(expiration))
}
// setExpiration sets the expiration based on:
// * If the provided expiration + the start time is after the current time
// then we reset the start time to the current time and set the expiration
// to equal the current time plus the expiration duration
// * If the provided expiration + the start time is before the current time
// or in other words if the new expiration does not cause an immediate
// expiration, then we set the expiration based on the new expiration duration
// but the most recent value of start
func (c *ExpiringCounter) setExpiration(duration time.Duration) {
// If we're trying to set an expiration and the start time has never
// been set, then set the start time equal to the current time
if c.start == ZeroTime {
c.start = time.Now()
}
// If the counter is already expired, the reset the start time
if c.IsExpired(duration) {
c.start = time.Now()
}
// Otherwise just change the expiration without resetting the start time
c.expiration = c.start.Add(duration)
c.duration = duration
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment