FILE / ScuroNeko/mtg

mtglib/internal/relay/relay.go

Исходный файл и его история в репозитории.
FILE b19f491fc8feabb07a236765a44264c0ffd3ee1d
Files
mtg/mtglib/internal/relay/relay.go
T
9seconds 16f9ec690b Fix race condition in closing a relay
This commit fixes a situration when relay can be reset before all
waiting goroutines are finished. For example, we terminate processing
based on some event: socket error etc. So, error happens and context is
cancelled. After that a main relay goroutine starts to wait. Meanwhile a
second goroutine reaches deferred function and set wg to done. It means
that main goroutine can continue.

In this case this is really possible that we can start resetting before
transmit goroutine really exits.

A correct solution is to always do wg.Done() as a first deferred thing
on entering to a function. In that case we do not need reordering and so
on.
2021-05-06 22:08:39 +03:00

130 lines
2.1 KiB
Go

package relay
import (
"context"
"io"
"sync"
"time"
)
type Relay struct {
ctx context.Context
ctxCancel context.CancelFunc
logger Logger
processMutex sync.Mutex
eastBuffer []byte
westBuffer []byte
tickChannel chan struct{}
errorChannel chan error
tickTimeout time.Duration
}
func (r *Relay) Reset() {
r.processMutex.Lock()
defer r.processMutex.Unlock()
if r.ctxCancel != nil {
r.ctxCancel()
}
r.ctx = nil
r.ctxCancel = nil
r.logger = nil
}
func (r *Relay) Process(eastConn, westConn io.ReadWriteCloser) error {
r.processMutex.Lock()
defer r.processMutex.Unlock()
eastConn = conn{
ReadWriteCloser: eastConn,
ctx: r.ctx,
tickChannel: r.tickChannel,
}
westConn = conn{
ReadWriteCloser: westConn,
ctx: r.ctx,
tickChannel: r.tickChannel,
}
wg := &sync.WaitGroup{}
wg.Add(3) // nolint: gomnd
go r.runObserver(eastConn, westConn, wg)
go r.transmit(eastConn, westConn, r.westBuffer, "west", wg)
r.transmit(westConn, eastConn, r.eastBuffer, "east", wg)
wg.Wait()
select {
case err := <-r.errorChannel:
return err
default:
return nil
}
}
func (r *Relay) transmit(src io.ReadCloser, dst io.WriteCloser,
buffer []byte, direction string, wg *sync.WaitGroup) {
defer wg.Done()
defer func() {
r.ctxCancel()
src.Close()
dst.Close()
}()
if _, err := io.CopyBuffer(dst, src, buffer); err != nil {
r.logger.Printf("error '%v' happened on direction %s", err, direction)
select {
case <-r.ctx.Done():
err = r.ctx.Err()
default:
}
select {
case r.errorChannel <- err:
default:
}
}
}
func (r *Relay) runObserver(one, another io.Closer, wg *sync.WaitGroup) {
defer wg.Done()
ticker := time.NewTicker(time.Second)
defer func() {
one.Close()
another.Close()
ticker.Stop()
select {
case <-ticker.C:
default:
}
}()
lastTickAt := time.Now()
for {
select {
case <-r.ctx.Done():
return
case <-r.tickChannel:
lastTickAt = time.Now()
case <-ticker.C:
if time.Since(lastTickAt) > r.tickTimeout {
r.logger.Printf("exit due to a timeout")
r.ctxCancel()
return
}
}
}
}