How can I sleep with responsive context cancelation? - go

In Go, I want to time.Sleep for some time (e.g. waiting between retries), but want to return quickly if the context gets canceled (not just from a deadline, but also manually).
What is the right or best way to do that? Thanks!

You can use select to acheive this:
package main
import (
"fmt"
"time"
"context"
)
func main() {
fmt.Println("Hello, playground")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func(){
t := time.Now()
select{
case <-ctx.Done(): //context cancelled
case <-time.After(2 * time.Second): //timeout
}
fmt.Printf("here after: %v\n", time.Since(t))
}()
cancel() //cancel manually, comment out to see timeout kick in
time.Sleep(3 * time.Second)
fmt.Println("done")
}
Here is the Go-playground link

Here is a sleepContext function that you can use instead of time.Sleep:
func sleepContext(ctx context.Context, delay time.Duration) {
select {
case <-ctx.Done():
case <-time.After(delay):
}
}
And some sample usage (full runnable code on the Go Playground):
func main() {
ctx := context.Background()
fmt.Println(time.Now())
sleepContext(ctx, 1*time.Second)
fmt.Println(time.Now())
ctxTimeout, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
sleepContext(ctxTimeout, 1*time.Second)
cancel()
fmt.Println(time.Now())
}

You can use select as others have mentioned; however, the other answers have a bug since timer.After() will leak memory if not cleaned up.
func SleepWithContext(ctx context.Context, d time.Duration) {
timer := time.NewTimer(d)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
}
}

The time.After() function has this problem:
The underlying Timer is not recovered by the garbage collector until
the timer fires. If efficiency is a concern, use NewTimer instead and
call Timer.Stop if the timer is no longer needed.
It is better to use a Timer object and call Stop():
// Delay returns nil after the specified duration or error if interrupted.
func Delay(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return fmt.Errorf("Interrupted")
case <-t.C:
}
return nil
}

I managed to do something similar by combining a CancelContext with a TimeoutContext...
Here is the sample code:
cancelCtx, cancel := context.WithCancel(context.Background())
defer cancel()
// The program "sleeps" for 5 seconds.
timeoutCtx, _ := context.WithTimeout(cancelCtx, 5*time.Second)
select {
case <-timeoutCtx.Done():
if cancelCtx.Err() != nil {
log.Printf("Context cancelled")
}
}
In this repo you can find the complete usage of the above code. Sorry for my short answer, I didn't turned on the computer yet, and is not that easy to answer from the phone...

Related

terminating blocking goroutines with errgroup

I have two tasks that are running in go routines. I am using errgroup. I am not sure how to use the errgroup.WithContext correctly.
In the following code, task1 is returning the error and I would like to terminate task2 (long running) when that happens. Please note that in this example time.sleep is added just to simulate my problem. In reality task1 and task2 are doing real work and does not have any sleep call.
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"time"
)
func task1(ctx context.Context) error {
time.Sleep(5 * time.Second)
fmt.Println("first finished, pretend error happened")
return ctx.Err()
}
func task2(ctx context.Context) error {
select {
case <-ctx.Done():
fmt.Println("task 1 is finished with error")
return ctx.Err()
default:
fmt.Println("second started")
time.Sleep(50 * time.Second)
fmt.Println("second finished")
}
return nil
}
func test() (err error) {
ctx := context.Background()
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
return task1(gctx)
})
g.Go(func() error {
return task2(gctx)
})
err = g.Wait()
if err != nil {
fmt.Println("wait done")
}
return err
}
func main() {
fmt.Println("main")
err := test()
if err != nil {
fmt.Println("main err")
fmt.Println(err.Error())
}
}
It's up to your tasks to handle context cancellation properly and not time.Sleep inside a select.
As stated in errgroup documentation:
WithContext returns a new Group and an associated Context derived from ctx.
The derived Context is canceled the first time a function passed to Go returns a non-nil error or the first time Wait returns, whichever occurs first.
You are using error group right, but your context handling needs a refactor.
Here is a refacor of your task 2:
func task2(ctx context.Context) error {
errCh := make(chan bool)
go func() {
time.Sleep(50 * time.Second)
errCh <- true
}()
select {
case <-ctx.Done():
return fmt.Errorf("context done: %w", ctx.Err())
case <-errCh:
return errors.New("task 2 failed")
}
}
With such select, you wait for the first channel to emit. In this case, it is the context expiration, unless you modify time sleep to be lower. Example playground.

