Files
kopia/internal/sleepable/sleepable_timer.go
2025-10-22 22:22:54 -07:00

79 lines
1.8 KiB
Go

// Package sleepable implements a timer that can trigger at or soon after a given time and supports the computer going to sleep while the timer is waiting.
package sleepable
import (
"sync"
"time"
)
// MaxSleepTime is the maximum duration the timer will sleep in one interval. Can be overridden for testing.
var MaxSleepTime = 15 * time.Second
// Timer is a timer that can trigger at or soon after a given time and supports the computer going to sleep while the timer is waiting.
type Timer struct {
C <-chan struct{}
stopChan chan struct{}
stopOnce sync.Once
}
// Stop stops the timer.
func (c *Timer) Stop() {
c.stopOnce.Do(func() {
close(c.stopChan)
})
}
// NewTimer creates a new timer that will trigger at the given time.
func NewTimer(nowFunc func() time.Time, until time.Time) *Timer {
ch := make(chan struct{})
t := &Timer{
C: ch,
stopChan: make(chan struct{}),
}
var currentTimer *time.Timer
// capture maxSleepTime at the start of the goroutine to avoid race conditions.
maxSleepTime := MaxSleepTime
// start a goroutine that will sleep until the timer is triggered or the timer is stopped.
go func() {
defer func() {
if currentTimer != nil {
currentTimer.Stop()
}
}()
for {
now := nowFunc()
// when the current time is after the target time, the timer immediately triggers by closing the channel.
if now.After(until) {
close(ch)
return
}
nextSleepTime := min(until.Sub(now), maxSleepTime)
if currentTimer != nil {
currentTimer.Stop()
}
currentTimer = time.NewTimer(nextSleepTime)
select {
case <-t.stopChan:
// stop channel was closed, exit without closing the "C" channel
return
case <-currentTimer.C:
// timer did trigger, re-execute the loop to see if we reached the target time
continue
}
}
}()
return t
}