Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -0,0 +1,262 @@
|
||||
module async
|
||||
|
||||
import context
|
||||
import sync
|
||||
import time
|
||||
|
||||
// PoolConfig configures a fixed-size concurrency pool.
|
||||
//
|
||||
// Both values must be positive. `workers` is the maximum number of jobs that
|
||||
// can execute concurrently; `queue_size` is the bounded backlog accepted by
|
||||
// try_submit() while all worker slots are busy.
|
||||
@[params]
|
||||
pub struct PoolConfig {
|
||||
pub:
|
||||
workers int
|
||||
queue_size int
|
||||
}
|
||||
|
||||
// Pool limits concurrent JobFn execution with fixed worker slots and bounded backlog.
|
||||
//
|
||||
// The pool owns one derived context shared by all jobs. Closing the pool stops
|
||||
// new submissions, waits for every accepted job, and returns the first job error
|
||||
// if any.
|
||||
@[heap]
|
||||
pub struct Pool {
|
||||
mut:
|
||||
ctx context.Context
|
||||
cancel context.CancelFn = unsafe { nil }
|
||||
tokens chan bool
|
||||
submit_ready chan int
|
||||
wg &sync.WaitGroup = sync.new_waitgroup()
|
||||
max_jobs int
|
||||
// Protects lifecycle flags, accepted job count, WaitGroup.add(), and
|
||||
// first_err. This lock is never held while a user JobFn runs.
|
||||
mutex &sync.Mutex = sync.new_mutex()
|
||||
first_err IError = none
|
||||
closed bool
|
||||
waited bool
|
||||
accepted int
|
||||
}
|
||||
|
||||
// new_pool creates a Pool with a background parent context.
|
||||
pub fn new_pool(config PoolConfig) !&Pool {
|
||||
return new_pool_with_context(context.background(), config)
|
||||
}
|
||||
|
||||
// new_pool_with_context creates a Pool with a context derived from parent.
|
||||
//
|
||||
// The worker limit and queue size are fixed for the pool lifetime. Parent
|
||||
// cancellation is cooperative: jobs must observe `ctx.done()` and return.
|
||||
pub fn new_pool_with_context(parent context.Context, config PoolConfig) !&Pool {
|
||||
if config.workers <= 0 {
|
||||
return error(err_pool_workers_invalid)
|
||||
}
|
||||
if config.queue_size <= 0 {
|
||||
return error(err_pool_queue_size_invalid)
|
||||
}
|
||||
ctx, cancel := new_cancel_context(parent)
|
||||
mut pool := &Pool{
|
||||
ctx: context.Context(ctx)
|
||||
cancel: cancel
|
||||
tokens: chan bool{cap: config.workers}
|
||||
submit_ready: chan int{cap: 1}
|
||||
wg: sync.new_waitgroup()
|
||||
max_jobs: config.workers + config.queue_size
|
||||
mutex: sync.new_mutex()
|
||||
}
|
||||
for _ in 0 .. config.workers {
|
||||
pool.tokens <- true
|
||||
}
|
||||
return pool
|
||||
}
|
||||
|
||||
// try_submit accepts f if the pool is open and its bounded backlog has capacity.
|
||||
//
|
||||
// It never blocks for queue space. A full backlog returns `async: pool queue is
|
||||
// full`, making backpressure explicit for callers.
|
||||
pub fn (mut p Pool) try_submit(f JobFn) ! {
|
||||
if f == unsafe { nil } {
|
||||
return error(err_nil_job)
|
||||
}
|
||||
p.mutex.lock()
|
||||
if p.closed {
|
||||
p.mutex.unlock()
|
||||
return error(err_pool_closed)
|
||||
}
|
||||
if p.accepted >= p.max_jobs {
|
||||
p.mutex.unlock()
|
||||
return error(err_pool_queue_full)
|
||||
}
|
||||
p.accept_job_locked()
|
||||
p.mutex.unlock()
|
||||
// The JobFn is passed directly to the spawned wrapper instead of being
|
||||
// stored in a channel. That keeps closure ownership with V's normal spawn
|
||||
// path while the token channel below still enforces the fixed worker limit.
|
||||
spawn run_pool_job(mut p, f)
|
||||
}
|
||||
|
||||
// submit_with_context waits until f can be accepted, parent is canceled, or the pool closes.
|
||||
//
|
||||
// The parent context bounds admission only. Once f is accepted, the job runs with
|
||||
// the pool's own context, matching try_submit() and preserving pool lifecycle ownership.
|
||||
pub fn (mut p Pool) submit_with_context(parent context.Context, f JobFn) ! {
|
||||
if f == unsafe { nil } {
|
||||
return error(err_nil_job)
|
||||
}
|
||||
mut submit_ctx := parent
|
||||
initial_err := submit_ctx.err()
|
||||
if initial_err !is none {
|
||||
return initial_err
|
||||
}
|
||||
done := submit_ctx.done()
|
||||
mut watch_done := true
|
||||
select {
|
||||
_ := <-done {
|
||||
err := submit_ctx.err()
|
||||
if err !is none {
|
||||
return err
|
||||
}
|
||||
watch_done = false
|
||||
}
|
||||
else {}
|
||||
}
|
||||
for {
|
||||
p.mutex.lock()
|
||||
if p.closed {
|
||||
p.mutex.unlock()
|
||||
return error(err_pool_closed)
|
||||
}
|
||||
if watch_done {
|
||||
err := submit_ctx.err()
|
||||
if err !is none {
|
||||
p.mutex.unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.accepted < p.max_jobs {
|
||||
p.accept_job_locked()
|
||||
p.mutex.unlock()
|
||||
spawn run_pool_job(mut p, f)
|
||||
return
|
||||
}
|
||||
submit_ready := p.submit_ready
|
||||
p.mutex.unlock()
|
||||
if !watch_done {
|
||||
_ := <-submit_ready
|
||||
continue
|
||||
}
|
||||
select {
|
||||
_ := <-submit_ready {
|
||||
continue
|
||||
}
|
||||
_ := <-done {
|
||||
err := submit_ctx.err()
|
||||
if err !is none {
|
||||
return err
|
||||
}
|
||||
watch_done = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// submit_with_timeout waits up to timeout for f to be accepted by the pool.
|
||||
//
|
||||
// The timeout bounds admission only. Accepted jobs still receive the pool context.
|
||||
pub fn (mut p Pool) submit_with_timeout(timeout time.Duration, f JobFn) ! {
|
||||
timeout_ctx, cancel := new_timeout_context(context.background(), timeout)
|
||||
mut submit_ctx := context.Context(timeout_ctx)
|
||||
defer {
|
||||
cancel()
|
||||
}
|
||||
p.submit_with_context(submit_ctx, f) or {
|
||||
if err.msg() == context_deadline_exceeded && timeout_ctx.was_canceled_by_timeout() {
|
||||
return error(err_timeout)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// wait closes the pool to new submissions, drains accepted jobs, and waits for completion.
|
||||
//
|
||||
// wait is one-shot. It returns the first job error observed by any accepted job.
|
||||
pub fn (mut p Pool) wait() ! {
|
||||
p.mutex.lock()
|
||||
if p.waited {
|
||||
p.mutex.unlock()
|
||||
return error(err_pool_wait_called)
|
||||
}
|
||||
p.waited = true
|
||||
p.closed = true
|
||||
p.wake_submitters_locked()
|
||||
p.mutex.unlock()
|
||||
|
||||
p.wg.wait()
|
||||
p.cancel()
|
||||
err := p.get_first_error()
|
||||
if err !is none {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// close is an explicit lifecycle alias for wait().
|
||||
//
|
||||
// It rejects later submissions and waits for all accepted jobs before returning.
|
||||
pub fn (mut p Pool) close() ! {
|
||||
p.wait()!
|
||||
}
|
||||
|
||||
fn run_pool_job(mut p Pool, f JobFn) {
|
||||
defer {
|
||||
p.finish_accepted_job()
|
||||
p.wg.done()
|
||||
}
|
||||
// The token channel is a bounded semaphore. At most `workers` accepted jobs
|
||||
// can pass this point and run user code concurrently.
|
||||
_ := <-p.tokens
|
||||
defer {
|
||||
p.tokens <- true
|
||||
}
|
||||
mut job_ctx := p.ctx
|
||||
f(mut job_ctx) or { p.set_first_error(err) }
|
||||
}
|
||||
|
||||
fn (mut p Pool) finish_accepted_job() {
|
||||
p.mutex.lock()
|
||||
p.accepted--
|
||||
p.wake_submitters_locked()
|
||||
p.mutex.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) accept_job_locked() {
|
||||
p.accepted++
|
||||
// add() is protected by the same mutex as wait(), so callers cannot race an
|
||||
// accepted job against pool shutdown.
|
||||
p.wg.add(1)
|
||||
}
|
||||
|
||||
fn (mut p Pool) wake_submitters_locked() {
|
||||
if !p.submit_ready.closed {
|
||||
p.submit_ready.close()
|
||||
}
|
||||
if !p.closed {
|
||||
p.submit_ready = chan int{cap: 1}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Pool) set_first_error(err IError) {
|
||||
p.mutex.lock()
|
||||
if p.first_err is none {
|
||||
p.first_err = err
|
||||
}
|
||||
p.mutex.unlock()
|
||||
}
|
||||
|
||||
fn (mut p Pool) get_first_error() IError {
|
||||
p.mutex.lock()
|
||||
err := p.first_err
|
||||
p.mutex.unlock()
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user