vxx / vlib / x / async / group.v
155 lines · 142 sloc · 4.47 KB · a66aa5b0fe618b32988f4fd9b235017c336940ba
Raw
1module async
2
3import context
4import sync
5
6// GroupConfig enables optional Group behavior while preserving new_group() defaults.
7@[params]
8pub struct GroupConfig {
9pub:
10 collect_errors bool
11 max_errors int
12}
13
14// Group coordinates a set of related concurrent jobs.
15//
16// Jobs share a derived context. The first job error is returned by wait() and
17// cancels the shared context so sibling jobs can stop cooperatively.
18@[heap]
19pub struct Group {
20mut:
21 ctx context.Context
22 cancel context.CancelFn = unsafe { nil }
23 wg &sync.WaitGroup = sync.new_waitgroup()
24 // Protects both WaitGroup lifecycle state and first_err. WaitGroup.add()
25 // must not race with WaitGroup.wait(), so go() and wait() share this lock.
26 mutex &sync.Mutex = sync.new_mutex()
27 // Stored once. Later job errors never replace the first failure observed by
28 // the group, even when several jobs fail concurrently after cancellation.
29 first_err IError = none
30 // Optional bounded collection of observed job errors. It does not change
31 // wait(), which still returns only first_err.
32 collect_errors bool
33 max_errors int
34 collected_errors []IError
35 // Set before wait() calls wg.wait(). Once true, no further jobs are accepted.
36 waiting bool
37}
38
39// new_group creates a Group with a shared cancellable context derived from parent.
40//
41// The parent is accepted by value to keep the public call site simple. The
42// derived context is owned by the group and canceled on first job error or when
43// wait() completes.
44pub fn new_group(parent context.Context) &Group {
45 return new_group_internal(parent, GroupConfig{})
46}
47
48// new_group_with_config creates a Group with opt-in bounded error collection.
49pub fn new_group_with_config(parent context.Context, config GroupConfig) !&Group {
50 if config.collect_errors && config.max_errors <= 0 {
51 return error(err_group_max_errors_invalid)
52 }
53 return new_group_internal(parent, config)
54}
55
56fn new_group_internal(parent context.Context, config GroupConfig) &Group {
57 ctx, cancel := new_cancel_context(parent)
58 return &Group{
59 ctx: context.Context(ctx)
60 cancel: cancel
61 wg: sync.new_waitgroup()
62 mutex: sync.new_mutex()
63 collect_errors: config.collect_errors
64 max_errors: config.max_errors
65 }
66}
67
68// go starts f in a new concurrent task.
69//
70// Calling go after wait has started returns an error. The task should not
71// panic; panics in spawned work are not recovered by x.async.
72pub fn (mut g Group) go(f JobFn) ! {
73 if f == unsafe { nil } {
74 return error(err_nil_job)
75 }
76 g.mutex.lock()
77 if g.waiting {
78 g.mutex.unlock()
79 return error(err_group_go_after_wait)
80 }
81 // add() happens while holding the same mutex that guards wait(), preventing
82 // callers from triggering sync.WaitGroup's add-while-waiting misuse panic.
83 g.wg.add(1)
84 g.mutex.unlock()
85 spawn run_group_job(mut g, f)
86}
87
88// wait blocks until all accepted group jobs finish.
89//
90// It returns the first job error, if any. wait may be called once; after it
91// starts, the group no longer accepts new jobs.
92pub fn (mut g Group) wait() ! {
93 g.mutex.lock()
94 if g.waiting {
95 g.mutex.unlock()
96 return error(err_group_wait_called)
97 }
98 g.waiting = true
99 g.mutex.unlock()
100
101 g.wg.wait()
102 // Always cancel after all jobs finish to release the derived context and to
103 // make the lifecycle symmetric with context.with_cancel().
104 g.cancel()
105 err := g.get_first_error()
106 if err !is none {
107 return err
108 }
109}
110
111// errors returns a snapshot of collected job errors.
112//
113// Collection is opt-in through new_group_with_config(). The order of concurrently
114// observed errors is not guaranteed.
115pub fn (mut g Group) errors() []IError {
116 g.mutex.lock()
117 errs := g.collected_errors.clone()
118 g.mutex.unlock()
119 return errs
120}
121
122fn run_group_job(mut g Group, f JobFn) {
123 defer {
124 g.wg.done()
125 }
126 // Each job gets its own local mutable interface value. The underlying
127 // context is shared and synchronized by the context module.
128 mut job_ctx := g.ctx
129 f(mut job_ctx) or { g.set_first_error(err) }
130}
131
132fn (mut g Group) set_first_error(err IError) {
133 mut should_cancel := false
134 g.mutex.lock()
135 if g.first_err is none {
136 g.first_err = err
137 should_cancel = true
138 }
139 if g.collect_errors && g.collected_errors.len < g.max_errors {
140 g.collected_errors << err
141 }
142 g.mutex.unlock()
143 if should_cancel {
144 // Cancel outside the group mutex. context cancellation can notify child
145 // contexts, so keeping our own lock out of that path avoids lock nesting.
146 g.cancel()
147 }
148}
149
150fn (mut g Group) get_first_error() IError {
151 g.mutex.lock()
152 err := g.first_err
153 g.mutex.unlock()
154 return err
155}
156