Skip to content

Instantly share code, notes, and snippets.

@pteich
Last active May 30, 2024 02:57
Show Gist options
  • Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
Example for using go's sync.errgroup together with signal detection signal.NotifyContext to stop all running goroutines
package main
import (
"context"
"errors"
"fmt"
"os/signal"
"syscall"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
defer done()
g, gctx := errgroup.WithContext(ctx)
// just a ticker every 2s
g.Go(func() error {
ticker := time.NewTicker(2 * time.Second)
i := 0
for {
i++
if i > 10 {
return nil
}
select {
case <-ticker.C:
fmt.Println("ticker 2s ticked")
case <-gctx.Done():
fmt.Println("closing ticker 2s goroutine")
return gctx.Err()
}
}
})
// just a ticker every 1s
g.Go(func() error {
ticker := time.NewTicker(1 * time.Second)
i := 0
for {
i++
if i > 10 {
return nil
}
select {
case <-ticker.C:
fmt.Println("ticker 1s ticked")
case <-gctx.Done():
fmt.Println("closing ticker 1s goroutine")
return gctx.Err()
}
}
})
// force a stop after 15s
time.AfterFunc(15*time.Second, func() {
fmt.Println("force finished after 15s")
done()
})
// wait for all errgroup goroutines
err := g.Wait()
if err != nil {
if errors.Is(err, context.Canceled) {
fmt.Println("context was canceled")
} else {
fmt.Printf("received error: %v\n", err)
}
} else {
fmt.Println("finished clean")
}
}
@otiai10
Copy link

otiai10 commented Apr 23, 2018

saved my day

@embano1
Copy link

embano1 commented Feb 27, 2020

Prob too late :) but just to prevent any surprises:

if err := g.Wait(); err == nil || err == context.Canceled {
		fmt.Println("finished clean")
	} else {
		fmt.Printf("received error: %v", err)
	}

@gebv
Copy link

gebv commented May 16, 2020

Create a context with a deadline (remove time.AfterFunc)

ctx, done := context.WithTimeout(Ctx, time.Millisecond*100) // for example 100ms

And yes - adding check on context.Cancelled

@pteich
Copy link
Author

pteich commented May 16, 2020

@gebv At the time of writing this (years ago) I thought time.AfterFunc() would be great for showing a potentially external cancelation of the context. But I agree a context.WithTimeout fits much better.

@pteich
Copy link
Author

pteich commented May 16, 2020

@embano1 As an even later response: Depends of what you call an error :) I would think a canceled context could be still an error. But good catch to force checking for it. I changed the gist.

@embano1
Copy link

embano1 commented May 18, 2020

/bin/true

@pantelis-karamolegkos
Copy link

Pretty neat 👍

@pantelis-karamolegkos
Copy link

Shouldn't there be a defer done() after line 17?

@pteich
Copy link
Author

pteich commented May 2, 2022

Shouldn't there be a defer done() after line 17?

For a real world usage I would suggest this too, agreed.

@alifarooq0
Copy link

This is great! Saved me time. thanks!

@jyouturner
Copy link

Correct me if I am wrong. I think line 21 is better to create the go-routine not part of the errgroup.
go func() { signalChannel := make(chan os.Signal, 1) ...
This way, there is no risk of the deadlock between this goroutine and g.wait().

@pteich
Copy link
Author

pteich commented Dec 14, 2022

Correct me if I am wrong. I think line 21 is better to create the go-routine not part of the errgroup. go func() { signalChannel := make(chan os.Signal, 1) ... This way, there is no risk of the deadlock between this goroutine and g.wait().

Yes, with my knowledge today I would probably do it this way. Not only to prevent a deadlock but also b/c it simply does not belong to the errorgroup and handles a higher level controlling. In addition, we now have a signal.NotifyContext that makes creating the initial context easier. But 5 years ago when I created it, I was happy to have a working solution :)

Maybe I create an updated version.

@ceferrari
Copy link

Maybe I create an updated version.

@pteich It would be great!

@bjwschaap
Copy link

Currently there is the NotifyContext, which makes it even simpler because you don't have to create a separate goroutine for propagating the signal to context cancellation: https://pkg.go.dev/os/signal#NotifyContext

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment