Parent-child context cancelling order in Go - go

I want to know if there are any guarantees regarding the return order upon Context cancellation in golang.
I want to create a context with cancellation and once all the listeners are done with processing catching and reacting to "<-ctx.Done()" from this context, I want to call os.Exit safely.
A concrete example to explain the idea of what I want is following. I want to catch a signal, trigger all cancellations, and then call os.Exit().
I create a context and listen for a signal:
ctx, cancel := context.WithCancel(context.Background())
go func() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
defer signal.Stop(c)
select {
case <-c:
cancel()
}
}()
In other places I "sign up" for this request several times:
res := NewRes()
go func() {
<-ctx.Done():
res.Close()
}()
But then I want to call os.Exit at the point when all the listeners are done.
For that I plan to create either parent or child context like this:
parent, pCancel := context.WithCancel(context.Background())
child, _ := context.WithCancel(parent)
go func() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
defer signal.Stop(c)
select {
case <-c:
pCancel()
case <-child.Done():
os.Exit(0)
}
}()
Unfortunately, I did not find the documentation describing the order how context are canceled, so I cannot come up with the correct solution for now.

You have to wait all routines before exiting. Calling pCancel() doesn't mean everything will stop. I recommend to do in routine all jobs, but on the main thread to wait for os.Interrupt signal.
Check example below
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"time"
)
func main() {
parent, pCancel := context.WithCancel(context.Background())
child, _ := context.WithCancel(parent)
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
go work(wg, child)
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
defer signal.Stop(c)
select {
case <-c:
pCancel()
fmt.Println("Waiting everyone to finish...")
wg.Wait()
fmt.Println("Exiting")
os.Exit(0)
}
}
func work(wg *sync.WaitGroup, ctx context.Context) {
done := false
wg.Add(1)
for !done {
fmt.Println("Doing something...")
time.Sleep(time.Second)
select {
case <-ctx.Done():
fmt.Println("Done")
done = true
default:
}
}
wg.Done()
}
Although, It's recommended to use principle "Share Memory By Communicating".
Here is another example without using WaitGroup.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
parent, pCancel := context.WithCancel(context.Background())
child, _ := context.WithCancel(parent)
done := make(chan struct{})
jobsCount := 10
for i := 0; i < jobsCount; i++ {
go work(child, done)
}
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt)
defer signal.Stop(c)
select {
case <-c:
pCancel()
fmt.Println("Waiting everyone to finish...")
for i := 0; i < jobsCount; i++ {
<-done
}
fmt.Println("Exiting")
os.Exit(0)
}
}
func work(ctx context.Context, doneChan chan struct{}) {
done := false
for !done {
fmt.Println("Doing something...")
time.Sleep(time.Second)
select {
case <-ctx.Done():
fmt.Println("Done")
done = true
default:
}
}
doneChan <- struct{}{}
}

Related

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().

Trigger a channel inside the loop where the the channel is consumed

How to rigger a channel inside the loop where the same channel is consumed. Below is a sample code that does not work. How is this achievable?
https://go.dev/play/p/o5ZhNfw4IFu
package main
import (
"context"
"fmt"
"time"
)
func main() {
ch1 := make(chan struct{})
ch2 := make(chan struct{})
defer close(ch1)
defer close(ch2)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
go func() {
time.Sleep(time.Second * 1)
ch1 <- struct{}{}
}()
loop:
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
break loop
case <-ch1:
fmt.Println("ch1")
ch2 <- struct{}{} // This here does not work!
case <-ch2:
fmt.Println("ch2")
}
}
}
1. send data to ch2 inside goroutine
package main
import (
"context"
"fmt"
"time"
)
func main() {
ch1 := make(chan struct{})
ch2 := make(chan struct{})
defer close(ch1)
defer close(ch2)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
go func() {
time.Sleep(time.Second * 1)
ch1 <- struct{}{}
}()
loop:
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
break loop
case <-ch1:
fmt.Println("ch1")
go func() {
ch2 <- struct{}{}
}()
case <-ch2:
fmt.Println("ch2")
}
}
}
or
2. make ch2 buffered
package main
import (
"context"
"fmt"
"time"
)
func main() {
ch1 := make(chan struct{})
ch2 := make(chan struct{}, 1)
defer close(ch1)
defer close(ch2)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
go func() {
time.Sleep(time.Second * 1)
ch1 <- struct{}{}
}()
loop:
for {
select {
case <-ctx.Done():
fmt.Println("timeout")
break loop
case <-ch1:
fmt.Println("ch1")
ch2 <- struct{}{}
case <-ch2:
fmt.Println("ch2")
}
}
}

Golang: Cannot send error to channel in recover()

