Last active
July 25, 2020 15:01
-
-
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?"."
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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