Skip to content

Commit 90bd2e1

Browse files
committed
Use ping-pong buffer for batch conn
To prevent writes from getting blocked while flushing the batch, switching to a ping-pong buffer. Add some more test to explicitly force write batch interval and write batch size flush.
1 parent 030898d commit 90bd2e1

3 files changed

Lines changed: 508 additions & 85 deletions

File tree

udp/batchconn.go

Lines changed: 235 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,248 @@ type BatchPacketConn interface {
3232
io.Closer
3333
}
3434

35+
// ---------------------------------
36+
37+
type messageBatch struct {
38+
size int
39+
batchConn BatchPacketConn
40+
41+
mu sync.Mutex
42+
messages []ipv4.Message
43+
writePos int
44+
}
45+
46+
func newMessageBatch(size int, batchConn BatchPacketConn) *messageBatch {
47+
m := &messageBatch{
48+
size: size,
49+
batchConn: batchConn,
50+
}
51+
m.init()
52+
53+
return m
54+
}
55+
56+
func (m *messageBatch) init() {
57+
m.messages = make([]ipv4.Message, m.size)
58+
for i := range m.messages {
59+
m.messages[i].Buffers = [][]byte{make([]byte, sendMTU)}
60+
}
61+
}
62+
63+
func (m *messageBatch) isFull() bool {
64+
return m.writePos == m.size
65+
}
66+
67+
func (m *messageBatch) EnqueueMessage(buf []byte, raddr net.Addr) (int, bool) {
68+
m.mu.Lock()
69+
defer m.mu.Unlock()
70+
71+
if len(buf) == 0 || m.isFull() {
72+
return 0, m.isFull()
73+
}
74+
75+
msg := &m.messages[m.writePos]
76+
// reset buffers
77+
msg.Buffers = msg.Buffers[:1]
78+
msg.Buffers[0] = msg.Buffers[0][:cap(msg.Buffers[0])]
79+
80+
if raddr != nil {
81+
msg.Addr = raddr
82+
}
83+
if n := copy(msg.Buffers[0], buf); n < len(buf) {
84+
extraBuffer := make([]byte, len(buf)-n)
85+
copy(extraBuffer, buf[n:])
86+
msg.Buffers = append(msg.Buffers, extraBuffer)
87+
} else {
88+
msg.Buffers[0] = msg.Buffers[0][:n]
89+
}
90+
m.writePos++
91+
92+
return len(buf), m.isFull()
93+
}
94+
95+
func (m *messageBatch) Flush() {
96+
m.mu.Lock()
97+
defer m.mu.Unlock()
98+
99+
var txN int
100+
for txN < m.writePos {
101+
n, err := m.batchConn.WriteBatch(m.messages[txN:m.writePos], 0)
102+
if err != nil {
103+
break
104+
}
105+
txN += n
106+
}
107+
108+
m.writePos = 0
109+
}
110+
111+
// ---------------------------------
112+
113+
type pingPong struct {
114+
mu sync.Mutex
115+
batches [2]*messageBatch
116+
writeBatchIdx int
117+
readBatchIdx int
118+
flushPending bool
119+
120+
writeReady chan struct{}
121+
flushCycleDone chan struct{}
122+
flusherDone chan struct{}
123+
124+
closed int32
125+
}
126+
127+
func newPingPong(size int, interval time.Duration, batchConn BatchPacketConn) *pingPong {
128+
p := &pingPong{
129+
writeReady: make(chan struct{}),
130+
flushCycleDone: make(chan struct{}),
131+
flusherDone: make(chan struct{}),
132+
}
133+
for i := 0; i < len(p.batches); i++ {
134+
p.batches[i] = newMessageBatch(size, batchConn)
135+
}
136+
137+
go p.flusher(interval)
138+
139+
return p
140+
}
141+
142+
func (p *pingPong) Close() {
143+
atomic.StoreInt32(&p.closed, 1)
144+
145+
select {
146+
case p.writeReady <- struct{}{}:
147+
default:
148+
}
149+
150+
<-p.flusherDone
151+
}
152+
153+
func (p *pingPong) EnqueueMessage(buf []byte, raddr net.Addr) int {
154+
p.mu.Lock()
155+
var (
156+
writeBatch *messageBatch
157+
n int
158+
isFull bool
159+
)
160+
for {
161+
if writeBatch = p.getWriteBatch(); writeBatch != nil {
162+
n, isFull = writeBatch.EnqueueMessage(buf, raddr)
163+
if n == len(buf) {
164+
break
165+
}
166+
}
167+
168+
p.mu.Unlock()
169+
select {
170+
case <-p.flushCycleDone:
171+
case <-time.After(100 * time.Microsecond):
172+
}
173+
174+
if atomic.LoadInt32(&p.closed) == 1 {
175+
return 0
176+
}
177+
178+
p.mu.Lock()
179+
}
180+
p.mu.Unlock()
181+
182+
// enqueuing given message fills up the write batch, queue up a flush
183+
if isFull {
184+
select {
185+
case p.writeReady <- struct{}{}:
186+
default:
187+
}
188+
}
189+
190+
return n
191+
}
192+
193+
func (p *pingPong) getWriteBatch() *messageBatch {
194+
if p.writeBatchIdx == p.readBatchIdx && p.flushPending {
195+
return nil
196+
}
197+
198+
return p.batches[p.writeBatchIdx]
199+
}
200+
201+
func (p *pingPong) updateWriteBatchAndGetReadBatch() *messageBatch {
202+
p.mu.Lock()
203+
defer p.mu.Unlock()
204+
205+
if p.writeBatchIdx != p.readBatchIdx || p.flushPending {
206+
return nil
207+
}
208+
209+
p.writeBatchIdx ^= 1
210+
p.flushPending = true
211+
212+
return p.batches[p.readBatchIdx]
213+
}
214+
215+
func (p *pingPong) updateReadBatch() {
216+
p.mu.Lock()
217+
defer p.mu.Unlock()
218+
219+
if !p.flushPending {
220+
return
221+
}
222+
223+
p.readBatchIdx ^= 1
224+
p.flushPending = false
225+
226+
select {
227+
case p.flushCycleDone <- struct{}{}:
228+
default:
229+
}
230+
}
231+
232+
func (p *pingPong) flusher(interval time.Duration) {
233+
defer close(p.flusherDone)
234+
235+
writeTicker := time.NewTicker(interval / 2)
236+
defer writeTicker.Stop()
237+
238+
lastFlushAt := time.Now().Add(-interval)
239+
for atomic.LoadInt32(&p.closed) != 1 {
240+
select {
241+
case <-writeTicker.C:
242+
if time.Since(lastFlushAt) < interval {
243+
continue
244+
}
245+
case <-p.writeReady:
246+
}
247+
248+
readBatch := p.updateWriteBatchAndGetReadBatch()
249+
if readBatch == nil {
250+
continue
251+
}
252+
253+
readBatch.Flush()
254+
p.updateReadBatch()
255+
256+
lastFlushAt = time.Now()
257+
}
258+
}
259+
260+
// ------------------------------
261+
35262
// BatchConn uses ipv4/v6.NewPacketConn to wrap a net.PacketConn to write/read messages in batch,
36263
// only available in linux. In other platform, it will use single Write/Read as same as net.Conn.
37264
type BatchConn struct {
38265
net.PacketConn
39266

40267
batchConn BatchPacketConn
41268

42-
batchWriteMutex sync.Mutex
43-
batchWriteMessages []ipv4.Message
44-
batchWritePos int
45-
batchWriteLast time.Time
46-
47-
batchWriteSize int
48-
batchWriteInterval time.Duration
49-
50-
closed int32
269+
// ping-pong the batches to be able to accept new packets while a batch is written to socket
270+
batchPingPong *pingPong
51271
}
52272

