Go calls the timeout handling method concurrently

  • 2020-06-23 00:34:53
  • OfStack

I have talked about THE coroutine of golang before, and I found that it seems very theoretical, especially in the aspect of concurrency security, so I combined with some examples on the Internet to test the magic of channel, select and context in go routine.

Scenario - Microservice invocation

We used gin (1 web framework) as the tool for handling requests, and the requirements were as follows:

A request from X calls the three methods A, B, and C in parallel and adds up the results returned by the three methods as Response for X.

However, our Response has a time requirement (no more than 3 seconds response time). Maybe one or two of A, B and C have complicated processing logic of 10 minutes, or the data volume is so large that the processing time exceeds the expectation, then we will immediately cut off and return the sum of any returned results.

Let's define the main function first:


func main() {
 r := gin.New()
 r.GET("/calculate", calHandler)
 http.ListenAndServe(":8008", r)
}

Very simple, common request acceptance and handler definition. calHandler is the function we use to handle the request.

Define three fake microservices, the third of which will be the digit of our timeout


func microService1() int {
 time.Sleep(1*time.Second)
 return 1
}

func microService2() int {
 time.Sleep(2*time.Second)
 return 2
}

func microService3() int {
 time.Sleep(10*time.Second)
 return 3
}

Next, what's in calHandler


func calHandler(c *gin.Context) {
 ...
}

Point 1- Concurrent calls

Just use go

So at the beginning of 1 we might write it like this:


go microService1()
go microService2()
go microService3()

It's very simple there's no, but wait, how do I get the return value?

In order to be able to accept the results of the processing in parallel, it is easy to think of using channel to do it.

So let's call the service like this:


var resChan = make(chan int, 3) //  Because there are 3 That's the result. So let's create 1 One can hold 3 A value of  int channel . 
go func() {
 resChan <- microService1()
}()

go func() {
 resChan <- microService2()
}()

go func() {
 resChan <- microService3()
}()

If there is something connected, there must be a way to calculate it, so we add 1 1 straight cycle to take the results in resChan and calculate the method:


var resContainer, sum int
for {
 resContainer = <-resChan
 sum += resContainer
}

So if we take 1 we have 1 sum to calculate every time we take it out of resChan.

Point 2-- Timeout signal

It's not over yet. What about the agreed timeout handling?

To implement timeout processing, we need to introduce one thing, context, what is context ?

We are using only one feature of context, timeout notification (which could have been replaced by channel).

You can see that we passed c * gin.Context as a parameter when defining calHandler, so we don't have to declare it ourselves.
gin.Context is simply understood as a context container that runs through the entire gin declaration cycle, somewhat like a doppelgant, or quantum entanglement.

With this ES99en.Context, we can operate on context in one place, and other functions or methods that are using context will also feel the changes made by context.


ctx, _ := context.WithTimeout(c, 3*time.Second) // define 1 A timeout  context

Whenever the time is up, we can use ES107en.Done () to get a timeout of channel(notification), and then stop and release ctx wherever the ctx is used.

In general, ctx.Done() is used in combination with select.

So we need another loop to listen for ctx.Done ()


for {
 select {
 case <- ctx.Done():
  //  Returns the result 
}

Now we have two for, can we merge it?


for {
 select {
 case resContainer = <-resChan:
  sum += resContainer
  fmt.Println("add", resContainer)
 case <- ctx.Done():
  fmt.Println("result:", sum)
  return
 }
}

Hey, that looks good.

But how do we output when we normally complete the microservice call?

Looks like we need another flag


var count int
for {
 select {
 case resContainer = <-resChan:
  sum += resContainer
  count ++
  fmt.Println("add", resContainer)
  if count > 2 {
   fmt.Println("result:", sum)
   return
  }
 case <- ctx.Done():
  fmt.Println("timeout result:", sum)
  return
 }
}

We add a counter because we are only calling the microservice three times, so when count is greater than 2, we should finish and output the result.

Point 3- Waiting in concurrency

The timer above is a lazy way because we know how many times the microservice is called. What if we don't know, or if we add it later?
Is it too sand sculpting to manually change the threshold of count each time? This is when we add the sync package.
One feature of sync that we will be using is WaitGroup. It is used to wait for a set of coroutines to complete before executing the next step.

Let's change the code block for the previous microservice invocation:


func microService1() int {
 time.Sleep(1*time.Second)
 return 1
}

func microService2() int {
 time.Sleep(2*time.Second)
 return 2
}

func microService3() int {
 time.Sleep(10*time.Second)
 return 3
}

0

Now that we have the success signal, we add it to the monitoring for loop and make some modifications to remove the original count judgment.


func microService1() int {
 time.Sleep(1*time.Second)
 return 1
}

func microService2() int {
 time.Sleep(2*time.Second)
 return 2
}

func microService3() int {
 time.Sleep(10*time.Second)
 return 3
}

1

Three case, with clear division of labor, one is used to get the result of service output and calculate, one is used to make the final completed output, and one is the timeout output.
We also run this loop listening as a coroutine.

At this point, all the main code is complete. Here is the full version


func microService1() int {
 time.Sleep(1*time.Second)
 return 1
}

func microService2() int {
 time.Sleep(2*time.Second)
 return 2
}

func microService3() int {
 time.Sleep(10*time.Second)
 return 3
}

2

The above program simply describes a processing scenario that calls another microservice timeout.

In the actual process, many, many spices need to be added to ensure the external integrity of the interface.


Related articles: