forked from alitto/pond
-
Notifications
You must be signed in to change notification settings - Fork 0
/
group.go
214 lines (166 loc) · 6.23 KB
/
group.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package pond
import (
"context"
"errors"
"sync"
"sync/atomic"
"github.com/alitto/pond/v2/internal/future"
)
var ErrGroupStopped = errors.New("task group stopped")
// TaskGroup represents a group of tasks that can be executed concurrently.
// The group can be waited on to block until all tasks have completed.
// If any of the tasks return an error, the group will return the first error encountered.
type TaskGroup interface {
// Submits a task to the group.
Submit(tasks ...func()) TaskGroup
// Submits a task to the group that can return an error.
SubmitErr(tasks ...func() error) TaskGroup
// Waits for all tasks in the group to complete.
// If any of the tasks return an error, the group will return the first error encountered.
// If the context is cancelled, the group will return the context error.
// If the group is stopped, the group will return ErrGroupStopped.
// If a task is running when the context is cancelled or the group is stopped, the task will be allowed to complete before returning.
Wait() error
// Returns a channel that is closed when all tasks in the group have completed, a task returns an error, or the group is stopped.
Done() <-chan struct{}
// Stops the group and cancels all remaining tasks. Running tasks are not interrupted.
Stop()
}
// ResultTaskGroup represents a group of tasks that can be executed concurrently.
// As opposed to TaskGroup, the tasks in a ResultTaskGroup yield a result.
// The group can be waited on to block until all tasks have completed.
// If any of the tasks return an error, the group will return the first error encountered.
type ResultTaskGroup[O any] interface {
// Submits a task to the group.
Submit(tasks ...func() O) ResultTaskGroup[O]
// Submits a task to the group that can return an error.
SubmitErr(tasks ...func() (O, error)) ResultTaskGroup[O]
// Waits for all tasks in the group to complete and returns the results of each task in the order they were submitted.
// If any of the tasks return an error, the group will return the first error encountered.
// If the context is cancelled, the group will return the context error.
// If the group is stopped, the group will return ErrGroupStopped.
// If a task is running when the context is cancelled or the group is stopped, the task will be allowed to complete before returning.
Wait() ([]O, error)
// Returns a channel that is closed when all tasks in the group have completed, a task returns an error, or the group is stopped.
Done() <-chan struct{}
// Stops the group and cancels all remaining tasks. Running tasks are not interrupted.
Stop()
}
type result[O any] struct {
Output O
Err error
}
type abstractTaskGroup[T func() | func() O, E func() error | func() (O, error), O any] struct {
pool *pool
nextIndex atomic.Int64
taskWaitGroup sync.WaitGroup
future *future.CompositeFuture[*result[O]]
futureResolver future.CompositeFutureResolver[*result[O]]
}
func (g *abstractTaskGroup[T, E, O]) Done() <-chan struct{} {
return g.future.Done(int(g.nextIndex.Load()))
}
func (g *abstractTaskGroup[T, E, O]) Stop() {
g.future.Cancel(ErrGroupStopped)
}
func (g *abstractTaskGroup[T, E, O]) Submit(tasks ...T) *abstractTaskGroup[T, E, O] {
for _, task := range tasks {
g.submit(task)
}
return g
}
func (g *abstractTaskGroup[T, E, O]) SubmitErr(tasks ...E) *abstractTaskGroup[T, E, O] {
for _, task := range tasks {
g.submit(task)
}
return g
}
func (g *abstractTaskGroup[T, E, O]) submit(task any) {
index := int(g.nextIndex.Add(1) - 1)
g.taskWaitGroup.Add(1)
err := g.pool.dispatcher.Write(func() error {
defer g.taskWaitGroup.Done()
// Check if the context has been cancelled to prevent running tasks that are not needed
if err := g.future.Context().Err(); err != nil {
g.futureResolver(index, &result[O]{
Err: err,
}, err)
return err
}
// Invoke the task
output, err := invokeTask[O](task)
g.futureResolver(index, &result[O]{
Output: output,
Err: err,
}, err)
return err
})
if err != nil {
g.taskWaitGroup.Done()
g.futureResolver(index, &result[O]{
Err: err,
}, err)
}
}
type taskGroup struct {
abstractTaskGroup[func(), func() error, struct{}]
}
func (g *taskGroup) Submit(tasks ...func()) TaskGroup {
g.abstractTaskGroup.Submit(tasks...)
return g
}
func (g *taskGroup) SubmitErr(tasks ...func() error) TaskGroup {
g.abstractTaskGroup.SubmitErr(tasks...)
return g
}
func (g *taskGroup) Wait() error {
_, err := g.future.Wait(int(g.nextIndex.Load()))
// This wait group could reach zero before the future is resolved if called in between tasks being submitted and the future being resolved.
// That's why we wait for the future to be resolved before waiting for the wait group.
g.taskWaitGroup.Wait()
return err
}
type resultTaskGroup[O any] struct {
abstractTaskGroup[func() O, func() (O, error), O]
}
func (g *resultTaskGroup[O]) Submit(tasks ...func() O) ResultTaskGroup[O] {
g.abstractTaskGroup.Submit(tasks...)
return g
}
func (g *resultTaskGroup[O]) SubmitErr(tasks ...func() (O, error)) ResultTaskGroup[O] {
g.abstractTaskGroup.SubmitErr(tasks...)
return g
}
func (g *resultTaskGroup[O]) Wait() ([]O, error) {
results, err := g.future.Wait(int(g.nextIndex.Load()))
// This wait group could reach zero before the future is resolved if called in between tasks being submitted and the future being resolved.
// That's why we wait for the future to be resolved before waiting for the wait group.
g.taskWaitGroup.Wait()
values := make([]O, len(results))
for i, result := range results {
if result != nil {
values[i] = result.Output
}
}
return values, err
}
func newTaskGroup(pool *pool, ctx context.Context) TaskGroup {
future, futureResolver := future.NewCompositeFuture[*result[struct{}]](ctx)
return &taskGroup{
abstractTaskGroup: abstractTaskGroup[func(), func() error, struct{}]{
pool: pool,
future: future,
futureResolver: futureResolver,
},
}
}
func newResultTaskGroup[O any](pool *pool, ctx context.Context) ResultTaskGroup[O] {
future, futureResolver := future.NewCompositeFuture[*result[O]](ctx)
return &resultTaskGroup[O]{
abstractTaskGroup: abstractTaskGroup[func() O, func() (O, error), O]{
pool: pool,
future: future,
futureResolver: futureResolver,
},
}
}