I try to send an error in the channel on recovery
Why this error is not sent to the channel?
package main
import (
"fmt"
"sync"
"errors"
)
func main() {
var wg sync.WaitGroup
wg.Add(1)
batchErrChan := make(chan error)
go func(errchan chan error) {
defer func() {
if r := recover(); r != nil {
errchan <- errors.New("recover err")
}
close(batchErrChan)
wg.Done()
}()
panic("ddd")
}(batchErrChan)
go func() {
for _ = range batchErrChan {
fmt.Println("err in range")
}
}()
wg.Wait()
}
https://play.golang.org/p/0ytunuYDWZU
I expect "err in range" to be printed, but it is not. Why?
Your program ends before the goroutine gets a chance to print the message. Try waiting to it:
...
done:=make(chan struct{})
go func() {
for _ = range batchErrChan {
fmt.Println("err in range")
}
close(done)
}()
wg.Wait()
<-done
}

Panic while trying to stop creating more goroutines

I'm trying to parallelize calls to an API to speed things up, but I'm facing a problem where I need to stop spinning up goroutines to call the API if I receive an error from one of the goroutine calls. Since I am closing the channel twice(once in the error handling part and when the execution is done), I'm getting a panic: close of closed channel error. Is there an elegant way to handle this without the program to panic? Any help would be appreciated!
The following is the pseudo-code snippet.
for i := 0; i < someNumber; i++ {
go func(num int, q chan<- bool) {
value, err := callAnAPI()
if err != nil {
close(q)//exit from the for-loop
}
// process the value here
wg.Done()
}(i, quit)
}
close(quit)
To mock my scenario, I have written the following program. Is there any way to exit the for-loop gracefully once the condition(commented out) is satisfied?
package main
import (
"fmt"
"sync"
)
func receive(q <-chan bool) {
for {
select {
case <-q:
return
}
}
}
func main() {
quit := make(chan bool)
var result []int
wg := &sync.WaitGroup{}
wg.Add(10)
for i := 0; i < 10; i++ {
go func(num int, q chan<- bool) {
//if num == 5 {
// close(q)
//}
result = append(result, num)
wg.Done()
}(i, quit)
}
close(quit)
receive(quit)
wg.Wait()
fmt.Printf("Result: %v", result)
}
You can use context package which defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
package main
import (
"context"
"fmt"
"sync"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished, even without error
wg := &sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func(num int) {
defer wg.Done()
select {
case <-ctx.Done():
return // Error occured somewhere, terminate
default: // avoid blocking
}
// your code here
// res, err := callAnAPI()
// if err != nil {
// cancel()
// return
//}
if num == 5 {
cancel()
return
}
fmt.Println(num)
}(i)
}
wg.Wait()
fmt.Println(ctx.Err())
}
Try on: Go Playground
You can also take a look to this answer for more detailed explanation.

goroutine didn't respect `ctx.done()` or quit properly

I am trying to achieve quit gracefully when user press Ctrl-C. I am trying the code in Make Ctrl+C cancel the context.Context.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
ctx := context.Background()
// trap Ctrl+C and call cancel on the context
ctx, cancel := context.WithCancel(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
defer func() {
signal.Stop(c)
cancel()
fmt.Println("Cleaned up")
}()
go func() {
select {
case <-c:
fmt.Println("Got interrupt signal")
cancel()
case <-ctx.Done():
}
fmt.Println("Stopped monitoring")
}()
select {
case <-ctx.Done():
fmt.Println("notified to quit")
case <-time.NewTimer(time.Second * 2).C:
fmt.Println("done something")
}
}
It works well as expected when user press Ctrl-c, it console out the following:
Got interrupt signal
Stopped monitoring
notified to quit
Cleaned up
However, if it quit normally, It doesn't work as expected as below:
done something
Cleaned up
I mean it should print out Stopped monitoring, but not. In defer cleanup function, it called cancel() which should trigger the select in monitoring goroutine to quit, but not.
How to solve the issue?
Thanks #Zan Lynx, I worked out the below solution.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
terminated := monitor(ctx, cancel)
defer func() {
cancel()
fmt.Println("Cleaned up")
<-terminated // wait for the monior goroutine quit
}()
select {
case <-ctx.Done():
fmt.Println("notified to quit")
case <-time.NewTimer(time.Second * 1).C:
fmt.Println("done something")
}
}
func monitor(ctx context.Context, cancel context.CancelFunc) <-chan interface{} {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
terminated := make(chan interface{})
go func() {
defer close(terminated)
defer fmt.Println("Stopped monitoring1")
defer signal.Stop(c)
select {
case <-c:
fmt.Println("Got interrupt singnal")
cancel()
case <-ctx.Done():
}
}()
return terminated
}

Resources