-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.go
More file actions
339 lines (286 loc) · 7.88 KB
/
bootstrap.go
File metadata and controls
339 lines (286 loc) · 7.88 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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package bootstrap
import (
"context"
"fmt"
"reflect"
"sync"
"github.com/viilon/bootstrap/dag"
)
// Bootstrap manages the bootstrap process with dependency injection and topological execution.
type Bootstrap struct {
providers []*dag.Node
values map[reflect.Type]reflect.Value
cleanups []func() error
functions map[uintptr]bool // Cache for registered functions to avoid duplicates
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
err error // Store the first error encountered during Add
}
// New creates a new Bootstrap.
func New() *Bootstrap {
ctx, cancel := context.WithCancel(context.Background())
r := &Bootstrap{
providers: make([]*dag.Node, 0),
values: make(map[reflect.Type]reflect.Value),
cleanups: make([]func() error, 0),
functions: make(map[uintptr]bool),
ctx: ctx,
cancel: cancel,
}
// Register default context provider
r.Add(func() context.Context {
return r.ctx
})
return r
}
// WithContext sets a custom context for the runner.
// It wraps the provided context with cancellation support, allowing Cleanup() to still work.
// This method is thread-safe and can be chained.
func (b *Bootstrap) WithContext(ctx context.Context) *Bootstrap {
b.mu.Lock()
defer b.mu.Unlock()
// Wrap the provided context with cancellation
c, cancel := context.WithCancel(ctx)
b.ctx = c
b.cancel = cancel
return b
}
// Add registers one or more constructors.
func (b *Bootstrap) Add(constructors ...interface{}) *Bootstrap {
b.mu.Lock()
defer b.mu.Unlock()
if b.err != nil {
return b
}
for _, c := range constructors {
if err := b.add(c); err != nil {
b.err = err
return b
}
}
return b
}
// Run executes all registered constructors in topological order.
func (b *Bootstrap) Run() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.err != nil {
return b.err
}
sorted, err := dag.Resolve(b.providers)
if err != nil {
return err
}
// Execute
for _, p := range sorted {
if err := b.execute(p); err != nil {
return err
}
}
return nil
}
// Cleanup gracefully shuts down the runner by calling registered cleanups in reverse order.
func (b *Bootstrap) Cleanup() error {
b.mu.Lock()
defer b.mu.Unlock()
// Cancel the context first
b.cancel()
var errs []error
// Execute cleanups in reverse order
for i := len(b.cleanups) - 1; i >= 0; i-- {
if err := b.cleanups[i](); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("cleanup errors: %v", errs)
}
return nil
}
func (b *Bootstrap) add(fn interface{}) error {
val := reflect.ValueOf(fn)
typ := val.Type()
if typ.Kind() == reflect.Func {
return b.registerProvider(fn)
}
if typ.Kind() == reflect.Ptr {
if val.IsNil() {
return fmt.Errorf("argument must not be nil")
}
elemType := typ.Elem()
// Case 1: Struct Injection (must embed bootstrap.Inject)
if elemType.Kind() == reflect.Struct {
if hasInject(elemType) {
return b.registerStructInjector(val)
}
// If not embedded Inject, fall through to Target Population
}
// Case 2: Target Population (pointer to pointer or interface, OR struct without Inject)
return b.registerTargetPopulator(val)
}
return fmt.Errorf("argument must be a function or pointer")
}
func (b *Bootstrap) registerProvider(fn interface{}) error {
val := reflect.ValueOf(fn)
typ := val.Type()
// Check inputs for embedded Inject
if err := checkInjectInTypes(typ.NumIn(), typ.In, "input"); err != nil {
return err
}
// Check outputs for embedded Inject
if err := checkInjectInTypes(typ.NumOut(), typ.Out, "output"); err != nil {
return err
}
ptr := val.Pointer()
if b.functions[ptr] {
return nil // Already registered
}
b.functions[ptr] = true
p, err := dag.NewNode(fn)
if err != nil {
return err
}
b.providers = append(b.providers, p)
return nil
}
func (b *Bootstrap) registerTargetPopulator(ptrVal reflect.Value) error {
// ptrVal is *T. We want to set it to a value of type T.
targetType := ptrVal.Type().Elem()
// Create synthetic function: func(v T)
fnType := reflect.FuncOf([]reflect.Type{targetType}, nil, false)
fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
ptrVal.Elem().Set(args[0])
return nil
})
// Register as provider
// We don't check for duplicates here as multiple variables might need same value?
// But b.add calls check duplicates based on function pointer.
// reflect.MakeFunc creates unique pointers? Let's assume so or checks handle it.
// Actually, we should probably bypass b.functions check for synthetic nodes,
// or ensure they are unique.
// For now, let's just use common logic but maybe synthetic functions have unique pointers.
p, err := dag.NewNode(fn.Interface())
if err != nil {
return err
}
b.providers = append(b.providers, p)
return nil
}
func (b *Bootstrap) registerStructInjector(structPtrVal reflect.Value) error {
// structPtrVal is *Struct.
structType := structPtrVal.Type().Elem()
var fieldTypes []reflect.Type
var fieldIndices []int
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
// Skip unexported fields? Usually yes.
if field.PkgPath != "" {
continue
}
// Skip the Inject field itself?
if field.Type == reflect.TypeOf(Inject{}) {
continue
}
fieldTypes = append(fieldTypes, field.Type)
fieldIndices = append(fieldIndices, i)
}
// Create synthetic function: func(f1 T1, f2 T2, ...)
fnType := reflect.FuncOf(fieldTypes, nil, false)
fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
elem := structPtrVal.Elem()
for i, arg := range args {
elem.Field(fieldIndices[i]).Set(arg)
}
return nil
})
p, err := dag.NewNode(fn.Interface())
if err != nil {
return err
}
b.providers = append(b.providers, p)
return nil
}
func hasInject(typ reflect.Type) bool {
injectType := reflect.TypeOf(Inject{})
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.Anonymous && field.Type == injectType {
return true
}
}
return false
}
func checkInjectInTypes(count int, getType func(int) reflect.Type, kindStr string) error {
for i := 0; i < count; i++ {
t := getType(i)
checkType := t
if checkType.Kind() == reflect.Ptr {
checkType = checkType.Elem()
}
if checkType.Kind() == reflect.Struct && hasInject(checkType) {
return fmt.Errorf("provider %s type %v embeds bootstrap.Inject, which is prohibited", kindStr, t)
}
}
return nil
}
func (b *Bootstrap) execute(p *dag.Node) error {
var args []reflect.Value
args = make([]reflect.Value, len(p.Inputs))
for i, in := range p.Inputs {
if val, ok := b.values[in]; ok {
args[i] = val
} else {
return fmt.Errorf("internal error: missing value for type %v", in)
}
}
results := p.Fn.Call(args)
// Check error returns
for _, idx := range p.ErrorIndices {
errVal := results[idx]
if !errVal.IsNil() {
return errVal.Interface().(error)
}
}
// Store results (excluding errors) and register cleanups
// Note: p.outputs corresponds to results excluding errors, BUT
// results contains EVERYTHING including errors at specific indices.
// We need to map outputs to results carefully.
outputIdx := 0
for i, res := range results {
// Check if this index was an error index
isError := false
for _, errIdx := range p.ErrorIndices {
if i == errIdx {
isError = true
break
}
}
if isError {
continue
}
if outputIdx < len(p.Outputs) {
outType := p.Outputs[outputIdx]
outputIdx++
// Store in values map
b.values[outType] = res
// Register Cleanup
if res.IsValid() {
// Check for nil only on nillable types to avoid panic
isNil := false
switch res.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
if res.IsNil() {
isNil = true
}
}
if !isNil {
if cleanable, ok := res.Interface().(Cleanable); ok {
b.cleanups = append(b.cleanups, cleanable.Cleanup)
}
}
}
}
}
return nil
}