gotimeoutgoroutinego-context

Run function every N seconds with context timeout


I have a basic question about scheduling "cancellable" goroutines.

I want to schedule a function execution, every 3 seconds.

The function can take up to 5 seconds.

In case it takes more than 2999ms I want to stop/terminate it, to avoid overlapping w/ the next one.

I'm doing it wrong:

func main() {
    fmt.Println("startProcessing")
    go startProcessing()

    time.Sleep(time.Second * 60)
    fmt.Println("endProcessing after 60s")
}

func startProcessing() {
    ticker := time.NewTicker(3 * time.Second)
    for _ = range ticker.C {
        ctx, _ := context.WithTimeout(context.Background(), (time.Second*3)-time.Millisecond)

        fmt.Println("start doSomething")
        doSomething(ctx)
    }
}

func doSomething(ctx context.Context) {
    executionTime := time.Duration(rand.Intn(5)+1) * time.Second

    for {
        select {
        case <-ctx.Done():
            fmt.Printf("timed out after %s\n", executionTime)
            return
        default:
            time.Sleep(executionTime)
            fmt.Printf("did something in %s\n", executionTime)
            return
        }
    }
}

This is my output now:

startProcessing

start doSomething

did something in 2s

start doSomething

did something in 3s

start doSomething

did something in 3s

start doSomething

did something in 5s

start doSomething

did something in 2s

...

I want to read timed out after 5s instead of did something in 5s.


Solution

  • You just need to put the time.Sleep(executionTime) outside the select and there is no need for the for loop. I think this is somehow what you want but beware that it's not good practice. So take a look at the warning below.

    func doSomething(ctx context.Context) {
        executionTime := time.Duration(rand.Intn(5)+1) * time.Second
        processed := make(chan int)
    
        go func() {
            time.Sleep(executionTime)
            processed <- 1
        }()
    
        select {
        case <-ctx.Done():
            fmt.Printf("timed out after %s\n", executionTime)
        case <-processed:
            fmt.Printf("did something in %s\n", executionTime)
        }
    }
    

    Obs: I changed the original answer a bit. We can not interrupt a goroutine in the middle of its execution. We could delegate the long-running task to another goroutine and receive the result through a dedicated channel.

    Warning: I wouldn't recommend that if you expect the processing time to exceed the deadline because now you will have a leaking goroutine.