-
-
Save pteich/c0bb58b0b7c8af7cc6a689dd0d3d26ef to your computer and use it in GitHub Desktop.
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") | |
} | |
} |
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)
}
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
@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.
@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.
/bin/true
Pretty neat 👍
Shouldn't there be a defer done()
after line 17?
Shouldn't there be a
defer done()
after line 17?
For a real world usage I would suggest this too, agreed.
This is great! Saved me time. thanks!
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().
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.
Maybe I create an updated version.
@pteich It would be great!
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
saved my day