// Licensed to Pulumi Corporation ("Pulumi") under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // Pulumi licenses this file to You 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. Progress Progress // an optional progress function. Delay *time.Duration // an optional delay duration. Backoff *float64 // an optional backoff multiplier. } type Progress func(int) bool const ( DefaultDelay time.Duration = 250 * time.Millisecond // by default, delay by 250ms DefaultBackoff float64 = 2.0 // by default, backoff by 2.0 ) type Acceptance func() (bool, error) // 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. func Until(ctx context.Context, acceptor Acceptor) (bool, error) { expired := false // 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 } // If the context expires before the waiter has accepted, return. go func() { <-ctx.Done() expired = true }() // Loop until the condition is accepted, or the context expires, whichever comes first. tries := 1 for !expired { if b, err := acceptor.Accept(); b || err != nil { return b, err } if acceptor.Progress != nil && !acceptor.Progress(tries) { break // progress function asked to quit. } time.Sleep(delay) delay = time.Duration(float64(delay) * backoff) tries++ } return false, nil } // 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, error) { var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, deadline) b, err := Until(ctx, acceptor) cancel() return b, 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, error) { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, timeout) b, err := Until(ctx, acceptor) cancel() return b, err }