How to make context with timeout and retries in Go?

huangapple go评论71阅读模式
英文:

How to make context with timeout and retries in Go?

问题

我尝试在Go语言中使用超时和多次重试来创建上下文。以下是代码示例:

func readRetry(port io.ReadWriteCloser, timeout, cnt int) []byte {
    fmt.Println("IN READ RETRY")
    for i := 0; i < cnt; i++ {
        fmt.Println("Read attempt:", i)
        res := readWithContext(timeout, port)
        if res != nil {
            return res
        }
    }
    return nil
}

func readWithContext(timeout int, port io.ReadWriteCloser) []byte {
    fmt.Println("IN readWithContext")
    fmt.Println("Opening channel")
    rcvCh := make(chan []byte)
    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*time.Duration(timeout)))
    defer cancel()

    go reader(ctx, port, rcvCh)

    for {
        select {
        case <-ctx.Done():
            fmt.Println("reader: context cancelled")
            return nil
        case buf := <-rcvCh:
            fmt.Println("reader: got data")
            return buf
        }
    }
}

func reader(ctx context.Context, port io.ReadWriteCloser, rcvCh chan []byte) {

    fmt.Println("IN reader")

    answ := make([]byte, 1024)
    buf := bytes.Buffer{}
    var err error

    for {
        i := 0
        i, err = port.Read(answ)
        if err != nil && err != io.EOF {
            log.Printf("port.Read: %v", err)
        }
        if i != 0 {
            answ = answ[:i]
            buf.Write(answ)
            if buf.Bytes()[len(buf.Bytes())-1] == delimiter {
                fmt.Print("Received:")
                printBuf(buf.Bytes())
                rcvCh <- buf.Bytes()
                return
            }
        }
    }
}

然后,我调用readRetry函数:result := readRetry(port, 2, 5),其中2秒超时,重试5次。但是,如果数据在第一次尝试时还没有准备好,那么reader函数无法写入rcvCh。这可能是因为通道已满了?为什么会这样?如果我尝试在readWithContext执行结束时关闭通道,会发生冲突-写入已关闭的通道。冲突在哪里?我认为,readWithContext每次都会作为一个新实例启动,创建一个新的rcvCh实例,如果reader通过超时关闭,所有的函数链和它们的局部变量,包括通道,都已被销毁。但是,似乎我犯了一个错误。那么,如何进行重试呢?

看一下日志的输出:

IN READ RETRY
Read attempt: 1
IN readWithContext
Opening channel
IN reader
Start reader
Received: 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0d // <- 数据已接收,但未写入通道!
reader: context cancelled
英文:

I try to make context with timeout && several retries in Go.
Here is the code example

func readRetry(port io.ReadWriteCloser, timeout, cnt int) []byte {
fmt.Println(&quot;IN READ RETRY&quot;)
for i := 0; i &lt; cnt; i++ {
fmt.Println(&quot;Read attempt:&quot;, i)
res := readWithContext(timeout, port)
if res != nil {
return res
}
}
return nil
}
func readWithContext(timeout int, port io.ReadWriteCloser) []byte {
fmt.Println(&quot;IN readWithContext&quot;)
fmt.Println(&quot;Opening channel&quot;)
rcvCh := make(chan []byte)
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*time.Duration(timeout)))
defer cancel()
go reader(ctx, port, rcvCh)
for {
select {
case &lt;-ctx.Done():
fmt.Println(&quot;reader: context cancelled&quot;)
return nil
case buf := &lt;-rcvCh:
fmt.Println(&quot;reader: got data&quot;)
return buf
}
}
}
func reader(ctx context.Context, port io.ReadWriteCloser, rcvCh chan []byte) {
fmt.Println(&quot;IN reader&quot;)
answ := make([]byte, 1024)
buf := bytes.Buffer{}
var err error
for {
i := 0
i, err = port.Read(answ)
if err != nil &amp;&amp; err != io.EOF {
log.Printf(&quot;port.Read: %v&quot;, err)
}
if i != 0 {
answ = answ[:i]
buf.Write(answ)
if buf.Bytes()[len(buf.Bytes())-1] == delimiter {
fmt.Print(&quot;Received: &quot;)
printBuf(buf.Bytes())
rcvCh &lt;- buf.Bytes() //if there is no data in the first attempt, cannot write to the channel here!!
return
}
}
}
}

Then, I call readRetry result := readRetry(port, 2, 5) // 2 seconds timeout, 5 retries. But if data is not ready for the first time, then reader cannot write to rcvCh . It might be full? Why? If I try to close channel at the end of readWithContext execution, there is a collision - write to closed channel. Where is the collision? It think, readWithContext starts every time as a new instance, creates a new instance of rcvCh, and if the reader closes via timeout, all chain of functions with their local variables, including the channel, has been destroyed. But, it seems, I make a mistake. So, how to make retries?
Look, how the log looks like:

IN READ RETRY
Read attempt: 1
IN readWithContext
Opening channel
IN reader
Start reader
Received: 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0d //&lt;- data is received, but not written to the channel!!
reader: context cancelled

答案1

得分: 0

每次重试都会创建一个新的读取器和一个新的通道。如果readWithContext超时,读取器仍然在等待,并且可能最终会读取,但现在没有人在通道的另一端监听,因此该读取器会泄漏。

使用一个单独的reader goroutine和一个单独的通道,使用readWithContext从中读取。如果上下文过期并且所有重试都耗尽,您还必须停止读取器。

英文:

Every retry creates a new reader and a new channel. If readWithContext times out, the reader is still there waiting, and probably eventually reading, but now there's nobody listening on the other end of the channel, so that reader is leaked.

Have a single reader goroutine, and a single channel, use readWithContext to read from it. If the context expires and all retries are exhausted, you have to stop the reader as well.

huangapple
  • 本文由 发表于 2022年12月1日 04:56:09
  • 转载请务必保留本文链接:https://go.coder-hub.com/74634058.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定