Skip to content

Commit bf3760d

Browse files
authored
refactor(flowcontrol): single-ended SafeQueue backed by container/heap (#1690)
* refactor: single-ended queue backed by heap The dispatch SafeQueue was double-ended, but nothing consumed PeekTail other than the flowQueueAccessor passthrough itself: dispatch needs only the head, and eviction uses an independent ordering. With PeekTail gone the queue is single-ended, so the bespoke max-min heap that gave O(1) access to both ends is no longer justified. - Remove the unused PeekTail from the SafeQueue contract and all implementers, mocks, and tests; rename PeekHead -> Peek now that there is a single end. - Replace the hand-rolled max-min heap (maxminheap.go) with a standard library container/heap priority queue (priorityqueue.go), eliminating the custom bubble-up/down logic and the latent bug class it carried. - Drop the side handle map: membership is validated by index identity (a *heapItem lives in exactly one queue), preserving the contract's Remove error semantics with less state. O(log n) targeted removal is retained via per-item index tracking. - Rename the registered queue MaxMinHeap -> PriorityQueue. Signed-off-by: Luke Van Drie <lukevandrie@google.com> * refactor(flowcontrol): apply review feedback on the priority queue Addresses review comments from @shmuelk on #1690: - Correct the new-file license headers to "2026 The llm-d Authors". - Clarify the PriorityQueue doc: ordering is maintained by an internal container/heap (the queue does not itself implement heap.Interface). - Narrow the Add and Remove critical sections to the shared heap mutation. Precondition checks (nil, type assertion) and the atomic byteSize update move outside the lock; the isInvalidated and index reads stay inside it, since those fields are written under the lock by Remove/Cleanup/Drain and reading them lock-free would race. - Compact Cleanup survivors in place instead of allocating a second slice, clearing the vacated tail so removed items are not retained. Signed-off-by: Luke Van Drie <lukevandrie@google.com> --------- Signed-off-by: Luke Van Drie <lukevandrie@google.com>
1 parent 546ea56 commit bf3760d

22 files changed

Lines changed: 405 additions & 737 deletions

File tree

pkg/epp/flowcontrol/contracts/mocks/mocks.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,7 @@ type MockSafeQueue struct {
163163
CapabilitiesV []flowcontrol.QueueCapability
164164
LenV int
165165
ByteSizeV uint64
166-
PeekHeadV flowcontrol.QueueItemAccessor
167-
PeekTailV flowcontrol.QueueItemAccessor
166+
PeekV flowcontrol.QueueItemAccessor
168167
AddFunc func(item flowcontrol.QueueItemAccessor)
169168
RemoveFunc func(handle flowcontrol.QueueItemHandle) (flowcontrol.QueueItemAccessor, error)
170169
CleanupFunc func(predicate contracts.PredicateFunc) []flowcontrol.QueueItemAccessor
@@ -176,12 +175,8 @@ func (m *MockSafeQueue) Capabilities() []flowcontrol.QueueCapability { return m.
176175
func (m *MockSafeQueue) Len() int { return m.LenV }
177176
func (m *MockSafeQueue) ByteSize() uint64 { return m.ByteSizeV }
178177

179-
func (m *MockSafeQueue) PeekHead() flowcontrol.QueueItemAccessor {
180-
return m.PeekHeadV
181-
}
182-
183-
func (m *MockSafeQueue) PeekTail() flowcontrol.QueueItemAccessor {
184-
return m.PeekTailV
178+
func (m *MockSafeQueue) Peek() flowcontrol.QueueItemAccessor {
179+
return m.PeekV
185180
}
186181

187182
func (m *MockSafeQueue) Add(item flowcontrol.QueueItemAccessor) {
@@ -366,8 +361,8 @@ func (m *MockManagedQueue) ByteSize() uint64 {
366361
return size
367362
}
368363

369-
// PeekHead returns the first item found in the mock queue. Note: map iteration order is not guaranteed.
370-
func (m *MockManagedQueue) PeekHead() flowcontrol.QueueItemAccessor {
364+
// Peek returns the first item found in the mock queue. Note: map iteration order is not guaranteed.
365+
func (m *MockManagedQueue) Peek() flowcontrol.QueueItemAccessor {
371366
m.mu.Lock()
372367
defer m.mu.Unlock()
373368
m.init()
@@ -376,8 +371,3 @@ func (m *MockManagedQueue) PeekHead() flowcontrol.QueueItemAccessor {
376371
}
377372
return nil // Queue is empty
378373
}
379-
380-
// PeekTail is not implemented for this mock.
381-
func (m *MockManagedQueue) PeekTail() flowcontrol.QueueItemAccessor {
382-
return nil
383-
}

pkg/epp/flowcontrol/controller/internal/processor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ func (sp *Processor) selectItem(
413413
}
414414
// The queue itself is responsible for explicit ordering via its configured OrderingPolicy.
415415
// We simply peek at the head.
416-
return queue.PeekHead(), nil
416+
return queue.Peek(), nil
417417
}
418418

419419
// dispatchItem handles the final steps of dispatching an item: removing it from the queue and finalizing its outcome.

pkg/epp/flowcontrol/framework/plugins/queue/benchmark_test.go

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ func BenchmarkQueues(b *testing.B) {
4646
benchmarkAddPeekRemove(b, q)
4747
})
4848

49-
b.Run("AddPeekTailRemove", func(b *testing.B) {
50-
benchmarkAddPeekTailRemove(b, q)
51-
})
52-
5349
b.Run("BulkAddThenBulkRemove", func(b *testing.B) {
5450
benchmarkBulkAddThenBulkRemove(b, q)
5551
})
@@ -79,10 +75,10 @@ func benchmarkAddRemove(b *testing.B, q contracts.SafeQueue) {
7975
})
8076
}
8177

82-
// benchmarkAddPeekRemove measures the throughput of a serial Add, PeekHead, and Remove sequence. This simulates a
78+
// benchmarkAddPeekRemove measures the throughput of a serial Add, Peek, and Remove sequence. This simulates a
8379
// common consumer pattern where a single worker peeks at an item before deciding to process and remove it.
8480
func benchmarkAddPeekRemove(b *testing.B, q contracts.SafeQueue) {
85-
// Pre-add one item so PeekHead doesn't fail on the first iteration.
81+
// Pre-add one item so Peek doesn't fail on the first iteration.
8682
initialItem := mocks.NewMockQueueItemAccessor(1, "initial", benchmarkFlowKey)
8783
q.Add(initialItem)
8884

@@ -91,11 +87,11 @@ func benchmarkAddPeekRemove(b *testing.B, q contracts.SafeQueue) {
9187
for b.Loop() {
9288
item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
9389
q.Add(item)
94-
peeked := q.PeekHead()
90+
peeked := q.Peek()
9591
if peeked == nil {
9692
// In a concurrent benchmark, this could happen if the queue becomes empty.
9793
// In a serial one, it's a fatal error.
98-
b.Fatal("PeekHead failed")
94+
b.Fatal("Peek failed")
9995
}
10096

10197
_, err := q.Remove(peeked.Handle())
@@ -121,9 +117,9 @@ func benchmarkBulkAddThenBulkRemove(b *testing.B, q contracts.SafeQueue) {
121117

122118
// Remove the same number of items
123119
for range items {
124-
peeked := q.PeekHead()
120+
peeked := q.Peek()
125121
if peeked == nil {
126-
b.Fatal("PeekHead failed")
122+
b.Fatal("Peek failed")
127123
}
128124
if _, err := q.Remove(peeked.Handle()); err != nil {
129125
b.Fatalf("Remove failed: %v", err)
@@ -132,31 +128,6 @@ func benchmarkBulkAddThenBulkRemove(b *testing.B, q contracts.SafeQueue) {
132128
}
133129
}
134130

135-
// benchmarkAddPeekTailRemove measures the throughput of a serial Add, PeekTail, and Remove sequence. This is useful for
136-
// understanding the performance of accessing the lowest-priority item.
137-
func benchmarkAddPeekTailRemove(b *testing.B, q contracts.SafeQueue) {
138-
// Pre-add one item so PeekTail doesn't fail on the first iteration.
139-
initialItem := mocks.NewMockQueueItemAccessor(1, "initial", benchmarkFlowKey)
140-
q.Add(initialItem)
141-
142-
b.ReportAllocs()
143-
144-
for b.Loop() {
145-
item := mocks.NewMockQueueItemAccessor(1, "item", benchmarkFlowKey)
146-
q.Add(item)
147-
148-
peeked := q.PeekTail()
149-
if peeked == nil {
150-
b.Fatal("PeekTail failed")
151-
}
152-
153-
_, err := q.Remove(peeked.Handle())
154-
if err != nil {
155-
b.Fatalf("Remove failed: %v", err)
156-
}
157-
}
158-
}
159-
160131
// benchmarkHighContention simulates a more realistic workload with multiple producers and consumers operating on the
161132
// queue concurrently.
162133
func benchmarkHighContention(b *testing.B, q contracts.SafeQueue) {
@@ -190,7 +161,7 @@ func benchmarkHighContention(b *testing.B, q contracts.SafeQueue) {
190161
// Consumers drive the benchmark.
191162
b.RunParallel(func(pb *testing.PB) {
192163
for pb.Next() {
193-
peeked := q.PeekHead()
164+
peeked := q.Peek()
194165
if peeked != nil {
195166
_, _ = q.Remove(peeked.Handle())
196167
}

pkg/epp/flowcontrol/framework/plugins/queue/functional_test.go

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var reverseEnqueueTimePolicy = &mocks.MockOrderingPolicy{
5858
},
5959
}
6060

61-
// testLifecycleAndOrdering is a helper function to execute a standard sequence of Add, PeekHead, and Remove operations
61+
// testLifecycleAndOrdering is a helper function to execute a standard sequence of Add, Peek, and Remove operations
6262
// on a queue. It verifies the queue's state (length, byte size) and item ordering based on the provided `itemsInOrder`
6363
// slice, which should be pre-sorted according to the `comparatorName` (which is just a string for
6464
// logging/identification).
@@ -71,11 +71,9 @@ func testLifecycleAndOrdering(
7171
) {
7272
t.Helper()
7373

74-
// PeekHead/PeekTail on empty queue
75-
peeked := q.PeekHead()
76-
assert.Nil(t, peeked, "[%s] PeekHead on empty queue should return a nil item", comparatorName)
77-
peeked = q.PeekTail()
78-
assert.Nil(t, peeked, "[%s] PeekTail on empty queue should return a nil item", comparatorName)
74+
// Peek on empty queue
75+
peeked := q.Peek()
76+
assert.Nil(t, peeked, "[%s] Peek on empty queue should return a nil item", comparatorName)
7977

8078
// Add items
8179
currentExpectedLen := 0
@@ -112,28 +110,20 @@ func testLifecycleAndOrdering(
112110
expectedLen := initialLen
113111
expectedByteSize := expectedTotalByteSize
114112
for i, expectedItem := range itemsInOrder {
115-
// Verify PeekHead
116-
peeked = q.PeekHead()
117-
require.NotNil(t, peeked, "[%s] PeekHead should return a non-nil item (iteration %d)", comparatorName, i)
113+
// Verify Peek
114+
peeked = q.Peek()
115+
require.NotNil(t, peeked, "[%s] Peek should return a non-nil item (iteration %d)", comparatorName, i)
118116
assert.Equal(t, expectedItem.OriginalRequest().ID(), peeked.OriginalRequest().ID(),
119-
"[%s] PeekHead must return the item (ID: %s) at the head of the queue (iteration %d)",
117+
"[%s] Peek must return the item (ID: %s) at the head of the queue (iteration %d)",
120118
comparatorName, expectedItem.OriginalRequest().ID(), i)
121119
peekedHandle := peeked.Handle()
122120
require.NotNil(t, peekedHandle, "[%s] Handle from a peeked item must not be nil (iteration %d)", comparatorName, i)
123121
require.False(t, peekedHandle.IsInvalidated(),
124122
"[%s] Handle from a peeked item must not be invalidated (iteration %d)", comparatorName, i)
125-
assert.Equal(t, expectedLen, q.Len(), "[%s] Len() must be unchanged after PeekHead (iteration %d)",
123+
assert.Equal(t, expectedLen, q.Len(), "[%s] Len() must be unchanged after Peek (iteration %d)",
126124
comparatorName, i)
127125
assert.Equal(t, expectedByteSize, q.ByteSize(),
128-
"[%s] ByteSize() must be unchanged after PeekHead (iteration %d)", comparatorName, i)
129-
130-
// Verify PeekTail
131-
peekedTail := q.PeekTail()
132-
require.NotNil(t, peekedTail, "[%s] PeekTail should return a non-nil item (iteration %d)", comparatorName, i)
133-
// The tail is the last item in the *remaining* ordered slice.
134-
expectedTailItem := itemsInOrder[len(itemsInOrder)-1]
135-
assert.Equal(t, expectedTailItem.OriginalRequest().ID(), peekedTail.OriginalRequest().ID(),
136-
"[%s] PeekTail must return the item with the lowest priority (iteration %d)", comparatorName, i)
126+
"[%s] ByteSize() must be unchanged after Peek (iteration %d)", comparatorName, i)
137127

138128
// Remove the head item
139129
removed, removeErr := q.Remove(peekedHandle)
@@ -156,8 +146,8 @@ func testLifecycleAndOrdering(
156146
assert.Zero(t, q.Len(), "[%s] Queue length should be 0 after all items are removed", comparatorName)
157147
assert.Zero(t, q.ByteSize(), "[%s] Queue byte size should be 0 after all items are removed", comparatorName)
158148

159-
peeked = q.PeekHead()
160-
assert.Nil(t, peeked, "[%s] PeekHead on an empty queue should return a nil item again", comparatorName)
149+
peeked = q.Peek()
150+
assert.Nil(t, peeked, "[%s] Peek on an empty queue should return a nil item again", comparatorName)
161151
}
162152

163153
// TestQueueConformance is the main conformance test suite for SafeQueue implementations.
@@ -404,7 +394,7 @@ func TestQueueConformance(t *testing.T) {
404394
// Verify remaining items are correct
405395
var remainingIDs []string
406396
for q.Len() > 0 {
407-
peeked := q.PeekHead()
397+
peeked := q.Peek()
408398
item, _ := q.Remove(peeked.Handle())
409399
remainingIDs = append(remainingIDs, item.OriginalRequest().ID())
410400
}
@@ -517,13 +507,9 @@ func TestQueueConformance(t *testing.T) {
517507
case 2: // Inspect
518508
_ = q.Len()
519509
_ = q.ByteSize()
520-
peeked := q.PeekHead()
521-
if q.Len() == 0 {
522-
assert.Nil(t, peeked, "PeekHead on empty queue expected nil")
523-
}
524-
peeked = q.PeekTail()
510+
peeked := q.Peek()
525511
if q.Len() == 0 {
526-
assert.Nil(t, peeked, "PeekTail on empty queue expected nil")
512+
assert.Nil(t, peeked, "Peek on empty queue expected nil")
527513
}
528514
case 3: // Cleanup
529515
q.Cleanup(func(item flowcontrol.QueueItemAccessor) bool { return false })

pkg/epp/flowcontrol/framework/plugins/queue/listqueue.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,8 @@ func (lq *listQueue) ByteSize() uint64 {
202202
return lq.byteSize.Load()
203203
}
204204

205-
// PeekHead returns the item at the front of the queue without removing it.
206-
func (lq *listQueue) PeekHead() flowcontrol.QueueItemAccessor {
205+
// Peek returns the item at the front of the queue without removing it.
206+
func (lq *listQueue) Peek() flowcontrol.QueueItemAccessor {
207207
lq.mu.RLock()
208208
defer lq.mu.RUnlock()
209209

@@ -213,15 +213,3 @@ func (lq *listQueue) PeekHead() flowcontrol.QueueItemAccessor {
213213
element := lq.requests.Front()
214214
return element.Value.(flowcontrol.QueueItemAccessor)
215215
}
216-
217-
// PeekTail returns the item at the back of the queue without removing it.
218-
func (lq *listQueue) PeekTail() flowcontrol.QueueItemAccessor {
219-
lq.mu.RLock()
220-
defer lq.mu.RUnlock()
221-
222-
if lq.requests.Len() == 0 {
223-
return nil
224-
}
225-
element := lq.requests.Back()
226-
return element.Value.(flowcontrol.QueueItemAccessor)
227-
}

0 commit comments

Comments
 (0)