Wait in doppel.Conn if there is anything to write

This commit is contained in:
9seconds
2026-03-19 15:42:00 +01:00
parent a23ae05f3b
commit 724904f50d
2 changed files with 63 additions and 20 deletions
+32 -20
View File
@@ -16,22 +16,25 @@ type Conn struct {
} }
type connPayload struct { type connPayload struct {
ctx context.Context ctx context.Context
ctxCancel context.CancelCauseFunc ctxCancel context.CancelCauseFunc
clock Clock clock Clock
wg sync.WaitGroup wg sync.WaitGroup
syncWriteLock sync.RWMutex writeStream bytes.Buffer
writeStream bytes.Buffer writtenCond sync.Cond
writeCond *sync.Cond done bool
} }
func (c Conn) Write(p []byte) (int, error) { func (c Conn) Write(p []byte) (int, error) {
c.p.syncWriteLock.RLock() if len(p) == 0 {
defer c.p.syncWriteLock.RUnlock() return 0, context.Cause(c.p.ctx)
}
c.p.writeCond.L.Lock() c.p.writtenCond.L.Lock()
c.p.writeStream.Write(p) c.p.writeStream.Write(p)
c.p.writeCond.L.Unlock() c.p.writtenCond.L.Unlock()
c.p.writtenCond.Signal()
return len(p), context.Cause(c.p.ctx) return len(p), context.Cause(c.p.ctx)
} }
@@ -43,8 +46,6 @@ func (c Conn) Start() {
} }
func (c Conn) start() { func (c Conn) start() {
defer c.p.writeCond.Broadcast()
buf := [tls.MaxRecordSize]byte{} buf := [tls.MaxRecordSize]byte{}
for { for {
@@ -54,11 +55,16 @@ func (c Conn) start() {
case <-c.p.clock.tick: case <-c.p.clock.tick:
} }
c.p.writeCond.L.Lock() size := c.p.clock.stats.Size()
n, err := c.p.writeStream.Read(buf[:c.p.clock.stats.Size()])
c.p.writeCond.L.Unlock()
if n == 0 || err != nil { c.p.writtenCond.L.Lock()
for c.p.writeStream.Len() == 0 && !c.p.done {
c.p.writtenCond.Wait()
}
n, _ := c.p.writeStream.Read(buf[:size])
c.p.writtenCond.L.Unlock()
if n == 0 {
continue continue
} }
@@ -66,13 +72,17 @@ func (c Conn) start() {
c.p.ctxCancel(err) c.p.ctxCancel(err)
return return
} }
c.p.writeCond.Signal()
} }
} }
func (c Conn) Stop() { func (c Conn) Stop() {
c.p.ctxCancel(nil) c.p.ctxCancel(nil)
c.p.writtenCond.L.Lock()
c.p.done = true
c.p.writtenCond.L.Unlock()
c.p.writtenCond.Broadcast()
c.p.wg.Wait() c.p.wg.Wait()
} }
@@ -83,7 +93,9 @@ func NewConn(ctx context.Context, conn essentials.Conn, stats *Stats) Conn {
p: &connPayload{ p: &connPayload{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
writeCond: sync.NewCond(&sync.Mutex{}), writtenCond: sync.Cond{
L: &sync.Mutex{},
},
clock: Clock{ clock: Clock{
stats: stats, stats: stats,
tick: make(chan struct{}), tick: make(chan struct{}),
+31
View File
@@ -141,6 +141,37 @@ func (suite *ConnTestSuite) TestWriteReturnsErrorAfterStop() {
suite.Error(err) suite.Error(err)
} }
func (suite *ConnTestSuite) TestStopDoesNotDeadlockWhenStartIsWaiting() {
suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")).
Return(0, nil).
Maybe()
for range 100 {
func() {
ctx, cancel := context.WithCancel(suite.ctx)
defer cancel()
c := NewConn(ctx, suite.connMock, &Stats{
k: 2.0,
lambda: 0.01,
})
done := make(chan struct{})
go func() {
defer close(done)
c.Stop()
}()
select {
case <-done:
case <-time.After(2 * time.Second):
suite.Fail("Stop() deadlocked: start() likely stuck in writtenCond.Wait()")
}
}()
}
}
func (suite *ConnTestSuite) TestStopOnUnderlyingWriteError() { func (suite *ConnTestSuite) TestStopOnUnderlyingWriteError() {
suite.connMock. suite.connMock.
On("Write", mock.AnythingOfType("[]uint8")). On("Write", mock.AnythingOfType("[]uint8")).