Search code examples
goasynchronoustimeoutpolling

Go - The most optimal way to implement a timeout


I have a service that is deployed asynchronously and I need to wait a specified amount of time for it to come online. If the specified amount of time elapses and we still aren't able to find the service, then we error out. What is the most optimal way of writing this in go? I was looking into using context.WithTimeout but not sure exactly how this would work. Thanks for the help!

func (c *Client) WaitForServiceToComeAlive(ctx context.Context, name string, timeout time.Duration) error {
    
    var mysvc *Service
    var err error

    endtime := time.Now().Add(timeout)

    for time.Now().Before(endtime) {
        mysvc, err = c.GetService(ctx, name)
        if err != nil {
            return err
        }

        if mysvc != nil {
            break
        }

        time.Sleep(time.Second * 10)
    }

    if mysvc == nil {
        return fmt.Errorf("svc %s did not register", name)
    }

    return nil
}

Solution

  • Never use time.Sleep especially if it's a long period - as it's uninterruptible. Why does this matter? If its in a goroutine and that task is not going to complete (i.e. context was canceled), then you would rather abort immediately.

    So to create a polling-wait with cancelation:

    select {
    case <-ctx.Done():               // cancel early if context is canceled
        return ctx.Err()
    case <-time.After(pollInterval): // wait for pollInterval duration
    }
    

    Put your larger timeout within the input context:

    ctx := context.TODO() // <- outer request context goes here or context.Background()
    
    // wrap context with a timeout
    ctx, cancel := context.WithTimeout(ctx, 1 * time.Minute)
    defer cancel() // avoid leaks
    
    err := c.WaitForServiceToComeAlive(ctx, "job", 10*time.Second /* poll interval */)
    

    then your service-wait function simplifies to:

    func (c *Client) WaitForServiceToComeAlive(ctx context.Context, name string, pollInterval time.Duration) error {
    
        var mysvc *Service
        var err error
    
        for {
            mysvc, err = c.GetService(name) // <- this should take a ctx too - if possible for early cancelation
            if err != nil {
                return err
            }
            if mysvc != nil {
                return nil
            }
    
            select {
            case <-ctx.Done():
                return ctx.Err()
            case <-time.After(pollInterval):
            }
        }
    
    }
    

    https://play.golang.org/p/JwH5CMyY0I2