@@ -38,13 +38,57 @@ func (p *intervalPoller) SetInterval(d time.Duration) {
3838}
3939
4040func (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