Skip to content

Commit 6235a2d

Browse files
committed
Preserve waitlist behavior with Snake
AI assisted with development. Every line of code was either written by or carefully reviewed by me :) Signed-off-by: Brett Wines <bwines@slack-corp.com>
1 parent 0eb41ba commit 6235a2d

4 files changed

Lines changed: 69 additions & 33 deletions

File tree

go/pools/smartconnpool/waitlist.go

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ type PoolConfig interface {
3333

3434
// waiter represents a client waiting for a connection in the waitlist
3535
type waiter[C Connection] struct {
36+
// setting is the connection Setting that we'd like, or nil if we'd like a
37+
// a connection with no Setting applied
38+
setting *Setting
3639
// conn is a channel that will receive the connection when it's ready
37-
conn chan *Pooled[C]
38-
request *loadshed.Request[*waiter[C]]
39-
err error
40+
conn chan *Pooled[C]
41+
err error
42+
// age is the amount of cycles this client has been on the waitlist
43+
age uint32
4044
}
4145

4246
type waitlist[C Connection] struct {
@@ -62,7 +66,7 @@ func (wl *waitlist[C]) waitForConn(ctx context.Context, setting *Setting, closeC
6266
defer wl.nodes.Put(elem)
6367

6468
conn := elem.conn
65-
*elem = waiter[C]{conn: conn}
69+
*elem = waiter[C]{conn: conn, setting: setting}
6670

6771
// Fast path: reject early using an atomic read of the list length to avoid
6872
// contending on the mutex under high query rates. This is racy — the count
@@ -99,9 +103,12 @@ func (wl *waitlist[C]) waitForConn(ctx context.Context, setting *Setting, closeC
99103
}
100104
return nil, ErrPoolWaiterCapReached
101105
}
102-
request, dropped := wl.snake.Enqueue(elem, valveID, snakePriority(priority))
103-
elem.request = request
104-
wl.syncTimer()
106+
if priority != loadshed.PriorityUndroppable {
107+
// Translate the Vitess proto priority (0 = most important) into Snake's
108+
// convention (higher priority shed last).
109+
priority = float64(sqlparser.MaxPriorityValue) - priority
110+
}
111+
request, dropped := wl.snake.Enqueue(elem, valveID, priority)
105112
wl.mu.Unlock()
106113
wl.reject(dropped)
107114

@@ -110,8 +117,7 @@ func (wl *waitlist[C]) waitForConn(ctx context.Context, setting *Setting, closeC
110117
// Pool was closed while we were waiting.
111118
wl.mu.Lock()
112119
// Try to find and remove ourselves from the list.
113-
removed, dropped := wl.snake.Cancel(elem.request)
114-
wl.syncTimer()
120+
removed, dropped := wl.snake.Cancel(request)
115121
wl.mu.Unlock()
116122
wl.reject(dropped)
117123

@@ -128,8 +134,7 @@ func (wl *waitlist[C]) waitForConn(ctx context.Context, setting *Setting, closeC
128134
// prevent another goroutine from trying to hand us a connection later on.
129135
wl.mu.Lock()
130136
// Try to find and remove ourselves from the list.
131-
removed, dropped := wl.snake.Cancel(elem.request)
132-
wl.syncTimer()
137+
removed, dropped := wl.snake.Cancel(request)
133138
wl.mu.Unlock()
134139
wl.reject(dropped)
135140

@@ -150,15 +155,9 @@ func (wl *waitlist[C]) aboveWaiterCap(maxWaiters uint) bool {
150155
return maxWaiters > 0 && wl.snake.Len() >= int(maxWaiters)
151156
}
152157

153-
func snakePriority(priority float64) float64 {
154-
if priority == loadshed.PriorityUndroppable {
155-
return priority
156-
}
157-
return float64(sqlparser.MaxPriorityValue) - priority
158-
}
159-
160158
func (wl *waitlist[C]) maybeStarvingCount() int {
161-
return wl.snake.Len()
159+
// TODO: Remove the age/starvation code since Snake guarantees prompt grant-or-shed.
160+
return 0
162161
}
163162

164163
// tryReturnConn tries handing over a connection to one of the waiters in the pool.
@@ -172,9 +171,25 @@ func (wl *waitlist[D]) tryReturnConn(conn *Pooled[D]) bool {
172171
}
173172

174173
func (wl *waitlist[D]) tryReturnConnSlow(conn *Pooled[D]) bool {
174+
const maxAge = 8
175+
connSetting := conn.Conn.Setting()
176+
175177
wl.mu.Lock()
176-
waiter, ok, dropped := wl.snake.Dequeue()
177-
wl.syncTimer()
178+
// iterate through the waitlist looking for either waiters that have been
179+
// here too long, or a waiter that is looking exactly for the same Setting
180+
// as the one we have in our connection.
181+
waiter, ok, dropped := wl.snake.DequeueMatching(func(waiter *waiter[D]) bool {
182+
if waiter.age > maxAge || waiter.setting == connSetting {
183+
return true
184+
}
185+
// this only ages the waiters that are being skipped over: we'll start
186+
// aging the waiters in the back once they get to the front of the pool.
187+
// the maxAge of 8 has been set empirically: smaller values cause clients
188+
// with a specific setting to slightly starve, and aging all the clients
189+
// in the list every time leads to unfairness when the system is at capacity
190+
waiter.age++
191+
return false
192+
})
178193
wl.mu.Unlock()
179194
wl.reject(dropped)
180195

@@ -200,16 +215,9 @@ func (wl *waitlist[C]) reject(waiters []*waiter[C]) {
200215
}
201216
}
202217

203-
func (wl *waitlist[C]) syncTimer() {
204-
if delay, ok := wl.snake.LockedTimerUpdate(); ok {
205-
time.AfterFunc(delay, wl.runDropTimer)
206-
}
207-
}
208-
209218
func (wl *waitlist[C]) runDropTimer() {
210219
wl.mu.Lock()
211220
dropped := wl.snake.LockedDropTimerFired()
212-
wl.syncTimer()
213221
wl.mu.Unlock()
214222
wl.reject(dropped)
215223
}
@@ -228,6 +236,7 @@ func (wl *waitlist[C]) init(poolName string, config PoolConfig) {
228236

229237
wl.snake = loadshed.NewSnake[*waiter[C]](loadshed.SnakeConfig{
230238
LoadsheddingAllowed: enabled,
239+
DropTimerFired: wl.runDropTimer,
231240
CoDel: loadshed.CoDelConfig{
232241
IntervalNs: func() int64 { return interval().Nanoseconds() },
233242
TargetNs: func() int64 { return target().Nanoseconds() },

go/pools/smartconnpool/waitlist_test.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,25 @@ func TestWaitlistShedsQueuedRequests(t *testing.T) {
139139
}
140140
}
141141

142-
func TestSnakePriority(t *testing.T) {
143-
assert.Equal(t, float64(100), snakePriority(0))
144-
assert.Equal(t, float64(70), snakePriority(30))
145-
assert.Equal(t, float64(0), snakePriority(100))
146-
assert.Equal(t, loadshed.PriorityUndroppable, snakePriority(loadshed.PriorityUndroppable))
142+
func TestWaitlistPreservesSettingAffinityAndAging(t *testing.T) {
143+
wl := waitlist[*TestConn]{}
144+
wl.init("", nil)
145+
146+
foo := &waiter[*TestConn]{setting: sFoo, conn: make(chan *Pooled[*TestConn], 1)}
147+
wl.snake.Enqueue(foo, "", loadshed.PriorityUndroppable)
148+
bar := &waiter[*TestConn]{setting: sBar, conn: make(chan *Pooled[*TestConn], 1)}
149+
wl.snake.Enqueue(bar, "", loadshed.PriorityUndroppable)
150+
conn := &Pooled[*TestConn]{Conn: &TestConn{setting: sBar}}
151+
152+
require.True(t, wl.tryReturnConn(conn))
153+
assert.Same(t, conn, <-bar.conn)
154+
assert.Equal(t, uint32(1), foo.age)
155+
assert.Equal(t, 0, wl.maybeStarvingCount())
156+
157+
foo.age = 9
158+
bar = &waiter[*TestConn]{setting: sBar, conn: make(chan *Pooled[*TestConn], 1)}
159+
wl.snake.Enqueue(bar, "", loadshed.PriorityUndroppable)
160+
161+
require.True(t, wl.tryReturnConn(conn))
162+
assert.Same(t, conn, <-foo.conn)
147163
}

go/vt/vttablet/tabletserver/loadshed/snake.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type (
4444
dropTimerExpectedNs int64
4545
cfg SnakeConfig
4646
clockFunc func() int64
47+
length atomic.Int64
4748

4849
shedCount atomic.Int64
4950
// shedByPriority breaks shedCount down by the shed request's priority label
@@ -111,6 +112,7 @@ func (s *Snake[T]) Enqueue(value T, valveID string, priority float64) (*Request[
111112

112113
req := s.q.lockedEnqueue(valveID, priority)
113114
req.value = value
115+
s.length.Add(1)
114116
if valveID != "" {
115117
s.lockedObserveValveDepth(valveID)
116118
}
@@ -142,6 +144,7 @@ func (s *Snake[T]) dequeue(match func(T) bool) (T, bool, []T) {
142144
if req != nil {
143145
s.q.lockedDequeue(req)
144146
req.signal(grantSentinel)
147+
s.length.Add(-1)
145148
now := s.clockFunc()
146149
s.lockedAccrueDropping(now)
147150
s.sojourn.Add(now - req.codelqEnqueuedAtNs)
@@ -155,11 +158,16 @@ func (s *Snake[T]) dequeue(match func(T) bool) (T, bool, []T) {
155158
return value, ok, s.droppedValues(pending)
156159
}
157160

161+
func (s *Snake[T]) Len() int {
162+
return int(s.length.Load())
163+
}
164+
158165
func (s *Snake[T]) Cancel(req *Request[T]) (bool, []T) {
159166
if req.signaledValue != nil {
160167
return false, nil
161168
}
162169
s.q.lockedCancel(req)
170+
s.length.Add(-1)
163171
var zero T
164172
req.value = zero
165173
dropped := s.q.lockedTakePendingDrops()
@@ -207,6 +215,7 @@ func (s *Snake[T]) droppedValues(requests []*Request[T]) []T {
207215
}
208216
values := make([]T, len(requests))
209217
for i, req := range requests {
218+
s.length.Add(-1)
210219
s.shedCount.Add(1)
211220
if s.shedByPriority != nil {
212221
s.shedByPriority.Add([]string{shedPriorityLabel(req.priority)}, 1)

go/vt/vttablet/tabletserver/query_executor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,8 @@ func (qre *QueryExecutor) getConn() (*connpool.PooledConn, error) {
827827
}(time.Now())
828828

829829
priority := float64(priorityFromOptions(qre.options, qre.tsv.config.TxThrottlerDefaultPriority))
830+
// Queries against a configured schema (e.g. performance_schema health
831+
// checks) are marked undroppable instead, so they are never shed.
830832
if matchesUndroppableSchema(qre.plan.SchemaQualifiers, qre.tsv.Config().LoadshedOltpRead.UndroppableSchemasValue()) {
831833
priority = loadshed.PriorityUndroppable
832834
}

0 commit comments

Comments
 (0)