53273
// NewBatchConn creates a *BatchConn from net.PacketConn with batch configs.
54274
func NewBatchConn(conn net.PacketConn, batchWriteSize int, batchWriteInterval time.Duration) *BatchConn {
55275
bc := &BatchConn{
56-
PacketConn: conn,
57-
batchWriteLast: time.Now(),
58-
batchWriteInterval: batchWriteInterval,
59-
batchWriteSize: batchWriteSize,
60-
batchWriteMessages: make([]ipv4.Message, batchWriteSize),
61-
}
62-
for i := range bc.batchWriteMessages {
63-
bc.batchWriteMessages[i].Buffers = [][]byte{make([]byte, sendMTU)}
276+
PacketConn: conn,
64277
}
65278

66279
// batch write only supports linux
@@ -70,34 +283,19 @@ func NewBatchConn(conn net.PacketConn, batchWriteSize int, batchWriteInterval ti
70283
} else if pc6 := ipv6.NewPacketConn(conn); pc6 != nil {
71284
bc.batchConn = pc6
72285
}
73-
}
74286

75-
if bc.batchConn != nil {
76-
go func() {
77-
writeTicker := time.NewTicker(batchWriteInterval / 2)
78-
defer writeTicker.Stop()
79-
for atomic.LoadInt32(&bc.closed) != 1 {
80-
<-writeTicker.C
81-
bc.batchWriteMutex.Lock()
82-
if bc.batchWritePos > 0 && time.Since(bc.batchWriteLast) >= bc.batchWriteInterval {
83-
_ = bc.flush()
84-
}
85-
bc.batchWriteMutex.Unlock()
86-
}
87-
}()
287+
bc.batchPingPong = newPingPong(batchWriteSize, batchWriteInterval, bc.batchConn)
88288
}
89289

