master
go 246 lines 4.92 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package functions
4
5 import (
6 "errors"
7 "sync"
8 )
9
10 var (
11 errSchedulerStopping = errors.New("scheduler is stopping")
12 errSchedulerInvalid = errors.New("scheduler invalid request")
13 )
14
15 type scheduleLane struct {
16 ownerUID string
17 queue []*invocationRequest
18 }
19
20 // keyScheduler serializes execution by schedule key while allowing concurrency
21 // across different keys.
22 type keyScheduler struct {
23 mux sync.Mutex
24
25 cond *sync.Cond
26
27 ready []*invocationRequest
28 lanes map[string]*scheduleLane
29
30 maxPending int
31 pending int
32 accepting bool
33 stopping bool
34
35 // enqueueWaiters counts goroutines currently blocked inside enqueue()
36 // waiting for space. Used by tests to synchronize deterministically
37 // instead of sleeping.
38 enqueueWaiters int
39 }
40
41 func newKeyScheduler(maxPending int) *keyScheduler {
42 s := &keyScheduler{
43 lanes: make(map[string]*scheduleLane),
44 maxPending: maxPending,
45 accepting: true,
46 }
47 s.cond = sync.NewCond(&s.mux)
48 return s
49 }
50
51 func (s *keyScheduler) enqueue(req *invocationRequest) error {
52 if req == nil || req.fn == nil || req.fn.UID == "" || req.scheduleKey == "" {
53 return errSchedulerInvalid
54 }
55
56 s.mux.Lock()
57 defer s.mux.Unlock()
58
59 // Block until there is space, or the scheduler is stopped. We must not
60 // drop dyncfg commands silently: an awaited enable/disable that is dropped
61 // here would wedge jobmgr's wait gate by leaving it waiting for a
62 // completion that will never arrive. Back-pressure flows upstream: the
63 // manager run-loop stops draining stdin, and netdata's write blocks on
64 // the OS pipe.
65 for {
66 if s.stopping || !s.accepting {
67 return errSchedulerStopping
68 }
69 if s.maxPending <= 0 || s.pending < s.maxPending {
70 break
71 }
72 s.enqueueWaiters++
73 s.cond.Wait()
74 s.enqueueWaiters--
75 }
76
77 lane := s.lanes[req.scheduleKey]
78 if lane == nil {
79 lane = &scheduleLane{}
80 s.lanes[req.scheduleKey] = lane
81 }
82
83 s.pending++
84 if lane.ownerUID == "" {
85 lane.ownerUID = req.fn.UID
86 s.ready = append(s.ready, req)
87 s.cond.Signal()
88 return nil
89 }
90
91 lane.queue = append(lane.queue, req)
92 return nil
93 }
94
95 func (s *keyScheduler) next() (*invocationRequest, bool) {
96 s.mux.Lock()
97 defer s.mux.Unlock()
98
99 for len(s.ready) == 0 && !s.stopping {
100 if s.drainedLocked() {
101 return nil, false
102 }
103 s.cond.Wait()
104 }
105
106 if len(s.ready) == 0 || s.stopping {
107 return nil, false
108 }
109
110 req := s.ready[0]
111 s.ready = s.ready[1:]
112 if s.pending > 0 {
113 s.pending--
114 }
115 // Wake any enqueue() waiters blocked on a full queue.
116 s.cond.Broadcast()
117 return req, true
118 }
119
120 func (s *keyScheduler) cancelQueued(scheduleKey, uid string) bool {
121 if scheduleKey == "" || uid == "" {
122 return false
123 }
124
125 s.mux.Lock()
126 defer s.mux.Unlock()
127
128 lane := s.lanes[scheduleKey]
129 if lane == nil || len(lane.queue) == 0 {
130 return false
131 }
132
133 for i, req := range lane.queue {
134 if req == nil || req.fn == nil || req.fn.UID != uid {
135 continue
136 }
137
138 copy(lane.queue[i:], lane.queue[i+1:])
139 lane.queue = lane.queue[:len(lane.queue)-1]
140 if s.pending > 0 {
141 s.pending--
142 }
143
144 if lane.ownerUID == "" && len(lane.queue) == 0 {
145 delete(s.lanes, scheduleKey)
146 }
147 // Wake any enqueue() waiters blocked on a full queue.
148 s.cond.Broadcast()
149 return true
150 }
151 return false
152 }
153
154 func (s *keyScheduler) complete(scheduleKey, uid string) {
155 if scheduleKey == "" || uid == "" {
156 return
157 }
158
159 s.mux.Lock()
160 defer s.mux.Unlock()
161
162 lane := s.lanes[scheduleKey]
163 if lane == nil || lane.ownerUID != uid {
164 return
165 }
166
167 if s.removeReadyLocked(uid) && s.pending > 0 {
168 s.pending--
169 }
170
171 if s.stopping {
172 delete(s.lanes, scheduleKey)
173 s.cond.Broadcast()
174 return
175 }
176
177 for len(lane.queue) > 0 {
178 next := lane.queue[0]
179 lane.queue = lane.queue[1:]
180 if next == nil || next.fn == nil {
181 if s.pending > 0 {
182 s.pending--
183 }
184 continue
185 }
186
187 lane.ownerUID = next.fn.UID
188 s.ready = append(s.ready, next)
189 // Broadcast: wakes both next() consumers and enqueue() producers.
190 s.cond.Broadcast()
191 return
192 }
193
194 lane.ownerUID = ""
195 if len(lane.queue) == 0 {
196 delete(s.lanes, scheduleKey)
197 }
198 s.cond.Broadcast()
199 }
200
201 func (s *keyScheduler) stopAccepting() {
202 s.mux.Lock()
203 s.accepting = false
204 if s.drainedLocked() {
205 s.cond.Broadcast()
206 }
207 s.mux.Unlock()
208 }
209
210 func (s *keyScheduler) stop() {
211 s.mux.Lock()
212 s.stopping = true
213 s.cond.Broadcast()
214 s.mux.Unlock()
215 }
216
217 func (s *keyScheduler) drainedLocked() bool {
218 return !s.accepting && s.pending == 0 && len(s.ready) == 0
219 }
220
221 func (s *keyScheduler) removeReadyLocked(uid string) bool {
222 for i, req := range s.ready {
223 if req == nil || req.fn == nil || req.fn.UID != uid {
224 continue
225 }
226
227 copy(s.ready[i:], s.ready[i+1:])
228 s.ready = s.ready[:len(s.ready)-1]
229 return true
230 }
231 return false
232 }
233
234 func (s *keyScheduler) pendingCount() int {
235 s.mux.Lock()
236 defer s.mux.Unlock()
237 return s.pending
238 }
239
240 // enqueueWaiterCount reports how many goroutines are currently blocked
241 // inside enqueue() waiting for queue space. Intended for tests.
242 func (s *keyScheduler) enqueueWaiterCount() int {
243 s.mux.Lock()
244 defer s.mux.Unlock()
245 return s.enqueueWaiters
246 }