vxx2 / vlib / x / async / pool.v
262 lines · 245 sloc · 6.41 KB · a66aa5b0fe618b32988f4fd9b235017c336940ba
Raw
1module async
2
3import context
4import sync
5import time
6
7// PoolConfig configures a fixed-size concurrency pool.
8//
9// Both values must be positive. `workers` is the maximum number of jobs that
10// can execute concurrently; `queue_size` is the bounded backlog accepted by
11// try_submit() while all worker slots are busy.
12@[params]
13pub struct PoolConfig {
14pub:
15 workers int
16 queue_size int
17}
18
19// Pool limits concurrent JobFn execution with fixed worker slots and bounded backlog.
20//
21// The pool owns one derived context shared by all jobs. Closing the pool stops
22// new submissions, waits for every accepted job, and returns the first job error
23// if any.
24@[heap]
25pub struct Pool {
26mut:
27 ctx context.Context
28 cancel context.CancelFn = unsafe { nil }
29 tokens chan bool
30 submit_ready chan int
31 wg &sync.WaitGroup = sync.new_waitgroup()
32 max_jobs int
33 // Protects lifecycle flags, accepted job count, WaitGroup.add(), and
34 // first_err. This lock is never held while a user JobFn runs.
35 mutex &sync.Mutex = sync.new_mutex()
36 first_err IError = none
37 closed bool
38 waited bool
39 accepted int
40}
41
42// new_pool creates a Pool with a background parent context.
43pub fn new_pool(config PoolConfig) !&Pool {
44 return new_pool_with_context(context.background(), config)
45}
46
47// new_pool_with_context creates a Pool with a context derived from parent.
48//
49// The worker limit and queue size are fixed for the pool lifetime. Parent
50// cancellation is cooperative: jobs must observe `ctx.done()` and return.
51pub fn new_pool_with_context(parent context.Context, config PoolConfig) !&Pool {
52 if config.workers <= 0 {
53 return error(err_pool_workers_invalid)
54 }
55 if config.queue_size <= 0 {
56 return error(err_pool_queue_size_invalid)
57 }
58 ctx, cancel := new_cancel_context(parent)
59 mut pool := &Pool{
60 ctx: context.Context(ctx)
61 cancel: cancel
62 tokens: chan bool{cap: config.workers}
63 submit_ready: chan int{cap: 1}
64 wg: sync.new_waitgroup()
65 max_jobs: config.workers + config.queue_size
66 mutex: sync.new_mutex()
67 }
68 for _ in 0 .. config.workers {
69 pool.tokens <- true
70 }
71 return pool
72}
73
74// try_submit accepts f if the pool is open and its bounded backlog has capacity.
75//
76// It never blocks for queue space. A full backlog returns `async: pool queue is
77// full`, making backpressure explicit for callers.
78pub fn (mut p Pool) try_submit(f JobFn) ! {
79 if f == unsafe { nil } {
80 return error(err_nil_job)
81 }
82 p.mutex.lock()
83 if p.closed {
84 p.mutex.unlock()
85 return error(err_pool_closed)
86 }
87 if p.accepted >= p.max_jobs {
88 p.mutex.unlock()
89 return error(err_pool_queue_full)
90 }
91 p.accept_job_locked()
92 p.mutex.unlock()
93 // The JobFn is passed directly to the spawned wrapper instead of being
94 // stored in a channel. That keeps closure ownership with V's normal spawn
95 // path while the token channel below still enforces the fixed worker limit.
96 spawn run_pool_job(mut p, f)
97}
98
99// submit_with_context waits until f can be accepted, parent is canceled, or the pool closes.
100//
101// The parent context bounds admission only. Once f is accepted, the job runs with
102// the pool's own context, matching try_submit() and preserving pool lifecycle ownership.
103pub fn (mut p Pool) submit_with_context(parent context.Context, f JobFn) ! {
104 if f == unsafe { nil } {
105 return error(err_nil_job)
106 }
107 mut submit_ctx := parent
108 initial_err := submit_ctx.err()
109 if initial_err !is none {
110 return initial_err
111 }
112 done := submit_ctx.done()
113 mut watch_done := true
114 select {
115 _ := <-done {
116 err := submit_ctx.err()
117 if err !is none {
118 return err
119 }
120 watch_done = false
121 }
122 else {}
123 }
124 for {
125 p.mutex.lock()
126 if p.closed {
127 p.mutex.unlock()
128 return error(err_pool_closed)
129 }
130 if watch_done {
131 err := submit_ctx.err()
132 if err !is none {
133 p.mutex.unlock()
134 return err
135 }
136 }
137 if p.accepted < p.max_jobs {
138 p.accept_job_locked()
139 p.mutex.unlock()
140 spawn run_pool_job(mut p, f)
141 return
142 }
143 submit_ready := p.submit_ready
144 p.mutex.unlock()
145 if !watch_done {
146 _ := <-submit_ready
147 continue
148 }
149 select {
150 _ := <-submit_ready {
151 continue
152 }
153 _ := <-done {
154 err := submit_ctx.err()
155 if err !is none {
156 return err
157 }
158 watch_done = false
159 continue
160 }
161 }
162 }
163}
164
165// submit_with_timeout waits up to timeout for f to be accepted by the pool.
166//
167// The timeout bounds admission only. Accepted jobs still receive the pool context.
168pub fn (mut p Pool) submit_with_timeout(timeout time.Duration, f JobFn) ! {
169 timeout_ctx, cancel := new_timeout_context(context.background(), timeout)
170 mut submit_ctx := context.Context(timeout_ctx)
171 defer {
172 cancel()
173 }
174 p.submit_with_context(submit_ctx, f) or {
175 if err.msg() == context_deadline_exceeded && timeout_ctx.was_canceled_by_timeout() {
176 return error(err_timeout)
177 }
178 return err
179 }
180}
181
182// wait closes the pool to new submissions, drains accepted jobs, and waits for completion.
183//
184// wait is one-shot. It returns the first job error observed by any accepted job.
185pub fn (mut p Pool) wait() ! {
186 p.mutex.lock()
187 if p.waited {
188 p.mutex.unlock()
189 return error(err_pool_wait_called)
190 }
191 p.waited = true
192 p.closed = true
193 p.wake_submitters_locked()
194 p.mutex.unlock()
195
196 p.wg.wait()
197 p.cancel()
198 err := p.get_first_error()
199 if err !is none {
200 return err
201 }
202}
203
204// close is an explicit lifecycle alias for wait().
205//
206// It rejects later submissions and waits for all accepted jobs before returning.
207pub fn (mut p Pool) close() ! {
208 p.wait()!
209}
210
211fn run_pool_job(mut p Pool, f JobFn) {
212 defer {
213 p.finish_accepted_job()
214 p.wg.done()
215 }
216 // The token channel is a bounded semaphore. At most `workers` accepted jobs
217 // can pass this point and run user code concurrently.
218 _ := <-p.tokens
219 defer {
220 p.tokens <- true
221 }
222 mut job_ctx := p.ctx
223 f(mut job_ctx) or { p.set_first_error(err) }
224}
225
226fn (mut p Pool) finish_accepted_job() {
227 p.mutex.lock()
228 p.accepted--
229 p.wake_submitters_locked()
230 p.mutex.unlock()
231}
232
233fn (mut p Pool) accept_job_locked() {
234 p.accepted++
235 // add() is protected by the same mutex as wait(), so callers cannot race an
236 // accepted job against pool shutdown.
237 p.wg.add(1)
238}
239
240fn (mut p Pool) wake_submitters_locked() {
241 if !p.submit_ready.closed {
242 p.submit_ready.close()
243 }
244 if !p.closed {
245 p.submit_ready = chan int{cap: 1}
246 }
247}
248
249fn (mut p Pool) set_first_error(err IError) {
250 p.mutex.lock()
251 if p.first_err is none {
252 p.first_err = err
253 }
254 p.mutex.unlock()
255}
256
257fn (mut p Pool) get_first_error() IError {
258 p.mutex.lock()
259 err := p.first_err
260 p.mutex.unlock()
261 return err
262}
263