2
\$\begingroup\$

I'm working on a simple waiter implementation, waiter - is a code that polls an external API until a desired response is received within a given period of time. I would like to keep it simple, but at the same time write it in the native Golang style. At the moment I come up with the following code (the actual API request is removed for brevity). I'm not entirely sure if the combination of a ticker and time.After is the right approach to achieve this, while it seems to be working as expected. I feel like there should be a common pattern for doing this.

package main

import (
    "fmt"
    "time"
)

func main() {

    ticker := time.NewTicker(1 * time.Second)
    tickerDone := make(chan bool, 1)
    timeoutDone := make(chan bool, 1)
    go func() {
        defer ticker.Stop()
        i := 1
        for {
            select {
            case <-tickerDone:
                fmt.Println("Stopping the ticker")
                timeoutDone <- true
                return
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
                if i == 3 {
                    fmt.Println("It's a 3rd tick, enought")
                    ticker.Stop()
                    tickerDone <- true
                }

                i += 1
            }
        }
    }()

    select {
    case <-timeoutDone:
        fmt.Println("Completed before the timeout")
    case <-time.After(5 * time.Second):
        fmt.Println("Timed out")
        ticker.Stop()
        tickerDone <- true
    }

}

```
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

For this example it's important to know whether calling the API blocks until a value is ready, or returns quickly with some sort of "not ready" indication.

If it's a blocking call, does it accept a context.Context argument?

If so, then all you need to do is this:

func main() {
  ctx := context.Background()
  ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
  defer cancel()

  val, err := callAPI(ctx, ...)
  if err != nil {
    // ...handle error, which can include context.DeadlineExceeded...
  }

  // ...val is the result from the API...
}

Generally speaking, context objects are the best way to implement timeouts and cancellation.

If the API is a blocking function that does not take a context argument, then I suggest wrapping it so that it does:

func myAPIWrapper(ctx context.Context, ...) (val apiValType, err error) {
  done := make(chan struct{})
  go func() {
    val, err = callAPI(...)
    close(done)
  }()

  select {
  case <-ctx.Done():
    // Context timed out or was canceled, return its error.
    return val, ctx.Err()

  case <-done:
    // API call completed.
    return val, err
  }
}

Finally, if the API is a non-blocking function that you have to call repeatedly, then I suggest wrapping that as follows (using a time.Ticker as you proposed). In all cases the idea is to present a cancelable, blocking function to main (or whatever has to call this):

func myAPIWrapper(ctx context.Context, ...) (val apiValType, err error) {
  done := make(chan struct{})
  go func() {
    defer close(done)

    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()

    for {
      // Don't wait for the ticker to fire before calling the API.
      var valReady bool
      val, valReady, err = callAPI(...)
      if err != nil || valReady {
        return
      }
      select {
      case <-ctx.Done():
        err = ctx.Err()
        return

      case <-ticker.C:
        // Do nothing here, just loop again.
      }
    }
  }()

  <-done
  return val, err
}
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.