Repository navigation
Expand file tree
/
Copy pathsend_queue.go
More file actions
170 lines (152 loc) · 4.55 KB
/
Copy pathsend_queue.go
File metadata and controls
170 lines (152 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package quic
import (
"net"
"github.com/quic-go/quic-go/internal/protocol"
)
type sender interface {
Send(p *packetBuffer, gsoSize uint16, ecn protocol.ECN)
SendProbe(*packetBuffer, net.Addr, packetInfo)
Run() error
WouldBlock() bool
Available() <-chan struct{}
Close()
}
type queueEntry struct {
buf *packetBuffer
gsoSize uint16
ecn protocol.ECN
}
type sendQueue struct {
queue chan queueEntry
closeCalled chan struct{} // runStopped when Close() is called
runStopped chan struct{} // runStopped when the run loop returns
available chan struct{}
conn sendConn
// Scratch for coalescing queued packets into one batched send (reused across Run
// iterations; Run is single-goroutine so no locking).
batch []queueEntry
bufs [][]byte
}
// batchWriter is an optional capability: a conn that can send several datagrams that
// share (gsoSize, ecn) in one call. On darwin this is one sendmsg_x, the analog of the
// Linux GSO path; a conn that does not implement it just gets one Write per packet.
type batchWriter interface {
writeBatch(bufs [][]byte, gsoSize uint16, ecn protocol.ECN) error
}
var _ sender = &sendQueue{}
const sendQueueCapacity = 8
// maxSendBatch caps how many queued packets one batched send coalesces. The queue holds
// at most sendQueueCapacity, so this is a safe upper bound with headroom.
const maxSendBatch = 16
func newSendQueue(conn sendConn) sender {
return &sendQueue{
conn: conn,
runStopped: make(chan struct{}),
closeCalled: make(chan struct{}),
available: make(chan struct{}, 1),
queue: make(chan queueEntry, sendQueueCapacity),
}
}
// Send sends out a packet. It's guaranteed to not block.
// Callers need to make sure that there's actually space in the send queue by calling WouldBlock.
// Otherwise Send will panic.
func (h *sendQueue) Send(p *packetBuffer, gsoSize uint16, ecn protocol.ECN) {
select {
case h.queue <- queueEntry{buf: p, gsoSize: gsoSize, ecn: ecn}:
// clear available channel if we've reached capacity
if len(h.queue) == sendQueueCapacity {
select {
case <-h.available:
default:
}
}
case <-h.runStopped:
default:
panic("sendQueue.Send would have blocked")
}
}
func (h *sendQueue) SendProbe(p *packetBuffer, addr net.Addr, info packetInfo) {
h.conn.WriteTo(p.Data, addr, info)
}
func (h *sendQueue) WouldBlock() bool {
return len(h.queue) == sendQueueCapacity
}
func (h *sendQueue) Available() <-chan struct{} {
return h.available
}
func (h *sendQueue) Run() error {
defer close(h.runStopped)
var shouldClose bool
for {
if shouldClose && len(h.queue) == 0 {
return nil
}
select {
case <-h.closeCalled:
h.closeCalled = nil // prevent this case from being selected again
// make sure that all queued packets are actually sent out
shouldClose = true
case e := <-h.queue:
batch := append(h.batch[:0], e)
// Opportunistically coalesce packets already queued that share the same
// (gsoSize, ecn) into one batched send. A packet that differs flushes the
// current batch and starts a new one.
for len(h.queue) > 0 && len(batch) < maxSendBatch {
n := <-h.queue
if n.gsoSize != batch[0].gsoSize || n.ecn != batch[0].ecn {
if err := h.flush(batch); err != nil {
h.batch = batch[:0]
return err
}
batch = append(h.batch[:0], n)
continue
}
batch = append(batch, n)
}
err := h.flush(batch)
h.batch = batch[:0]
if err != nil {
return err
}
}
}
}
// flush sends a group of queued packets that share (gsoSize, ecn), as one batched send
// when the conn supports it (and gsoSize is 0, i.e. darwin), else one Write each. It
// releases every packet buffer afterward and signals availability. The size-error check
// is preserved: a "datagram too large" reply drives path MTU discovery rather than
// tearing the connection down.
func (h *sendQueue) flush(batch []queueEntry) error {
var sendErr error
if bw, ok := h.conn.(batchWriter); ok && len(batch) > 1 && batch[0].gsoSize == 0 {
bufs := h.bufs[:0]
for _, e := range batch {
bufs = append(bufs, e.buf.Data)
}
sendErr = bw.writeBatch(bufs, batch[0].gsoSize, batch[0].ecn)
h.bufs = bufs[:0]
} else {
for _, e := range batch {
if err := h.conn.Write(e.buf.Data, e.gsoSize, e.ecn); err != nil {
sendErr = err
break
}
}
}
for _, e := range batch {
e.buf.Release()
}
select {
case h.available <- struct{}{}:
default:
}
if sendErr != nil && !isSendMsgSizeErr(sendErr) {
return sendErr
}
return nil
}
func (h *sendQueue) Close() {
close(h.closeCalled)
// wait until the run loop returned
<-h.runStopped
}