Codebase list golang-gomega / 618a133
Introduce TryAgainAfter Onsi Fakhouri 1 year, 6 months ago
6 changed file(s) with 178 addition(s) and 44 deletion(s). Raw diff Collapse all Expand all
552552 If a matcher returns `StopTrying` for `error`, or calls `StopTrying(...).Now()`, `Eventually` and `Consistently` will stop polling and fail: `StopTrying` **always** signifies a failure.
553553
554554 > Note: An alternative mechanism for having matchers bail out early is documented in the [custom matchers section below](#aborting-eventuallyconsistently). This mechanism, which entails implementing a `MatchMayChangeIntheFuture(<actual>) bool` method, allows matchers to signify that no future change is possible out-of-band of the call to the matcher.
555
556 ### Changing the Polling Interval Dynamically
557
558 You typically configure the polling interval for `Eventually` and `Consistently` using the `.WithPolling()` or `.ProbeEvery()` chaining methods. Sometimes, however, a polled function or matcher might want to signal that a service is unavailable but should be tried again after a certain duration.
559
560 You can signal this to both `Eventually` and `Consistently` using `TryAgainAfter(<duration>)`. This error-signal operates like `StopTrying()`: you can return `TryAgainAfter(<duration>)` as an error or throw a panic via `TryAgainAfter(<duration>).Now()`. In either case, both `Eventually` and `Consistently` will wait for the specified duration before trying again.
561
562 If a timeout occurs after the `TryAgainAfter` signal is sent but _before_ the next poll occurs both `Eventually` _and_ `Consistently` will always fail and print out the content of `TryAgainAfter`. The default message is `"told to try again after <duration>"` however, as with `StopTrying` you can use `.Wrap()` and `.Attach()` to wrap an error and attach additional objects to include in the message, respectively.
555563
556564 ### Modifying Default Intervals
557565
405405 }
406406
407407 /*
408 StopTrying can be used to signal to Eventually and Consistently that the polled function will not change
409 and that they should stop trying. In the case of Eventually, if a match does not occur in this, final, iteration then a failure will result. In the case of Consistently, as long as this last iteration satisfies the match, the assertion will be considered successful.
408 StopTrying can be used to signal to Eventually and Consistentlythat they should abort and stop trying. This always results in a failure of the assertion - and the failure message is the content of the StopTrying signal.
410409
411410 You can send the StopTrying signal by either returning StopTrying("message") as an error from your passed-in function _or_ by calling StopTrying("message").Now() to trigger a panic and end execution.
412411
413 StopTrying has the same signature as `fmt.Errorf`, and you can use `%w` to wrap StopTrying around another error. Doing so signals to Gomega that the assertion should (a) stop trying _and_ that (b) an underlying error has occurred. This, in turn, implies that no match should be attempted as the returned values cannot be trusted.
412 You can also wrap StopTrying around an error with `StopTrying("message").Wrap(err)` and can attach additional objects via `StopTrying("message").Attach("description", object). When rendered, the signal will include the wrapped error and any attached objects rendered using Gomega's default formatting.
414413
415414 Here are a couple of examples. This is how you might use StopTrying() as an error to signal that Eventually should stop:
416415
417416 playerIndex, numPlayers := 0, 11
418417 Eventually(func() (string, error) {
419 name := client.FetchPlayer(playerIndex)
420 playerIndex += 1
421 if playerIndex == numPlayers {
422 return name, StopTrying("No more players left")
423 } else {
424 return name, nil
425 }
418 if playerIndex == numPlayers {
419 return "", StopTrying("no more players left")
420 }
421 name := client.FetchPlayer(playerIndex)
422 playerIndex += 1
423 return name, nil
426424 }).Should(Equal("Patrick Mahomes"))
427
428 note that the final `name` returned alongside `StopTrying()` will be processed.
429425
430426 And here's an example where `StopTrying().Now()` is called to halt execution immediately:
431427
432428 Eventually(func() []string {
433429 names, err := client.FetchAllPlayers()
434430 if err == client.IRRECOVERABLE_ERROR {
435 StopTrying("Irrecoverable error occurred").Now()
431 StopTrying("Irrecoverable error occurred").Wrap(err).Now()
436432 }
437433 return names
438434 }).Should(ContainElement("Patrick Mahomes"))
439435 */
440436 var StopTrying = internal.StopTrying
441437
438 /*
439 TryAgainAfter(<duration>) allows you to adjust the polling interval for the _next_ iteration of `Eventually` or `Consistently`. Like `StopTrying` you can either return `TryAgainAfter` as an error or trigger it immedieately with `.Now()`
440
441 When `TryAgainAfter(<duration>` is triggered `Eventually` and `Consistently` will wait for that duration. If a timeout occurs before the next poll is triggered both `Eventually` and `Consistently` will always fail with the content of the TryAgainAfter message. As with StopTrying you can `.Wrap()` and error and `.Attach()` additional objects to `TryAgainAfter`.
442 */
443 var TryAgainAfter = internal.TryAgainAfter
444
442445 // SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses.
443446 func SetDefaultEventuallyTimeout(t time.Duration) {
444447 Default.SetDefaultEventuallyTimeout(t)
349349 defer lock.Unlock()
350350 message := ""
351351 if err != nil {
352 //TODO - formatting for TryAgainAfter?
352353 if asyncSignal, ok := AsAsyncSignalError(err); ok && asyncSignal.IsStopTrying() {
353354 message = err.Error()
354355 for _, attachment := range asyncSignal.Attachments {
384385 }
385386
386387 for {
387 if asyncSignal, ok := AsAsyncSignalError(err); ok && asyncSignal.IsStopTrying() {
388 fail("Told to stop trying")
389 return false
388 var nextPoll <-chan time.Time = nil
389 var isTryAgainAfterError = false
390
391 if asyncSignal, ok := AsAsyncSignalError(err); ok {
392 if asyncSignal.IsStopTrying() {
393 fail("Told to stop trying")
394 return false
395 }
396 if asyncSignal.IsTryAgainAfter() {
397 nextPoll = time.After(asyncSignal.TryAgainDuration())
398 isTryAgainAfterError = true
399 }
390400 }
391401
392402 if err == nil && matches == desiredMatch {
393403 if assertion.asyncType == AsyncAssertionTypeEventually {
394404 return true
395405 }
396 } else {
406 } else if !isTryAgainAfterError {
397407 if assertion.asyncType == AsyncAssertionTypeConsistently {
398408 fail("Failed")
399409 return false
409419 }
410420 }
411421
422 if nextPoll == nil {
423 nextPoll = assertion.afterPolling()
424 }
425
412426 select {
413 case <-assertion.afterPolling():
427 case <-nextPoll:
414428 v, e := pollActual()
415429 lock.Lock()
416430 value, err = v, e
430444 fail("Timed out")
431445 return false
432446 } else {
447 if isTryAgainAfterError {
448 fail("Timed out while waiting on TryAgainAfter")
449 return false
450 }
433451 return true
434452 }
435453 }
12311231
12321232 })
12331233 })
1234
12351234 })
12361235
12371236 Describe("The StopTrying signal - when sent by the matcher", func() {
13121311 Eventually(nil).Should(QuickMatcher(func(actual any) (bool, error) {
13131312 panic("welp")
13141313 }))
1314 })
1315 })
1316 })
1317
1318 Describe("dynamically adjusting the polling interval", func() {
1319 var i int
1320 var times []time.Duration
1321 var t time.Time
1322
1323 BeforeEach(func() {
1324 i = 0
1325 times = []time.Duration{}
1326 t = time.Now()
1327 })
1328
1329 Context("and the assertion eventually succeeds", func() {
1330 It("adjusts the timing of the next iteration", func() {
1331 Eventually(func() error {
1332 times = append(times, time.Since(t))
1333 t = time.Now()
1334 i += 1
1335 if i < 3 {
1336 return errors.New("stay on target")
1337 }
1338 if i == 3 {
1339 return TryAgainAfter(time.Millisecond * 200)
1340 }
1341 if i == 4 {
1342 return errors.New("you've switched off your targeting computer")
1343 }
1344 if i == 5 {
1345 TryAgainAfter(time.Millisecond * 100).Now()
1346 }
1347 if i == 6 {
1348 return errors.New("stay on target")
1349 }
1350 return nil
1351 }).ProbeEvery(time.Millisecond * 10).Should(Succeed())
1352 Ω(i).Should(Equal(7))
1353 Ω(times).Should(HaveLen(7))
1354 Ω(times[0]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1355 Ω(times[1]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1356 Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1357 Ω(times[3]).Should(BeNumerically("~", time.Millisecond*200, time.Millisecond*200))
1358 Ω(times[4]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1359 Ω(times[5]).Should(BeNumerically("~", time.Millisecond*100, time.Millisecond*100))
1360 Ω(times[6]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1361 })
1362 })
1363
1364 Context("and the assertion timesout while waiting", func() {
1365 It("fails with a timeout and emits the try again after error", func() {
1366 ig.G.Eventually(func() (int, error) {
1367 times = append(times, time.Since(t))
1368 t = time.Now()
1369 i += 1
1370 if i < 3 {
1371 return i, nil
1372 }
1373 if i == 3 {
1374 return i, TryAgainAfter(time.Second * 10).Wrap(errors.New("bam"))
1375 }
1376 return i, nil
1377 }).ProbeEvery(time.Millisecond * 10).WithTimeout(time.Millisecond * 300).Should(Equal(4))
1378 Ω(i).Should(Equal(3))
1379 Ω(times).Should(HaveLen(3))
1380 Ω(times[0]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1381 Ω(times[1]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1382 Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1383
1384 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
1385 Ω(ig.FailureMessage).Should(ContainSubstring("Error: told to try again after 10s: bam"))
1386 })
1387 })
1388
1389 Context("when used with Consistently", func() {
1390 It("doesn't immediately count as a failure and adjusts the timing of the next iteration", func() {
1391 Consistently(func() (int, error) {
1392 times = append(times, time.Since(t))
1393 t = time.Now()
1394 i += 1
1395 if i == 3 {
1396 return i, TryAgainAfter(time.Millisecond * 200)
1397 }
1398 return i, nil
1399 }).ProbeEvery(time.Millisecond * 10).WithTimeout(time.Millisecond * 500).Should(BeNumerically("<", 1000))
1400 Ω(times[0]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1401 Ω(times[1]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1402 Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1403 Ω(times[3]).Should(BeNumerically("~", time.Millisecond*200, time.Millisecond*200))
1404 Ω(times[4]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1405 })
1406
1407 It("doesn count as a failure if a timeout occurs during the try again after window", func() {
1408 ig.G.Consistently(func() (int, error) {
1409 times = append(times, time.Since(t))
1410 t = time.Now()
1411 i += 1
1412 if i == 3 {
1413 return i, TryAgainAfter(time.Second * 10).Wrap(errors.New("bam"))
1414 }
1415 return i, nil
1416 }).ProbeEvery(time.Millisecond * 10).WithTimeout(time.Millisecond * 300).Should(BeNumerically("<", 1000))
1417 Ω(times[0]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1418 Ω(times[1]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1419 Ω(times[2]).Should(BeNumerically("~", time.Millisecond*10, time.Millisecond*10))
1420 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out while waiting on TryAgainAfter after"))
1421 Ω(ig.FailureMessage).Should(ContainSubstring("Error: told to try again after 10s: bam"))
13151422 })
13161423 })
13171424 })
22 import (
33 "errors"
44 "time"
5 "fmt"
56 )
67
78 type AsyncSignalErrorType int
1112 AsyncSignalErrorTypeTryAgainAfter
1213 )
1314
14 type StopTryingError interface {
15 type AsyncSignalError interface {
1516 error
16 Wrap(err error) StopTryingError
17 Attach(description string, obj any) StopTryingError
17 Wrap(err error) AsyncSignalError
18 Attach(description string, obj any) AsyncSignalError
1819 Now()
1920 }
2021
21 type TryAgainAfterError interface {
22 error
23 Now()
24 }
2522
26 var StopTrying = func(message string) StopTryingError {
27 return &AsyncSignalError{
23 var StopTrying = func(message string) AsyncSignalError {
24 return &AsyncSignalErrorImpl{
2825 message: message,
2926 asyncSignalErrorType: AsyncSignalErrorTypeStopTrying,
3027 }
3128 }
3229
33 var TryAgainAfter = func(duration time.Duration) TryAgainAfterError {
34 return &AsyncSignalError{
30 var TryAgainAfter = func(duration time.Duration) AsyncSignalError {
31 return &AsyncSignalErrorImpl{
32 message: fmt.Sprintf("told to try again after %s", duration),
3533 duration: duration,
3634 asyncSignalErrorType: AsyncSignalErrorTypeTryAgainAfter,
3735 }
4240 Object any
4341 }
4442
45 type AsyncSignalError struct {
43 type AsyncSignalErrorImpl struct {
4644 message string
4745 wrappedErr error
4846 asyncSignalErrorType AsyncSignalErrorType
5048 Attachments []AsyncSignalErrorAttachment
5149 }
5250
53 func (s *AsyncSignalError) Wrap(err error) StopTryingError {
51 func (s *AsyncSignalErrorImpl) Wrap(err error) AsyncSignalError {
5452 s.wrappedErr = err
5553 return s
5654 }
5755
58 func (s *AsyncSignalError) Attach(description string, obj any) StopTryingError {
56 func (s *AsyncSignalErrorImpl) Attach(description string, obj any) AsyncSignalError {
5957 s.Attachments = append(s.Attachments, AsyncSignalErrorAttachment{description, obj})
6058 return s
6159 }
6260
63 func (s *AsyncSignalError) Error() string {
61 func (s *AsyncSignalErrorImpl) Error() string {
6462 if s.wrappedErr == nil {
6563 return s.message
6664 } else {
6866 }
6967 }
7068
71 func (s *AsyncSignalError) Unwrap() error {
69 func (s *AsyncSignalErrorImpl) Unwrap() error {
7270 if s == nil {
7371 return nil
7472 }
7573 return s.wrappedErr
7674 }
7775
78 func (s *AsyncSignalError) Now() {
76 func (s *AsyncSignalErrorImpl) Now() {
7977 panic(s)
8078 }
8179
82 func (s *AsyncSignalError) IsStopTrying() bool {
80 func (s *AsyncSignalErrorImpl) IsStopTrying() bool {
8381 return s.asyncSignalErrorType == AsyncSignalErrorTypeStopTrying
8482 }
8583
86 func (s *AsyncSignalError) IsTryAgainAfter() bool {
84 func (s *AsyncSignalErrorImpl) IsTryAgainAfter() bool {
8785 return s.asyncSignalErrorType == AsyncSignalErrorTypeTryAgainAfter
8886 }
8987
90 func (s *AsyncSignalError) TryAgainDuration() time.Duration {
88 func (s *AsyncSignalErrorImpl) TryAgainDuration() time.Duration {
9189 return s.duration
9290 }
9391
94 func AsAsyncSignalError(actual interface{}) (*AsyncSignalError, bool) {
92 func AsAsyncSignalError(actual interface{}) (*AsyncSignalErrorImpl, bool) {
9593 if actual == nil {
9694 return nil, false
9795 }
9896 if actualErr, ok := actual.(error); ok {
99 var target *AsyncSignalError
97 var target *AsyncSignalErrorImpl
10098 if errors.As(actualErr, &target) {
10199 return target, true
102100 } else {
1515 st := StopTrying("I've tried 17 times - give up!")
1616 Ω(st.Error()).Should(Equal("I've tried 17 times - give up!"))
1717 Ω(errors.Unwrap(st)).Should(BeNil())
18 Ω(st.(*internal.AsyncSignalError).IsStopTrying()).Should(BeTrue())
18 Ω(st.(*internal.AsyncSignalErrorImpl).IsStopTrying()).Should(BeTrue())
1919 })
2020 })
2121
3131
3232 Describe("When attaching objects", func() {
3333 It("attaches them, with their descriptions", func() {
34 st := StopTrying("Welp!").Attach("Max retries attained", 17).Attach("Got this response", "FLOOP").(*internal.AsyncSignalError)
34 st := StopTrying("Welp!").Attach("Max retries attained", 17).Attach("Got this response", "FLOOP").(*internal.AsyncSignalErrorImpl)
3535 Ω(st.Attachments).Should(HaveLen(2))
3636 Ω(st.Attachments[0]).Should(Equal(internal.AsyncSignalErrorAttachment{"Max retries attained", 17}))
3737 Ω(st.Attachments[1]).Should(Equal(internal.AsyncSignalErrorAttachment{"Got this response", "FLOOP"}))
4040
4141 Describe("when invoking Now()", func() {
4242 It("should panic with itself", func() {
43 st := StopTrying("bam").(*internal.AsyncSignalError)
43 st := StopTrying("bam").(*internal.AsyncSignalErrorImpl)
4444 Ω(st.Now).Should(PanicWith(st))
4545 })
4646 })