goroutine not seeing context cancel?

I have two goroutines running at the same time.
At some point, I want my program to exit gracefully so I use the cancel() func to notify my goroutines that they need to be stopped, but only one of the two receive the message.
here is my main (simplified):
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
err := eng.Watcher(ctx, wg)
if err != nil {
cancel()
}
}()
go func() {
err := eng.Suspender(ctx, wg)
if err != nil {
cancel()
}
}()
<-done // wait for SIGINT / SIGTERM
log.Print("receive shutdown")
cancel()
wg.Wait()
log.Print("controller exited properly")
The Suspender goroutine exist successfully (here is the code):
package main
import (
"context"
"sync"
"time"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
)
func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
contextLogger := eng.logger.WithFields(log.Fields{
"go-routine": "Suspender",
})
contextLogger.Info("starting Suspender goroutine")
now := time.Now().In(eng.loc)
for {
select {
case n := <-eng.Wl:
//dostuff
case <-ctx.Done():
// The context is over, stop processing results
contextLogger.Infof("goroutine Suspender canceled by context")
return nil
}
}
}
and here is the func that is not receiving the context cancellation:
package main
import (
"context"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
contextLogger := eng.logger.WithFields(log.Fields{
"go-routine": "Watcher",
"uptime-schedule": eng.upTimeSchedule,
})
contextLogger.Info("starting Watcher goroutine")
ticker := time.NewTicker(time.Second * 30)
for {
select {
case <-ctx.Done():
contextLogger.Infof("goroutine watcher canceled by context")
log.Printf("toto")
return nil
case <-ticker.C:
//dostuff
}
}
}
}
Can you please help me ?
Thanks :)
Did you try it with an errgroup? It has context cancellation baked in:
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
// "golang.org/x/sync/errgroup"
wg, ctx := errgroup.WithContext(ctx)
wg.Go(func() error {
return eng.Watcher(ctx, wg)
})
wg.Go(func() error {
return eng.Suspender(ctx, wg)
})
wg.Go(func() error {
defer cancel()
<-done
return nil
})
err := wg.Wait()
if err != nil {
log.Print(err)
}
log.Print("receive shutdown")
log.Print("controller exited properly")
On the surface the code looks good. The only thing I can think is that it's busy in "dostuff". It can be tricky to step through timing related code in the debugger so try adding some logging:
case <-ticker.C:
log.Println("doing stuff")
//dostuff
log.Println("done stuff")
(I also assume you are calling wg.Done() in your go-routines somewhere though if they are missing that would not be the cause of the problem you describe.)
The code in Suspender and in Watcher doesn't decrement the waitgroup counter through the Done() method call - the reason behind the infinite execution.
And to be honest it's quite normal to forget such small things. That's why as a standard general practice in Go, it is suggested to use defer and handle things that are critical (and should be handled inside the function/method ) at the very beginning.
The updated implementation might look like
func (eng *Engine) Suspender(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
// ------------------------------------
func (eng *Engine) Watcher(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
contextLogger := eng.logger.WithFields(log.Fields{
Also, another suggestion, looking at the main routine, it is always suggested to pass context by value to any go-routine or method calls (lambda) that are being invoked.
This approach saves developers from a lot of program-related bugs that can't be noticed very easily.
go func(ctx context.Context) {
err := eng.Watcher(ctx, wg)
if err != nil {
cancel()
}
}(ctx)
Edit-1: (the exact solution)
Try passing the context using the value in the go routines as I mentioned earlier. Otherwise, both of the go routine will use a single context (because you are referencing it) and only one ctx.Done() will be fired.
By passing ctx as a value 2 separate child contexts are created in Go. And while closing parent with cancel() - both children independently fires ctx.Done().

How can I completely terminate the running go func() when ctx times out?

When I want ctx timeout, what should I do to completely terminate the method that is executing longRunningCalculation()?
package main
import (
"context"
"log"
"time"
)
func longRunningCalculation(timeCost int) chan string {
result := make(chan string)
go func() {
time.Sleep(time.Second * (time.Duration(timeCost)))
log.Println("Still doing other things...") //Even if it times out, this goroutine is still doing other tasks.
result <- "Done"
log.Println(timeCost)
}()
return result
}
func jobWithTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-ctx.Done():
log.Println(ctx.Err())
return
case result := <-longRunningCalculation(3):
log.Println(result)
}
}
func main() {
jobWithTimeout()
time.Sleep(time.Second * 5)
}
What did you expect to see?
2019/09/25 11:00:16 context deadline exceeded
What did you see instead?
2019/09/25 11:00:16 context deadline exceeded
2019/09/25 11:00:17 Still doing other things...
To stop the goroutine started by longRunningCalculation when the caller's context times out, you need to pass ctx into longRunningCalculation and explicitly handle the context timing out, the same way you do in jobWithTimeout
Doing things that way also means instead of calling time.Sleep, that time.Tick will be a better choice, so both timers are running at the same time. Like so:
package main
import (
"context"
"log"
"time"
)
func longRunningCalculation(ctx context.Context, timeCost int) chan string {
result := make(chan string)
go func() {
calcDone := time.Tick(time.Second * time.Duration(timeCost))
log.Printf("entering select (longRunningCalculation)")
select {
case <-ctx.Done():
result <- "Caller timed out"
return
case <-calcDone:
log.Println("Still doing other things...") //Even if it times out, this goroutine is still doing other tasks.
result <- "Done"
}
log.Println(timeCost)
}()
return result
}
func jobWithTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
result := longRunningCalculation(ctx, 3)
log.Printf("entering select (jobWithTimeout)")
select {
case <-ctx.Done():
log.Println(ctx.Err())
return
case res := <-result:
log.Println(res)
}
}
func main() {
jobWithTimeout()
}

Go app that relies on a poller to always be running, advice on ensuring its stability

So my application relies on a goroutine that polls every x seconds.
func main() {
// ...
go p.StartPoller();
}
What are some tips to make sure this poller is always running?
I'm just weary of things maybe because I don't understand fully the concept of error trapping in go. Since errors are values, and assuming I don't or any of the libraries I use call panic(), and no null pointer references or array out of bounds any code inside of this goroutine should not crash the goroutine correct?
func (p *Poller) StartPoller() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
// code here
}
}
You are right, the code you posted should never panic and hence "crash" the goroutine.
As a best practice, to ensure the // code here also doesn't do that, "wrap" it in a function (anonymous or named one), and use recover() (deferred!) in that. This will ensure that the polling task will also never "crash" the polling scheduler.
Something like this:
func (p *Poller) StartPoller() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
<-ticker.C
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered: %v", r)
}
}()
// code here
// If this would panic, it will be recovered...
}()
}
}
And even if the poller is to be always running, I would still add a "shutdown" channel to it, giving the possibility of graceful termination:
func (p *Poller) StartPoller(shutdown <-chan struct{}) {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
case <-shutdown:
return
}
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered: %v", r)
}
}()
// code here
// If this would panic, it will be recovered...
}()
}
}

context cancel does not exit

Expected: To be done after approx. 2 seconds
Actual: Runs indefinitely.
Don't understand what could be causing it to run indefinitely.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for i := range generator(ctx) {
select {
case <-time.After(2 * time.Second):
cancel()
return
default:
fmt.Println(i)
}
}
}
func generator(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
count := 0
for {
select {
case <-ctx.Done():
return
case ch <- count:
count++
}
}
}()
return ch
}
The main issue is that your channel returned from generator(ctx) emits values almost as fast as you can read them.
The channel created by time.After(2 * time.Second) is discarded almost immediately, and you create a new timeout channel every iteration through the generator.
If you make one small change; create the timeout channel outside the loop, and then put it in the select clause you'll see it begin to work.
timeout := time.After(2 * time.Second)
for i := range generator(ctx) {
select {
case <-timeout:
cancel()
return
default:
fmt.Println(i)
}
}
https://play.golang.org/p/zb3wn5FJuK

Resources