/
paralyze.go
198 lines (170 loc) · 4.89 KB
/
paralyze.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
package paralyze
import (
"context"
"errors"
"sync"
"time"
)
// Paralyzable is a type of function that can be paralyzed. Since most
// functions don't carry this signature, a common pattern is to wrap an
// existing function in a Paralyzable function.
type Paralyzable func() (interface{}, error)
// ParalyzableCtx is the same as a Paralyzable function, except it accepts a
// context.Context. Functions that implement this type should respect the
// documentation found here: https://godoc.org/context
type ParalyzableCtx func(context.Context) (interface{}, error)
// General errors that can be returned from the paralyze package
var (
ErrTimedOut = errors.New("timed out")
ErrCanceled = errors.New("canceled")
)
// Paralyze parallelizes a function and returns a slice containing results and
// a slice containing errors. The results at each index are not mutually exclusive,
// that is if results[i] is not nil, errors[i] is not guaranteed to be nil.
func Paralyze(funcs ...Paralyzable) (results []interface{}, errors []error) {
var wg sync.WaitGroup
results = make([]interface{}, len(funcs))
errors = make([]error, len(funcs))
wg.Add(len(funcs))
var panik interface{}
var panikOnce sync.Once
for i, fn := range funcs {
go func(i int, fn Paralyzable) {
defer func() {
if r := recover(); r != nil {
panikOnce.Do(func() { panik = r })
}
}()
defer wg.Done()
results[i], errors[i] = fn()
}(i, fn)
}
wg.Wait()
if panik != nil {
panic(panik)
}
return results, errors
}
type ResErr struct {
Res interface{}
Err error
}
// ParalyzeM parallelizes a map of strings to functions. The return type is a
// map of keys to a map containing two keys: res and err.
func ParalyzeM(m map[string]Paralyzable) map[string]ResErr {
var names []string
var fns []Paralyzable
for name, fn := range m {
names = append(names, name)
fns = append(fns, fn)
}
res := make(map[string]ResErr)
results, errs := Paralyze(fns...)
for i := range results {
name := names[i]
res[name] = ResErr{
Res: results[i],
Err: errs[i],
}
}
return res
}
// ParalyzeWithTimeout does the same as Paralyze, but it accepts a timeout. If
// the timeout is exceeded before all paralyzed functions are complete, the
// unfinished results will be discarded without being cancelled. Any complete
// tasks will be unaffected.
func ParalyzeWithTimeout(timeout time.Duration, funcs ...Paralyzable) ([]interface{}, []error) {
if timeout == 0 {
return Paralyze(funcs...)
}
cancel := make(chan struct{})
go time.AfterFunc(timeout, func() { close(cancel) })
results, errors := ParalyzeWithCancel(cancel, funcs...)
for i, err := range errors {
if err == ErrCanceled {
errors[i] = ErrTimedOut
}
}
return results, errors
}
// ParalyzeWithCancel does the same as Paralyze, but it accepts a channel that
// allows the function to respond before the paralyzed functions are finished.
// Any functions that are still oustanding will have errors set as ErrCanceled.
func ParalyzeWithCancel(cancel <-chan struct{}, funcs ...Paralyzable) ([]interface{}, []error) {
var wg sync.WaitGroup
results := make([]interface{}, len(funcs))
errors := make([]error, len(funcs))
wg.Add(len(funcs))
for i, fn := range funcs {
go func(i int, fn func() chan ResErr) {
defer wg.Done()
ch := fn()
select {
case resErr := <-ch:
results[i] = resErr.Res
errors[i] = resErr.Err
case <-cancel:
errors[i] = ErrCanceled
}
}(i, convert(fn))
}
wg.Wait()
return results, errors
}
// ParalyzeWithContext takes a slice of functions that accept a
// context.Context. These functions are responsible for releasing resources
// (closing connections, etc.) and should respect ctx.Done().
func ParalyzeWithContext(ctx context.Context, funcs ...ParalyzableCtx) ([]interface{}, []error) {
var wg sync.WaitGroup
results := make([]interface{}, len(funcs))
errors := make([]error, len(funcs))
wg.Add(len(funcs))
for i, fn := range funcs {
go func(i int, fn ParalyzableCtx) {
defer wg.Done()
results[i], errors[i] = fn(ctx)
}(i, fn)
}
wg.Wait()
return results, errors
}
func convert(fn func() (interface{}, error)) func() chan ResErr {
return func() chan ResErr {
ch := make(chan ResErr, 1)
go func() {
res, err := fn()
ch <- ResErr{res, err}
}()
return ch
}
}
type paralyzer struct {
maxConcurrency int
}
func ParalyzeLimit(limit int, tasks ...Paralyzable) ([]interface{}, []error) {
var wg sync.WaitGroup
sem := make(chan struct{}, limit)
results := make([]interface{}, len(tasks))
errors := make([]error, len(tasks))
wg.Add(len(tasks))
var panik interface{}
var panikOnce sync.Once
for i, fn := range tasks {
sem <- struct{}{}
go func(i int, fn Paralyzable) {
defer func() {
wg.Done()
<-sem
if r := recover(); r != nil {
panikOnce.Do(func() { panik = r })
}
}()
results[i], errors[i] = fn()
}(i, fn)
}
wg.Wait()
if panik != nil {
panic(panik)
}
return results, errors
}