Skip to content

Commit 11b9619

Browse files
committed
fix(poller): wait for both wall clock and monotonic value
Signed-off-by: Babak K. Shandiz <babakks@github.com>
1 parent d28c578 commit 11b9619

File tree

1 file changed

+51
-7
lines changed

1 file changed

+51
-7
lines changed

device/poller.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,57 @@ func (p *intervalPoller) SetInterval(d time.Duration) {
3838
}
3939

4040
func (p *intervalPoller) Wait() error {
41-
t := time.NewTimer(p.interval)
42-
select {
43-
case <-p.ctx.Done():
44-
t.Stop()
45-
return p.ctx.Err()
46-
case <-t.C:
47-
return nil
41+
// We know that in virtualised environments (e.g. WSL or VMs), the monotonic
42+
// clock, which is the source of time measurements in Go, can run faster than
43+
// real time. So, polling intervals should be adjusted to avoid falling into
44+
// an endless loop of "slow_down" errors from the server. See the following
45+
// issue in cli/cli for more context (especially what's after this particular
46+
// comment):
47+
// - https://github.com/cli/cli/issues/9370#issuecomment-3759706125
48+
//
49+
// We've observed ~10% faster ticking, thanks to community, but a chat with
50+
// AI suggests it's typically between 5-15% on WSL, and can get up to 30% in
51+
// worst cases. There are issues reported on the WSL repo, but I couldn't
52+
// find any documented/conclusive data about this.
53+
//
54+
// See more:
55+
// - https://github.com/microsoft/WSL/issues/12583
56+
//
57+
// Although the wall clock is not a reliable source for time measurement, but
58+
// using it we can spot any time drift. Here, we'll wait until both the wall
59+
// clock and the monotonic value are past the intended wait time. To avoid
60+
// falling into a convergence loop (i.e. getting smaller and smaller wait
61+
// intervals), we have to limit the minimum wait interval to 1s.
62+
//
63+
// Although it's rare, but it's possible for the wall clock to be modified by
64+
// the user or NTP adjustments during polling. To avoid being affected by
65+
// large changes in the wall clock, we'll also cap the secondary wait time to
66+
// the original polling interval.
67+
68+
tstop := time.Now().UnixNano() + int64(p.interval)
69+
interval := p.interval
70+
71+
for {
72+
select {
73+
case <-p.ctx.Done():
74+
return p.ctx.Err()
75+
case now := <-time.After(interval):
76+
diff := now.UnixNano() - tstop
77+
if diff > 0 {
78+
return nil
79+
}
80+
81+
diff = -diff
82+
if diff < int64(time.Second) {
83+
// Keep a 1s minimum interval for secondary waits.
84+
interval = time.Second
85+
} else if diff > int64(p.interval) {
86+
// Cap the secondary wait to the original interval.
87+
interval = p.interval
88+
} else {
89+
interval = time.Duration(diff)
90+
}
91+
}
4892
}
4993
}
5094

0 commit comments

Comments
 (0)