90290
return bc
91291
}
92292

93293
// Close batchConn and the underlying PacketConn.
94294
func (c *BatchConn) Close() error {
95-
atomic.StoreInt32(&c.closed, 1)
96-
c.batchWriteMutex.Lock()
97-
if c.batchWritePos > 0 {
98-
_ = c.flush()
295+
if c.batchPingPong != nil {
296+
c.batchPingPong.Close()
99297
}
100-
c.batchWriteMutex.Unlock()
298+
101299
if c.batchConn != nil {
102300
return c.batchConn.Close()
103301
}
@@ -111,35 +309,7 @@ func (c *BatchConn) WriteTo(b []byte, addr net.Addr) (int, error) {
111309
return c.PacketConn.WriteTo(b, addr)
112310
}
113311

114-
return c.enqueueMessage(b, addr)
115-
}
116-
117-
func (c *BatchConn) enqueueMessage(buf []byte, raddr net.Addr) (int, error) {
118-
var err error
119-
c.batchWriteMutex.Lock()
120-
defer c.batchWriteMutex.Unlock()
121-
122-
msg := &c.batchWriteMessages[c.batchWritePos]
123-
// reset buffers
124-
msg.Buffers = msg.Buffers[:1]
125-
msg.Buffers[0] = msg.Buffers[0][:cap(msg.Buffers[0])]
126-
127-
c.batchWritePos++
128-
if raddr != nil {
129-
msg.Addr = raddr
130-
}
131-
if n := copy(msg.Buffers[0], buf); n < len(buf) {
132-
extraBuffer := make([]byte, len(buf)-n)
133-
copy(extraBuffer, buf[n:])
134-
msg.Buffers = append(msg.Buffers, extraBuffer)
135-
} else {
136-
msg.Buffers[0] = msg.Buffers[0][:n]
137-
}
138-
if c.batchWritePos == c.batchWriteSize {
139-
err = c.flush()
140-
}
141-
142-
return len(buf), err
312+
return c.batchPingPong.EnqueueMessage(b, addr), nil
143313
}
144314

145315
// ReadBatch reads messages in batch, return length of message readed and error.
@@ -158,21 +328,3 @@ func (c *BatchConn) ReadBatch(msgs []ipv4.Message, flags int) (int, error) {
158328

159329
return c.batchConn.ReadBatch(msgs, flags)
160330
}
161-
162-
func (c *BatchConn) flush() error {
163-
var writeErr error
164-
var txN int
165-
for txN < c.batchWritePos {
166-
n, err := c.batchConn.WriteBatch(c.batchWriteMessages[txN:c.batchWritePos], 0)
167-
if err != nil {
168-
writeErr = err
169-
170-
break
171-
}
172-
txN += n
173-
}
174-
c.batchWritePos = 0
175-
c.batchWriteLast = time.Now()
176-
177-
return writeErr
178-
}

0 commit comments

Comments
 (0)