Change:
I noticed that this code: was printing which should not happen. Also occurs in wait.Forever. I suspect that in regressed this as that was the most recent major update and I only started seeing this on a recent apimachinery bump.
/sig api-machinery /assign as the reviewer
GitHub didn't allow me to assign the following users: zhan849. Note that only , repo collaborators and people who have commented on this issue/PR can be assigned. Additionally, issues/PRs can only have 10 assignees at the same time. For more information please see <detailsIn response to : /sig api-machinery /assign as the reviewer Instructions for interacting with me using PR comments are available . If you have questions or suggestions related to my behavior, please file an issue against the repository. </details
/assign
This is a correctness regression because the wait methods expect not to be run run more often than the period and may guard expensive behavior. Suggest a revert followed by a more rigorous test and review on merge.
thanks for reporting this, and I apologize about the regression I made. I did some quick test, and I am able to reproduce it occasionally, which indicate a race somewhere: Test: Failed output: Successful output? In the original code I checked in, I initialized the timer in the backoff manager construction time: However, after I changed to initializing the timer when is called, I'm not able to reproduce the race. The results indicated that the timer initialization is likely to be the place that caused the regression , and seems to be able to get fixed by change the way the timer gets initialized. However I'm unable to identify why it could cause such race the first time. Are you able to provide any insights? /cc
why are we initializing a timer with a zero duration? that looks related to this issue:
I see us resetting a timer we have no guarantee is stopped/expired:
dropped the information needed to accurately stop/reset the timer as documented
Hi thanks for providing the information! seems like the correct place to initialize the timer is in the "Backoff" method, since zero duration is problematic. When I was putting up the interface, I documented that the timer will not be drained, and the reason is that returns a timer explicitly for user to block on that, so it might be confusing to block in itself Shall we change the impl to always drain the channel in backoff instead or relying on user to use it correctly? with this being said, will be changed to , which means that the will still rely on user's providing information about whether the channel is drained or not.
/cc thoughts? ^^
- sorry for that - we hit the bug with resetting timer not the first time, I should have caught that (I remember at least two other bugs with that). I'm not sure revert is a good option, because there were PRs that were chained on that: - this would have to be a very careful manual revert which I'm not sure it justifies the risk, given that 1.18 is already out. Currently the behavior is unpredictable, because if getNextbackoof() called for the first time is non-zero, it may or may not be ready immediately depending on the race. That is obviously bad. Initializing with infinity may solve this particular problem, but I need to think more about it to ensure that we don't have any problems in the later phase too...
thanks ! and sry again I did not read the timer's doc carefully enough but simply followed the Clock interface :( To fix, we can also initialize the timer to nil and create a fresh one when is first called, it's safe as this component is designed to run in single-threaded environment. But to ensure timer is drained before reset, we need to have user explicitly pass in "if timer is drained" into the to accurately decide if needs to drain. Or, is it ok to document the behavior and assume user will drain on before calling next ?
Very quick (I might have missed something) glance from the code suggests, that the only place we would need to change is actually BackoffUntil method. So passing the "if timer is drained" bit seems less error prone imho.
We should avoid changing the interface in a patch release.
I think initializing the timer in Backoff() and documenting the returned timer must be expired and drained before calling Backoff() again is the most compatible thing we can do:
Thanks and I'm also more inclined to Jordan's proposal, besides avoid interface change, if user are able to accurately pass "if timer is drained" info, it's fair to let user finish draining. do you have a PR already? of if not, I can make one, and add more tests if possible about this failure case.
I do not have a PR open

Discussion:
} // BackoffManager manages backoff with a particular scheme based on its underlying implementation. It provides <del> // an interface to return a timer for backoff, and caller shall backoff until Timer.C returns. If the second Backoff() // is called before the timer from the first Backoff() call finishes, the first timer will NOT be drained. </del> <ins> // an interface to return a timer for backoff, and caller shall backoff until Timer.C() drains. If the second Backoff() // is called before the timer from the first Backoff() call finishes, the first timer will NOT be drained and result in // undetermined behavior. </ins> // The BackoffManager is supposed to be called in a single-threaded environment. type BackoffManager interface { Backoff() clock.Timer

Steps: math.MaxInt32, Cap:   maxBackoff, }, <del> backoffTimer:         c.NewTimer(0), </del> <ins> backoffTimer:         nil, </ins> initialBackoff:       initBackoff, lastBackoffStart:     c.Now(), backoffResetDuration: resetDuration,

return b.backoff.Step() } <del> // Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer for backoff. </del> <ins> // Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer for exponential backoff. // The returned timer must be drained before calling Backoff() the second time </ins> func (b *exponentialBackoffManagerImpl) Backoff() clock.Timer { <del> b.backoffTimer.Reset(b.getNextBackoff()) </del> <ins> if b.backoffTimer == nil { b.backoffTimer = b.clock.NewTimer(b.getNextBackoff()) } else { b.backoffTimer.Reset(b.getNextBackoff()) } </ins> return b.backoffTimer }

clock:        c, duration:     duration, jitter:       jitter, <del> backoffTimer: c.NewTimer(0), </del> <ins> backoffTimer: nil, </ins> } }

return jitteredPeriod } <ins> // Backoff implements BackoffManager.Backoff, it returns a timer so caller can block on the timer for jittered backoff. // The returned timer must be drained before calling Backoff() the second time </ins> func (j *jitteredBackoffManagerImpl) Backoff() clock.Timer { <del> j.backoffTimer.Reset(j.getNextBackoff()) </del> <ins> backoff := j.getNextBackoff() if j.backoffTimer == nil { j.backoffTimer = j.clock.NewTimer(backoff) } else { j.backoffTimer.Reset(backoff) } </ins> return j.backoffTimer }

t.Errorf("backoff should be 1, but got %d", backoff) } } <ins> func TestJitterBackoffManagerWithRealClock(t *testing.T) { backoffMgr := NewJitteredBackoffManager(1*time.Millisecond, 0, &clock.RealClock{}) for i := 0; i < 5; i++ { start := time.Now() <-backoffMgr.Backoff().C() passed := time.Now().Sub(start) if passed < 1*time.Millisecond { t.Errorf("backoff should be at least 1ms, but got %s", passed.String()) } } } func TestExponentialBackoffManagerWithRealClock(t *testing.T) { // backoff at least 1ms, 2ms, 4ms, 8ms, 10ms, 10ms, 10ms durationFactors := []time.Duration{1, 2, 4, 8, 10, 10, 10} backoffMgr := NewExponentialBackoffManager(1*time.Millisecond, 10*time.Millisecond, 1*time.Hour, 2.0, 0.0, &clock.RealClock{}) for i := range durationFactors { start := time.Now() <-backoffMgr.Backoff().C() passed := time.Now().Sub(start) if passed < durationFactors[i]*time.Millisecond { t.Errorf("backoff should be at least %d ms, but got %s", durationFactors[i], passed.String()) } } } </ins>