master
go 273 lines 5.96 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package functions
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11 )
12
13 func TestKeyScheduler_CompleteSkipsInvalidQueuedEntries(t *testing.T) {
14 tests := map[string]struct {
15 setup func(*keyScheduler)
16 check func(*testing.T, *keyScheduler)
17 }{
18 "promotes next valid request after invalid queue heads": {
19 setup: func(s *keyScheduler) {
20 s.pending = 3
21 s.lanes["k"] = &scheduleLane{
22 ownerUID: "owner",
23 queue: []*invocationRequest{
24 nil,
25 {},
26 {
27 fn: &Function{UID: "next"},
28 scheduleKey: "k",
29 },
30 },
31 }
32 },
33 check: func(t *testing.T, s *keyScheduler) {
34 t.Helper()
35 lane, ok := s.lanes["k"]
36 require.True(t, ok)
37 require.NotNil(t, lane)
38 assert.Equal(t, "next", lane.ownerUID)
39 assert.Empty(t, lane.queue)
40 require.Len(t, s.ready, 1)
41 assert.Equal(t, "next", s.ready[0].fn.UID)
42 assert.Equal(t, 1, s.pending)
43
44 req, ok := s.next()
45 require.True(t, ok)
46 require.NotNil(t, req)
47 assert.Equal(t, "next", req.fn.UID)
48 assert.Equal(t, 0, s.pending)
49 },
50 },
51 "drops invalid-only queued entries without pending leak": {
52 setup: func(s *keyScheduler) {
53 s.pending = 2
54 s.lanes["k"] = &scheduleLane{
55 ownerUID: "owner",
56 queue: []*invocationRequest{
57 nil,
58 {},
59 },
60 }
61 },
62 check: func(t *testing.T, s *keyScheduler) {
63 t.Helper()
64 _, ok := s.lanes["k"]
65 assert.False(t, ok)
66 assert.Empty(t, s.ready)
67 assert.Equal(t, 0, s.pending)
68 },
69 },
70 }
71
72 for name, tc := range tests {
73 t.Run(name, func(t *testing.T) {
74 s := newKeyScheduler(10)
75 tc.setup(s)
76 s.complete("k", "owner")
77 tc.check(t, s)
78 })
79 }
80 }
81
82 func TestKeyScheduler_EnqueueValidation(t *testing.T) {
83 tests := map[string]struct {
84 req *invocationRequest
85 adjust func(*keyScheduler)
86 want error
87 }{
88 "nil request returns invalid error": {
89 req: nil,
90 want: errSchedulerInvalid,
91 },
92 "nil function returns invalid error": {
93 req: &invocationRequest{
94 scheduleKey: "k",
95 },
96 want: errSchedulerInvalid,
97 },
98 "empty uid returns invalid error": {
99 req: &invocationRequest{
100 fn: &Function{UID: ""},
101 scheduleKey: "k",
102 },
103 want: errSchedulerInvalid,
104 },
105 "empty schedule key returns invalid error": {
106 req: &invocationRequest{
107 fn: &Function{UID: "tx1"},
108 scheduleKey: "",
109 },
110 want: errSchedulerInvalid,
111 },
112 "stopping scheduler returns stopping error": {
113 req: &invocationRequest{
114 fn: &Function{UID: "tx1"},
115 scheduleKey: "k",
116 },
117 adjust: func(s *keyScheduler) { s.stopAccepting() },
118 want: errSchedulerStopping,
119 },
120 "valid request is admitted": {
121 req: &invocationRequest{
122 fn: &Function{UID: "tx1"},
123 scheduleKey: "k",
124 },
125 want: nil,
126 },
127 }
128
129 for name, tc := range tests {
130 t.Run(name, func(t *testing.T) {
131 s := newKeyScheduler(1)
132 if tc.adjust != nil {
133 tc.adjust(s)
134 }
135
136 err := s.enqueue(tc.req)
137 if tc.want == nil {
138 require.NoError(t, err)
139 return
140 }
141 require.ErrorIs(t, err, tc.want)
142 })
143 }
144 }
145
146 func TestKeyScheduler_StopPaths(t *testing.T) {
147 tests := map[string]struct {
148 run func(t *testing.T)
149 }{
150 "stopAccepting on drained scheduler makes next return false": {
151 run: func(t *testing.T) {
152 s := newKeyScheduler(1)
153 s.stopAccepting()
154
155 req, ok := s.next()
156 assert.False(t, ok)
157 assert.Nil(t, req)
158 },
159 },
160 "stop wakes blocked next waiter": {
161 run: func(t *testing.T) {
162 s := newKeyScheduler(1)
163 done := make(chan struct{})
164
165 go func() {
166 defer close(done)
167 req, ok := s.next()
168 assert.False(t, ok)
169 assert.Nil(t, req)
170 }()
171
172 time.Sleep(20 * time.Millisecond)
173 s.stop()
174
175 select {
176 case <-done:
177 case <-time.After(time.Second):
178 t.Fatal("timed out waiting for next() waiter to exit after stop()")
179 }
180 },
181 },
182 }
183
184 for name, tc := range tests {
185 t.Run(name, tc.run)
186 }
187 }
188
189 func TestKeyScheduler_EnqueueBlocksUntilSpace(t *testing.T) {
190 tests := map[string]struct {
191 run func(t *testing.T)
192 }{
193 "enqueue blocks until next() frees space": {
194 run: func(t *testing.T) {
195 s := newKeyScheduler(1)
196 req1 := &invocationRequest{
197 fn: &Function{UID: "tx1"},
198 scheduleKey: "k1",
199 }
200 req2 := &invocationRequest{
201 fn: &Function{UID: "tx2"},
202 scheduleKey: "k2",
203 }
204
205 require.NoError(t, s.enqueue(req1))
206
207 enqueued := make(chan error, 1)
208 go func() {
209 enqueued <- s.enqueue(req2)
210 }()
211
212 require.Eventually(t, func() bool {
213 return s.enqueueWaiterCount() == 1
214 }, time.Second, time.Millisecond, "second enqueue never reached blocking wait")
215
216 select {
217 case <-enqueued:
218 t.Fatal("second enqueue should still be blocked while queue is full")
219 default:
220 }
221
222 got, ok := s.next()
223 require.True(t, ok)
224 require.NotNil(t, got)
225 require.Equal(t, "tx1", got.fn.UID)
226
227 select {
228 case err := <-enqueued:
229 require.NoError(t, err)
230 case <-time.After(time.Second):
231 t.Fatal("second enqueue did not unblock after space freed")
232 }
233 },
234 },
235 "stop unblocks waiting enqueue with stopping error": {
236 run: func(t *testing.T) {
237 s := newKeyScheduler(1)
238 req1 := &invocationRequest{
239 fn: &Function{UID: "tx1"},
240 scheduleKey: "k1",
241 }
242 req2 := &invocationRequest{
243 fn: &Function{UID: "tx2"},
244 scheduleKey: "k2",
245 }
246
247 require.NoError(t, s.enqueue(req1))
248
249 enqueued := make(chan error, 1)
250 go func() {
251 enqueued <- s.enqueue(req2)
252 }()
253
254 require.Eventually(t, func() bool {
255 return s.enqueueWaiterCount() == 1
256 }, time.Second, time.Millisecond, "second enqueue never reached blocking wait")
257
258 s.stop()
259
260 select {
261 case err := <-enqueued:
262 require.ErrorIs(t, err, errSchedulerStopping)
263 case <-time.After(time.Second):
264 t.Fatal("blocked enqueue did not return after stop()")
265 }
266 },
267 },
268 }
269
270 for name, tc := range tests {
271 t.Run(name, tc.run)
272 }
273 }