-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeps.go
More file actions
221 lines (197 loc) · 5.36 KB
/
deps.go
File metadata and controls
221 lines (197 loc) · 5.36 KB
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
215
216
217
218
219
220
221
package st
import (
"context"
"fmt"
"log"
"os"
"reflect"
"runtime"
"strings"
"sync"
)
// simpleConsoleLogger is an unstructured logger designed for emitting simple
// messages to the console in `-v`/`--verbose` mode.
var simpleConsoleLogger = log.New(os.Stderr, "[STAVE] ", 0) //nolint:gochecknoglobals // This is unchanged in the course of the process lifecycle.
type onceMap struct {
mu *sync.Mutex
m map[onceKey]*onceFun
}
type onceKey struct {
Name string
ID string
}
func (o *onceMap) LoadOrStore(theFunc Fn) *onceFun {
defer o.mu.Unlock()
o.mu.Lock()
key := onceKey{
Name: theFunc.Name(),
ID: theFunc.ID(),
}
existing, ok := o.m[key]
if ok {
return existing
}
one := &onceFun{
once: &sync.Once{},
fn: theFunc,
displayName: displayName(theFunc.Name()),
}
o.m[key] = one
return one
}
// SerialDeps is like Deps except it runs each dependency serially, instead of
// in parallel. This can be useful for resource intensive dependencies that
// shouldn't be run at the same time.
func SerialDeps(fns ...interface{}) {
funcs := checkFns(fns)
ctx := context.Background()
for i := range fns {
runDeps(ctx, funcs[i:i+1])
}
}
// SerialCtxDeps is like CtxDeps except it runs each dependency serially,
// instead of in parallel. This can be useful for resource intensive
// dependencies that shouldn't be run at the same time.
func SerialCtxDeps(ctx context.Context, fns ...interface{}) {
funcs := checkFns(fns)
for i := range fns {
runDeps(ctx, funcs[i:i+1])
}
}
// CtxDeps runs the given functions as dependencies of the calling function.
// Dependencies must only be of type:
//
// func()
// error
// func(context.Context)
// error
//
// Or a similar method on a st.Namespace type.
// Or an st.Fn interface.
//
// The function calling Deps is guaranteed that all dependent functions will be
// run exactly once when Deps returns. Dependent functions may in turn declare
// their own dependencies using Deps. Each dependency is run in their own
// goroutines. Each function is given the context provided if the function
// prototype allows for it.
func CtxDeps(ctx context.Context, fns ...interface{}) {
funcs := checkFns(fns)
runDeps(ctx, funcs)
}
// runDeps assumes you've already called checkFns.
func runDeps(ctx context.Context, fns []Fn) {
mu := &sync.Mutex{}
var errs []string
var exit int
wg := &sync.WaitGroup{}
for _, f := range fns {
fn := onces.LoadOrStore(f)
wg.Add(1)
go func() {
defer func() {
if panicValue := recover(); panicValue != nil {
mu.Lock()
if err, ok := panicValue.(error); ok {
exit = changeExit(exit, ExitStatus(err))
} else {
exit = changeExit(exit, 1)
}
errs = append(errs, fmt.Sprint(panicValue))
mu.Unlock()
}
wg.Done()
}()
if err := fn.run(ctx); err != nil {
mu.Lock()
errs = append(errs, fmt.Sprint(err))
exit = changeExit(exit, ExitStatus(err))
mu.Unlock()
}
}()
}
wg.Wait()
if len(errs) > 0 {
panic(Fatal(exit, strings.Join(errs, "\n")))
}
}
func checkFns(fns []interface{}) []Fn {
funcs := make([]Fn, len(fns))
for iFunc, theFunc := range fns {
if fn, ok := theFunc.(Fn); ok {
funcs[iFunc] = fn
continue
}
// Check if the target provided is a not function so we can give a clear warning
t := reflect.TypeOf(theFunc)
if t == nil || t.Kind() != reflect.Func {
panic(fmt.Errorf("non-function used as a target dependency: %T. The st.Deps, st.SerialDeps and st.CtxDeps functions accept function names, such as st.Deps(TargetA, TargetB)", theFunc)) //nolint:lll // Long string-literal.
}
funcs[iFunc] = F(theFunc)
}
if err := checkForCycle(funcs); err != nil {
panic(fmt.Errorf("checking for cycles in dependency graph: %w", err))
}
return funcs
}
// Deps runs the given functions in parallel, exactly once. Dependencies must
// only be of type:
//
// func()
// error
// func(context.Context)
// error
//
// Or a similar method on a st.Namespace type.
// Or an st.Fn interface.
//
// This is a way to build up a tree of dependencies with each dependency
// defining its own dependencies. Functions must have the same signature as a
// Stave target, i.e. optional context argument, optional error return.
func Deps(fns ...interface{}) {
CtxDeps(context.Background(), fns...)
}
func changeExit(oldExitCode, newExitCode int) int {
if newExitCode == 0 {
return oldExitCode
}
if oldExitCode == 0 {
return newExitCode
}
if oldExitCode == newExitCode {
return oldExitCode
}
// both different and both non-zero, just set
// exit to 1. Nothing more we can do.
return 1
}
// funcName returns the unique name for the function.
func funcName(i interface{}) string {
return funcObj(i).Name()
}
func funcObj(i interface{}) *runtime.Func {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer())
}
func displayName(name string) string {
splitByPackage := strings.Split(name, ".")
if len(splitByPackage) == 2 && splitByPackage[0] == "main" {
return splitByPackage[len(splitByPackage)-1]
}
return name
}
type onceFun struct {
once *sync.Once
fn Fn
err error
displayName string
}
// run will run the function exactly once and capture the error output. Further runs simply return
// the same error output.
func (o *onceFun) run(ctx context.Context) error {
o.once.Do(func() {
if Verbose() {
simpleConsoleLogger.Println("Running dependency:", displayName(o.fn.Name()))
}
o.err = o.fn.Run(ctx)
})
return o.err
}