github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/oracleobjectstorage/waiter.go (about) 1 //go:build !plan9 && !solaris && !js 2 3 package oracleobjectstorage 4 5 import ( 6 "context" 7 "fmt" 8 "strings" 9 "time" 10 11 "github.com/rclone/rclone/fs" 12 ) 13 14 var refreshGracePeriod = 30 * time.Second 15 16 // StateRefreshFunc is a function type used for StateChangeConf that is 17 // responsible for refreshing the item being watched for a state change. 18 // 19 // It returns three results. `result` is any object that will be returned 20 // as the final object after waiting for state change. This allows you to 21 // return the final updated object, for example an EC2 instance after refreshing 22 // it. A nil result represents not found. 23 // 24 // `state` is the latest state of that object. And `err` is any error that 25 // may have happened while refreshing the state. 26 type StateRefreshFunc func() (result interface{}, state string, err error) 27 28 // StateChangeConf is the configuration struct used for `WaitForState`. 29 type StateChangeConf struct { 30 Delay time.Duration // Wait this time before starting checks 31 Pending []string // States that are "allowed" and will continue trying 32 Refresh StateRefreshFunc // Refreshes the current state 33 Target []string // Target state 34 Timeout time.Duration // The amount of time to wait before timeout 35 MinTimeout time.Duration // Smallest time to wait before refreshes 36 PollInterval time.Duration // Override MinTimeout/backoff and only poll this often 37 NotFoundChecks int // Number of times to allow not found (nil result from Refresh) 38 39 // This is to work around inconsistent APIs 40 ContinuousTargetOccurrence int // Number of times the Target state has to occur continuously 41 } 42 43 // WaitForStateContext watches an object and waits for it to achieve the state 44 // specified in the configuration using the specified Refresh() func, 45 // waiting the number of seconds specified in the timeout configuration. 46 // 47 // If the Refresh function returns an error, exit immediately with that error. 48 // 49 // If the Refresh function returns a state other than the Target state or one 50 // listed in Pending, return immediately with an error. 51 // 52 // If the Timeout is exceeded before reaching the Target state, return an 53 // error. 54 // 55 // Otherwise, the result is the result of the first call to the Refresh function to 56 // reach the target state. 57 // 58 // Cancellation from the passed in context will cancel the refresh loop 59 func (conf *StateChangeConf) WaitForStateContext(ctx context.Context, entityType string) (interface{}, error) { 60 // fs.Debugf(entityType, "Waiting for state to become: %s", conf.Target) 61 62 notfoundTick := 0 63 targetOccurrence := 0 64 65 // Set a default for times to check for not found 66 if conf.NotFoundChecks == 0 { 67 conf.NotFoundChecks = 20 68 } 69 70 if conf.ContinuousTargetOccurrence == 0 { 71 conf.ContinuousTargetOccurrence = 1 72 } 73 74 type Result struct { 75 Result interface{} 76 State string 77 Error error 78 Done bool 79 } 80 81 // Read every result from the refresh loop, waiting for a positive result.Done. 82 resCh := make(chan Result, 1) 83 // cancellation channel for the refresh loop 84 cancelCh := make(chan struct{}) 85 86 result := Result{} 87 88 go func() { 89 defer close(resCh) 90 91 select { 92 case <-time.After(conf.Delay): 93 case <-cancelCh: 94 return 95 } 96 97 // start with 0 delay for the first loop 98 var wait time.Duration 99 100 for { 101 // store the last result 102 resCh <- result 103 104 // wait and watch for cancellation 105 select { 106 case <-cancelCh: 107 return 108 case <-time.After(wait): 109 // first round had no wait 110 if wait == 0 { 111 wait = 100 * time.Millisecond 112 } 113 } 114 115 res, currentState, err := conf.Refresh() 116 result = Result{ 117 Result: res, 118 State: currentState, 119 Error: err, 120 } 121 122 if err != nil { 123 resCh <- result 124 return 125 } 126 127 // If we're waiting for the absence of a thing, then return 128 if res == nil && len(conf.Target) == 0 { 129 targetOccurrence++ 130 if conf.ContinuousTargetOccurrence == targetOccurrence { 131 result.Done = true 132 resCh <- result 133 return 134 } 135 continue 136 } 137 138 if res == nil { 139 // If we didn't find the resource, check if we have been 140 // not finding it for a while, and if so, report an error. 141 notfoundTick++ 142 if notfoundTick > conf.NotFoundChecks { 143 result.Error = &NotFoundError{ 144 LastError: err, 145 Retries: notfoundTick, 146 } 147 resCh <- result 148 return 149 } 150 } else { 151 // Reset the counter for when a resource isn't found 152 notfoundTick = 0 153 found := false 154 155 for _, allowed := range conf.Target { 156 if currentState == allowed { 157 found = true 158 targetOccurrence++ 159 if conf.ContinuousTargetOccurrence == targetOccurrence { 160 result.Done = true 161 resCh <- result 162 return 163 } 164 continue 165 } 166 } 167 168 for _, allowed := range conf.Pending { 169 if currentState == allowed { 170 found = true 171 targetOccurrence = 0 172 break 173 } 174 } 175 176 if !found && len(conf.Pending) > 0 { 177 result.Error = &UnexpectedStateError{ 178 LastError: err, 179 State: result.State, 180 ExpectedState: conf.Target, 181 } 182 resCh <- result 183 return 184 } 185 } 186 187 // Wait between refreshes using exponential backoff, except when 188 // waiting for the target state to reoccur. 189 if targetOccurrence == 0 { 190 wait *= 2 191 } 192 193 // If a poll interval has been specified, choose that interval. 194 // Otherwise, bound the default value. 195 if conf.PollInterval > 0 && conf.PollInterval < 180*time.Second { 196 wait = conf.PollInterval 197 } else { 198 if wait < conf.MinTimeout { 199 wait = conf.MinTimeout 200 } else if wait > 10*time.Second { 201 wait = 10 * time.Second 202 } 203 } 204 205 // fs.Debugf(entityType, "[TRACE] Waiting %s before next try", wait) 206 } 207 }() 208 209 // store the last value result from the refresh loop 210 lastResult := Result{} 211 212 timeout := time.After(conf.Timeout) 213 for { 214 select { 215 case r, ok := <-resCh: 216 // channel closed, so return the last result 217 if !ok { 218 return lastResult.Result, lastResult.Error 219 } 220 221 // we reached the intended state 222 if r.Done { 223 return r.Result, r.Error 224 } 225 226 // still waiting, store the last result 227 lastResult = r 228 case <-ctx.Done(): 229 close(cancelCh) 230 return nil, ctx.Err() 231 case <-timeout: 232 // fs.Debugf(entityType, "[WARN] WaitForState timeout after %s", conf.Timeout) 233 // fs.Debugf(entityType, "[WARN] WaitForState starting %s refresh grace period", refreshGracePeriod) 234 235 // cancel the goroutine and start our grace period timer 236 close(cancelCh) 237 timeout := time.After(refreshGracePeriod) 238 239 // we need a for loop and a label to break on, because we may have 240 // an extra response value to read, but still want to wait for the 241 // channel to close. 242 forSelect: 243 for { 244 select { 245 case r, ok := <-resCh: 246 if r.Done { 247 // the last refresh loop reached the desired state 248 return r.Result, r.Error 249 } 250 251 if !ok { 252 // the goroutine returned 253 break forSelect 254 } 255 256 // target state not reached, save the result for the 257 // TimeoutError and wait for the channel to close 258 lastResult = r 259 case <-ctx.Done(): 260 fs.Errorf(entityType, "Context cancellation detected, abandoning grace period") 261 break forSelect 262 case <-timeout: 263 fs.Errorf(entityType, "WaitForState exceeded refresh grace period") 264 break forSelect 265 } 266 } 267 268 return nil, &TimeoutError{ 269 LastError: lastResult.Error, 270 LastState: lastResult.State, 271 Timeout: conf.Timeout, 272 ExpectedState: conf.Target, 273 } 274 } 275 } 276 } 277 278 // NotFoundError resource not found error 279 type NotFoundError struct { 280 LastError error 281 LastRequest interface{} 282 LastResponse interface{} 283 Message string 284 Retries int 285 } 286 287 func (e *NotFoundError) Error() string { 288 if e.Message != "" { 289 return e.Message 290 } 291 292 if e.Retries > 0 { 293 return fmt.Sprintf("couldn't find resource (%d retries)", e.Retries) 294 } 295 296 return "couldn't find resource" 297 } 298 299 func (e *NotFoundError) Unwrap() error { 300 return e.LastError 301 } 302 303 // UnexpectedStateError is returned when Refresh returns a state that's neither in Target nor Pending 304 type UnexpectedStateError struct { 305 LastError error 306 State string 307 ExpectedState []string 308 } 309 310 func (e *UnexpectedStateError) Error() string { 311 return fmt.Sprintf( 312 "unexpected state '%s', wanted target '%s'. last error: %s", 313 e.State, 314 strings.Join(e.ExpectedState, ", "), 315 e.LastError, 316 ) 317 } 318 319 func (e *UnexpectedStateError) Unwrap() error { 320 return e.LastError 321 } 322 323 // TimeoutError is returned when WaitForState times out 324 type TimeoutError struct { 325 LastError error 326 LastState string 327 Timeout time.Duration 328 ExpectedState []string 329 } 330 331 func (e *TimeoutError) Error() string { 332 expectedState := "resource to be gone" 333 if len(e.ExpectedState) > 0 { 334 expectedState = fmt.Sprintf("state to become '%s'", strings.Join(e.ExpectedState, ", ")) 335 } 336 337 extraInfo := make([]string, 0) 338 if e.LastState != "" { 339 extraInfo = append(extraInfo, fmt.Sprintf("last state: '%s'", e.LastState)) 340 } 341 if e.Timeout > 0 { 342 extraInfo = append(extraInfo, fmt.Sprintf("timeout: %s", e.Timeout.String())) 343 } 344 345 suffix := "" 346 if len(extraInfo) > 0 { 347 suffix = fmt.Sprintf(" (%s)", strings.Join(extraInfo, ", ")) 348 } 349 350 if e.LastError != nil { 351 return fmt.Sprintf("timeout while waiting for %s%s: %s", 352 expectedState, suffix, e.LastError) 353 } 354 355 return fmt.Sprintf("timeout while waiting for %s%s", 356 expectedState, suffix) 357 } 358 359 func (e *TimeoutError) Unwrap() error { 360 return e.LastError 361 }