github.com/jrperritt/terraform@v0.1.1-0.20170525065507-96f391dafc38/helper/resource/state.go (about)

     1  package resource
     2  
     3  import (
     4  	"log"
     5  	"time"
     6  )
     7  
     8  var refreshGracePeriod = 30 * time.Second
     9  
    10  // StateRefreshFunc is a function type used for StateChangeConf that is
    11  // responsible for refreshing the item being watched for a state change.
    12  //
    13  // It returns three results. `result` is any object that will be returned
    14  // as the final object after waiting for state change. This allows you to
    15  // return the final updated object, for example an EC2 instance after refreshing
    16  // it.
    17  //
    18  // `state` is the latest state of that object. And `err` is any error that
    19  // may have happened while refreshing the state.
    20  type StateRefreshFunc func() (result interface{}, state string, err error)
    21  
    22  // StateChangeConf is the configuration struct used for `WaitForState`.
    23  type StateChangeConf struct {
    24  	Delay          time.Duration    // Wait this time before starting checks
    25  	Pending        []string         // States that are "allowed" and will continue trying
    26  	Refresh        StateRefreshFunc // Refreshes the current state
    27  	Target         []string         // Target state
    28  	Timeout        time.Duration    // The amount of time to wait before timeout
    29  	MinTimeout     time.Duration    // Smallest time to wait before refreshes
    30  	PollInterval   time.Duration    // Override MinTimeout/backoff and only poll this often
    31  	NotFoundChecks int              // Number of times to allow not found
    32  
    33  	// This is to work around inconsistent APIs
    34  	ContinuousTargetOccurence int // Number of times the Target state has to occur continuously
    35  }
    36  
    37  // WaitForState watches an object and waits for it to achieve the state
    38  // specified in the configuration using the specified Refresh() func,
    39  // waiting the number of seconds specified in the timeout configuration.
    40  //
    41  // If the Refresh function returns a error, exit immediately with that error.
    42  //
    43  // If the Refresh function returns a state other than the Target state or one
    44  // listed in Pending, return immediately with an error.
    45  //
    46  // If the Timeout is exceeded before reaching the Target state, return an
    47  // error.
    48  //
    49  // Otherwise, result the result of the first call to the Refresh function to
    50  // reach the target state.
    51  func (conf *StateChangeConf) WaitForState() (interface{}, error) {
    52  	log.Printf("[DEBUG] Waiting for state to become: %s", conf.Target)
    53  
    54  	notfoundTick := 0
    55  	targetOccurence := 0
    56  
    57  	// Set a default for times to check for not found
    58  	if conf.NotFoundChecks == 0 {
    59  		conf.NotFoundChecks = 20
    60  	}
    61  
    62  	if conf.ContinuousTargetOccurence == 0 {
    63  		conf.ContinuousTargetOccurence = 1
    64  	}
    65  
    66  	type Result struct {
    67  		Result interface{}
    68  		State  string
    69  		Error  error
    70  		Done   bool
    71  	}
    72  
    73  	// Read every result from the refresh loop, waiting for a positive result.Done.
    74  	resCh := make(chan Result, 1)
    75  	// cancellation channel for the refresh loop
    76  	cancelCh := make(chan struct{})
    77  
    78  	result := Result{}
    79  
    80  	go func() {
    81  		defer close(resCh)
    82  
    83  		time.Sleep(conf.Delay)
    84  
    85  		// start with 0 delay for the first loop
    86  		var wait time.Duration
    87  
    88  		for {
    89  			// store the last result
    90  			resCh <- result
    91  
    92  			// wait and watch for cancellation
    93  			select {
    94  			case <-cancelCh:
    95  				return
    96  			case <-time.After(wait):
    97  				// first round had no wait
    98  				if wait == 0 {
    99  					wait = 100 * time.Millisecond
   100  				}
   101  			}
   102  
   103  			res, currentState, err := conf.Refresh()
   104  			result = Result{
   105  				Result: res,
   106  				State:  currentState,
   107  				Error:  err,
   108  			}
   109  
   110  			if err != nil {
   111  				resCh <- result
   112  				return
   113  			}
   114  
   115  			// If we're waiting for the absence of a thing, then return
   116  			if res == nil && len(conf.Target) == 0 {
   117  				targetOccurence++
   118  				if conf.ContinuousTargetOccurence == targetOccurence {
   119  					result.Done = true
   120  					resCh <- result
   121  					return
   122  				}
   123  				continue
   124  			}
   125  
   126  			if res == nil {
   127  				// If we didn't find the resource, check if we have been
   128  				// not finding it for awhile, and if so, report an error.
   129  				notfoundTick++
   130  				if notfoundTick > conf.NotFoundChecks {
   131  					result.Error = &NotFoundError{
   132  						LastError: err,
   133  						Retries:   notfoundTick,
   134  					}
   135  					resCh <- result
   136  					return
   137  				}
   138  			} else {
   139  				// Reset the counter for when a resource isn't found
   140  				notfoundTick = 0
   141  				found := false
   142  
   143  				for _, allowed := range conf.Target {
   144  					if currentState == allowed {
   145  						found = true
   146  						targetOccurence++
   147  						if conf.ContinuousTargetOccurence == targetOccurence {
   148  							result.Done = true
   149  							resCh <- result
   150  							return
   151  						}
   152  						continue
   153  					}
   154  				}
   155  
   156  				for _, allowed := range conf.Pending {
   157  					if currentState == allowed {
   158  						found = true
   159  						targetOccurence = 0
   160  						break
   161  					}
   162  				}
   163  
   164  				if !found && len(conf.Pending) > 0 {
   165  					result.Error = &UnexpectedStateError{
   166  						LastError:     err,
   167  						State:         result.State,
   168  						ExpectedState: conf.Target,
   169  					}
   170  					resCh <- result
   171  					return
   172  				}
   173  			}
   174  
   175  			// Wait between refreshes using exponential backoff, except when
   176  			// waiting for the target state to reoccur.
   177  			if targetOccurence == 0 {
   178  				wait *= 2
   179  			}
   180  
   181  			// If a poll interval has been specified, choose that interval.
   182  			// Otherwise bound the default value.
   183  			if conf.PollInterval > 0 && conf.PollInterval < 180*time.Second {
   184  				wait = conf.PollInterval
   185  			} else {
   186  				if wait < conf.MinTimeout {
   187  					wait = conf.MinTimeout
   188  				} else if wait > 10*time.Second {
   189  					wait = 10 * time.Second
   190  				}
   191  			}
   192  
   193  			log.Printf("[TRACE] Waiting %s before next try", wait)
   194  		}
   195  	}()
   196  
   197  	// store the last value result from the refresh loop
   198  	lastResult := Result{}
   199  
   200  	timeout := time.After(conf.Timeout)
   201  	for {
   202  		select {
   203  		case r, ok := <-resCh:
   204  			// channel closed, so return the last result
   205  			if !ok {
   206  				return lastResult.Result, lastResult.Error
   207  			}
   208  
   209  			// we reached the intended state
   210  			if r.Done {
   211  				return r.Result, r.Error
   212  			}
   213  
   214  			// still waiting, store the last result
   215  			lastResult = r
   216  
   217  		case <-timeout:
   218  			log.Printf("[WARN] WaitForState timeout after %s", conf.Timeout)
   219  			log.Printf("[WARN] WaitForState starting %s refresh grace period", refreshGracePeriod)
   220  
   221  			// cancel the goroutine and start our grace period timer
   222  			close(cancelCh)
   223  			timeout := time.After(refreshGracePeriod)
   224  
   225  			// we need a for loop and a label to break on, because we may have
   226  			// an extra response value to read, but still want to wait for the
   227  			// channel to close.
   228  		forSelect:
   229  			for {
   230  				select {
   231  				case r, ok := <-resCh:
   232  					if r.Done {
   233  						// the last refresh loop reached the desired state
   234  						return r.Result, r.Error
   235  					}
   236  
   237  					if !ok {
   238  						// the goroutine returned
   239  						break forSelect
   240  					}
   241  
   242  					// target state not reached, save the result for the
   243  					// TimeoutError and wait for the channel to close
   244  					lastResult = r
   245  				case <-timeout:
   246  					log.Println("[ERROR] WaitForState exceeded refresh grace period")
   247  					break forSelect
   248  				}
   249  			}
   250  
   251  			return nil, &TimeoutError{
   252  				LastError:     lastResult.Error,
   253  				LastState:     lastResult.State,
   254  				Timeout:       conf.Timeout,
   255  				ExpectedState: conf.Target,
   256  			}
   257  		}
   258  	}
   259  }