Skip to content

Commit ce2bcd1

Browse files
committed
Use unlimited ring buffers in the Produce path, to allow in-flight Produce requests per broker to go above 9 (when configured to a higher value)
Signed-off-by: Marco Pracucci <marco@pracucci.com>
1 parent a08e2dc commit ce2bcd1

5 files changed

Lines changed: 407 additions & 133 deletions

File tree

‎pkg/kgo/broker.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ type broker struct {
159159
reapMu sync.Mutex // held when modifying a brokerCxn
160160

161161
// reqs manages incoming message requests.
162-
reqs ringReq
162+
reqs unlimitedRing[promisedReq]
163163
// dead is an atomic so a backed up reqs cannot block broker stoppage.
164164
dead atomicBool
165165
}
@@ -725,7 +725,7 @@ type brokerCxn struct {
725725
successes uint64
726726

727727
// resps manages reading kafka responses.
728-
resps ringResp
728+
resps fixedRing[promisedResp]
729729
// dead is an atomic so that a backed up resps cannot block cxn death.
730730
dead atomicBool
731731
// closed in cloneConn; allows throttle waiting to quit

‎pkg/kgo/producer.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type producer struct {
5858
idMu sync.Mutex
5959
idVersion int16
6060

61-
batchPromises ringBatchPromise
61+
batchPromises unlimitedRing[batchPromise] // we never call die() on it
6262

6363
txnMu sync.Mutex
6464
inTxn bool
@@ -595,7 +595,7 @@ type batchPromise struct {
595595
}
596596

597597
func (p *producer) promiseBatch(b batchPromise) {
598-
if first := p.batchPromises.push(b); first {
598+
if first, _ := p.batchPromises.push(b); first {
599599
go p.finishPromises(b)
600600
}
601601
}
@@ -639,7 +639,7 @@ start:
639639
cl.prsPool.put(b.recs)
640640
}
641641

642-
b, more = p.batchPromises.dropPeek()
642+
b, more, _ = p.batchPromises.dropPeek()
643643
if more {
644644
goto start
645645
}

‎pkg/kgo/ring.go‎

Lines changed: 50 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package kgo
22

3-
import "sync"
3+
import (
4+
"sync"
5+
)
46

5-
// The ring types below are fixed sized blocking MPSC ringbuffers. These
6-
// replace channels in a few places in this client. The *main* advantage they
7+
// The ring types below are based on fixed sized blocking MPSC ringbuffer.
8+
// One type is a fixed size ring, and the other type is an unlimited ring
9+
// that uses a fixed size ring if the number of elements is less than 8.
10+
//
11+
// These rings replace channels in a few places in this client. The *main* advantage they
712
// provide is to allow loops that terminate.
813
//
914
// With channels, we always have to have a goroutine draining the channel. We
@@ -40,19 +45,19 @@ const (
4045
eight = mask7 + 1
4146
)
4247

43-
type ringReq struct {
48+
type fixedRing[T any] struct {
4449
mu sync.Mutex
4550
c *sync.Cond
4651

47-
elems [eight]promisedReq
52+
elems [eight]T
4853

4954
head uint8
5055
tail uint8
5156
l uint8
5257
dead bool
5358
}
5459

55-
func (r *ringReq) die() {
60+
func (r *fixedRing[T]) die() {
5661
r.mu.Lock()
5762
defer r.mu.Unlock()
5863

@@ -62,7 +67,7 @@ func (r *ringReq) die() {
6267
}
6368
}
6469

65-
func (r *ringReq) push(pr promisedReq) (first, dead bool) {
70+
func (r *fixedRing[T]) push(elem T) (first, dead bool) {
6671
r.mu.Lock()
6772
defer r.mu.Unlock()
6873

@@ -77,18 +82,19 @@ func (r *ringReq) push(pr promisedReq) (first, dead bool) {
7782
return false, true
7883
}
7984

80-
r.elems[r.tail] = pr
85+
r.elems[r.tail] = elem
8186
r.tail = (r.tail + 1) & mask7
8287
r.l++
8388

8489
return r.l == 1, false
8590
}
8691

87-
func (r *ringReq) dropPeek() (next promisedReq, more, dead bool) {
92+
func (r *fixedRing[T]) dropPeek() (next T, more, dead bool) {
8893
r.mu.Lock()
8994
defer r.mu.Unlock()
9095

91-
r.elems[r.head] = promisedReq{}
96+
var zero T
97+
r.elems[r.head] = zero
9298
r.head = (r.head + 1) & mask7
9399
r.l--
94100

@@ -101,169 +107,88 @@ func (r *ringReq) dropPeek() (next promisedReq, more, dead bool) {
101107
return r.elems[r.head], r.l > 0, r.dead
102108
}
103109

104-
// ringResp duplicates the code above, but for promisedResp
105-
type ringResp struct {
110+
// This type is slightly different because we can have overflow.
111+
// If we have overflow, we add to overflow until overflow is drained -- we
112+
// always want strict ordering.
113+
type unlimitedRing[T any] struct {
106114
mu sync.Mutex
107-
c *sync.Cond
108115

109-
elems [eight]promisedResp
116+
elems [eight]T
110117

111118
head uint8
112119
tail uint8
113120
l uint8
114121
dead bool
122+
123+
overflow []T
115124
}
116125

117-
func (r *ringResp) die() {
126+
func (r *unlimitedRing[T]) die() {
118127
r.mu.Lock()
119128
defer r.mu.Unlock()
120129

121130
r.dead = true
122-
if r.c != nil {
123-
r.c.Broadcast()
124-
}
125131
}
126132

127-
func (r *ringResp) push(pr promisedResp) (first, dead bool) {
133+
func (r *unlimitedRing[T]) push(elem T) (first, dead bool) {
128134
r.mu.Lock()
129135
defer r.mu.Unlock()
130136

131-
for r.l == eight && !r.dead {
132-
if r.c == nil {
133-
r.c = sync.NewCond(&r.mu)
134-
}
135-
r.c.Wait()
136-
}
137-
138137
if r.dead {
139138
return false, true
140139
}
141140

142-
r.elems[r.tail] = pr
143-
r.tail = (r.tail + 1) & mask7
144-
r.l++
145-
146-
return r.l == 1, false
147-
}
148-
149-
func (r *ringResp) dropPeek() (next promisedResp, more, dead bool) {
150-
r.mu.Lock()
151-
defer r.mu.Unlock()
152-
153-
r.elems[r.head] = promisedResp{}
154-
r.head = (r.head + 1) & mask7
155-
r.l--
156-
157-
if r.c != nil {
158-
r.c.Signal()
159-
}
160-
161-
return r.elems[r.head], r.l > 0, r.dead
162-
}
163-
164-
// ringSeqResp duplicates the code above, but for *seqResp. We leave off die
165-
// because we do not use it, but we keep `c` for testing lowering eight/mask7.
166-
type ringSeqResp struct {
167-
mu sync.Mutex
168-
c *sync.Cond
169-
170-
elems [eight]*seqResp
171-
172-
head uint8
173-
tail uint8
174-
l uint8
175-
}
176-
177-
func (r *ringSeqResp) push(sr *seqResp) (first bool) {
178-
r.mu.Lock()
179-
defer r.mu.Unlock()
180-
181-
for r.l == eight {
182-
if r.c == nil {
183-
r.c = sync.NewCond(&r.mu)
184-
}
185-
r.c.Wait()
186-
}
187-
188-
r.elems[r.tail] = sr
189-
r.tail = (r.tail + 1) & mask7
190-
r.l++
191-
192-
return r.l == 1
193-
}
194-
195-
func (r *ringSeqResp) dropPeek() (next *seqResp, more bool) {
196-
r.mu.Lock()
197-
defer r.mu.Unlock()
198-
199-
r.elems[r.head] = nil
200-
r.head = (r.head + 1) & mask7
201-
r.l--
202-
203-
if r.c != nil {
204-
r.c.Signal()
205-
}
206-
207-
return r.elems[r.head], r.l > 0
208-
}
209-
210-
// Also no die; this type is slightly different because we can have overflow.
211-
// If we have overflow, we add to overflow until overflow is drained -- we
212-
// always want strict odering.
213-
type ringBatchPromise struct {
214-
mu sync.Mutex
215-
216-
elems [eight]batchPromise
217-
218-
head uint8
219-
tail uint8
220-
l uint8
221-
222-
overflow []batchPromise
223-
}
224-
225-
func (r *ringBatchPromise) push(b batchPromise) (first bool) {
226-
r.mu.Lock()
227-
defer r.mu.Unlock()
228-
229141
// If the ring is full, we go into overflow; if overflow is non-empty,
230142
// for ordering purposes, we add to the end of overflow. We only go
231143
// back to using the ring once overflow is finally empty.
232144
if r.l == eight || len(r.overflow) > 0 {
233-
r.overflow = append(r.overflow, b)
234-
return false
145+
r.overflow = append(r.overflow, elem)
146+
return false, false
235147
}
236148

237-
r.elems[r.tail] = b
149+
r.elems[r.tail] = elem
238150
r.tail = (r.tail + 1) & mask7
239151
r.l++
240152

241-
return r.l == 1
153+
return r.l == 1, false
242154
}
243155

244-
func (r *ringBatchPromise) dropPeek() (next batchPromise, more bool) {
156+
func (r *unlimitedRing[T]) dropPeek() (next T, more, dead bool) {
245157
r.mu.Lock()
246158
defer r.mu.Unlock()
247159

248160
// We always drain the ring first. If the ring is ever empty, there
249161
// must be overflow: we would not be here if the ring is not-empty.
250162
if r.l > 1 {
251-
r.elems[r.head] = batchPromise{}
163+
var zero T
164+
r.elems[r.head] = zero
252165
r.head = (r.head + 1) & mask7
253166
r.l--
254-
return r.elems[r.head], true
167+
return r.elems[r.head], true, r.dead
255168
} else if r.l == 1 {
256-
r.elems[r.head] = batchPromise{}
169+
var zero T
170+
r.elems[r.head] = zero
257171
r.head = (r.head + 1) & mask7
258172
r.l--
259173
if len(r.overflow) == 0 {
260-
return next, false
174+
return next, false, r.dead
261175
}
262-
return r.overflow[0], true
176+
return r.overflow[0], true, r.dead
263177
}
264178
r.overflow = r.overflow[1:]
179+
180+
// If the overflow slice is 8x the fixed ring size, and the overflow length
181+
// is less than 1/8 of the capacity, then we do shrink the overflow slice
182+
// to 2x the currently required capacity. This prevents edge cases where
183+
// the overflow slice could grow indefinitely.
184+
if cap(r.overflow) > 64 && len(r.overflow) < cap(r.overflow)/8 {
185+
shrunk := make([]T, len(r.overflow), len(r.overflow)*2)
186+
copy(shrunk, r.overflow)
187+
r.overflow = shrunk
188+
}
189+
265190
if len(r.overflow) > 0 {
266-
return r.overflow[0], true
191+
return r.overflow[0], true, r.dead
267192
}
268-
return next, false
193+
return next, false, r.dead
269194
}

0 commit comments

Comments
 (0)