// Copyright 2016-2018, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package retry import ( "context" "time" ) type Acceptor struct { Accept Acceptance // a function that determines when to proceed. Delay *time.Duration // an optional delay duration. Backoff *float64 // an optional backoff multiplier. MaxDelay *time.Duration // an optional maximum delay duration. } // Acceptance is meant to accept a condition. // It returns true when this condition has succeeded, and false otherwise // (to which we respond by waiting and retrying after a certain period of time). // If a non-nil error is returned, retrying halts. // The interface{} data may be used to return final values to the caller. // // Try specifies the attempt number, // zero indicating that this is the first attempt with no retries. type Acceptance func(try int, nextRetryTime time.Duration) (success bool, result interface{}, err error) const ( DefaultDelay time.Duration = 100 * time.Millisecond // by default, delay by 100ms DefaultBackoff float64 = 1.5 // by default, backoff by 1.5x DefaultMaxDelay time.Duration = 5 * time.Second // by default, no more than 5 seconds ) // Retryer provides the ability to run and retry a fallible operation // with exponential backoff. type Retryer struct { // Returns a channel that will send the time after the duration elapses. // // Defaults to time.After. After func(time.Duration) <-chan time.Time } // Until runs the provided acceptor until one of the following conditions is met: // // - the operation succeeds: returns true and the result // - the context expires: returns false and no result or errors // - the operation returns an error: returns an error // // Note that the number of attempts is not limited. // The Acceptance function is responsible for determining // when to stop retrying. func (r *Retryer) Until(ctx context.Context, acceptor Acceptor) (bool, interface{}, error) { timeAfter := time.After if r.After != nil { timeAfter = r.After } // Prepare our delay and backoff variables. var delay time.Duration if acceptor.Delay == nil { delay = DefaultDelay } else { delay = *acceptor.Delay } var backoff float64 if acceptor.Backoff == nil { backoff = DefaultBackoff } else { backoff = *acceptor.Backoff } var maxDelay time.Duration if acceptor.MaxDelay == nil { maxDelay = DefaultMaxDelay } else { maxDelay = *acceptor.MaxDelay } // Loop until the condition is accepted or the context expires, whichever comes first. try := 0 for { if delay > maxDelay { delay = maxDelay } // Try the acceptance condition; if it returns true, or an error, we are done. b, data, err := acceptor.Accept(try, delay) if b || err != nil { return b, data, err } // Wait for delay or timeout. select { case <-timeAfter(delay): // Continue on. case <-ctx.Done(): return false, nil, nil } delay = time.Duration(float64(delay) * backoff) try++ } } // Until waits until the acceptor accepts the current condition, or the context expires, whichever comes first. A // return boolean of true means the acceptor eventually accepted; a non-nil error means the acceptor returned an error. // If an acceptor accepts a condition after the context has expired, we ignore the expiration and return the condition. // // This uses [Retryer] with the default settings. func Until(ctx context.Context, acceptor Acceptor) (bool, interface{}, error) { return (&Retryer{}).Until(ctx, acceptor) } // UntilDeadline creates a child context with the given deadline, and then invokes the above Until function. func UntilDeadline(ctx context.Context, acceptor Acceptor, deadline time.Time) (bool, interface{}, error) { var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, deadline) b, data, err := Until(ctx, acceptor) cancel() return b, data, err } // UntilTimeout creates a child context with the given timeout, and then invokes the above Until function. func UntilTimeout(ctx context.Context, acceptor Acceptor, timeout time.Duration) (bool, interface{}, error) { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) b, data, err := Until(ctx, acceptor) cancel() return b, data, err }