Skip to content

Commit aaae50e

Browse files
authored
feat: limited support for running inner() in parallel (#61)
1 parent 6b7b723 commit aaae50e

File tree

5 files changed

+115
-27
lines changed

5 files changed

+115
-27
lines changed

api.go

+13
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,19 @@ func callsInner(fn interface{}) Provider {
300300
})
301301
}
302302

303+
// Parallel annotates a wrap function to indicate that
304+
// the inner function may be invoked in parallel.
305+
//
306+
// At the current time, support for this is very
307+
// limited. Returned values cannot be propagated
308+
// across such a call and the resulting lack of
309+
// initialization can cause a panic.
310+
func Parallel(fn interface{}) Provider {
311+
return newThing(fn).modify(func(fm *provider) {
312+
fm.parallel = true
313+
})
314+
}
315+
303316
// TODO: add ExampleLoose
304317

305318
// Loose annotates a wrap function to indicate that when trying

doc.go

+23-21
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
// Obligatory // comment
22

33
/*
4-
54
Package nject is a general purpose dependency injection framework.
65
It provides wrapping, pruning, and indirect variable passing. It is type safe
76
and using it requires no type assertions. There are two main injection APIs:
87
Run and Bind. Bind is designed to be used at program initialization and
98
does as much work as possible then rather than during main execution.
109
11-
List of providers
10+
# List of providers
1211
1312
The API for nject is a list of providers (injectors) that are run in order.
1413
The final function in the list must be called. The other functions are called
@@ -32,7 +31,7 @@ is a simple example:
3231
In this example, context.Background and log.Default are not invoked because
3332
their outputs are not used by the final function (http.ListenAndServe).
3433
35-
How to use
34+
# How to use
3635
3736
The basic idea of nject is to assemble a Collection of providers and then use
3837
that collection to supply inputs for functions that may use some or all of
@@ -81,7 +80,7 @@ is
8180
8281
1st2nd
8382
84-
Collections
83+
# Collections
8584
8685
Providers are grouped as into linear sequences. When building an injection chain,
8786
the providers are grouped into several sets: LITERAL, STATIC, RUN. The LITERAL
@@ -100,7 +99,7 @@ The STATIC set is composed of the cacheable injectors.
10099
101100
The RUN set if everything else.
102101
103-
Injectors
102+
# Injectors
104103
105104
All injectors have the following type signature:
106105
@@ -114,7 +113,7 @@ are dropped from the handler chain. They are not invoked. Injectors
114113
that have no output values are a special case and they are always retained
115114
in the handler chain.
116115
117-
Cached injectors
116+
# Cached injectors
118117
119118
In injector that is annotated as Cacheable() may promoted to the STATIC set.
120119
An injector that is annotated as MustCache() must be promoted to
@@ -139,7 +138,7 @@ is injected, all chains will share the same pointer.
139138
return &j
140139
}))
141140
142-
Memoized injectors
141+
# Memoized injectors
143142
144143
Injectors in the STATIC set are only run for initialization. For some things,
145144
like opening a database, that may still be too often. Injectors that are marked
@@ -155,7 +154,7 @@ Memoized injectors may not have any inputs that are go maps, slices, or function
155154
Arrays, structs, and interfaces are okay. This requirement is recursive so a struct that
156155
that has a slice in it is not okay.
157156
158-
Fallible injectors
157+
# Fallible injectors
159158
160159
Fallible injectors are special injectors that change the behavior of the injection
161160
chain if they return error. Fallible injectors in the RUN set, that return error
@@ -208,7 +207,7 @@ Some examples:
208207
return nil
209208
}
210209
211-
Wrap functions and middleware
210+
# Wrap functions and middleware
212211
213212
A wrap function interrupts the linear sequence of providers. It may or may
214213
invoke the remainder of the sequence that comes after it. The remainder of
@@ -248,7 +247,11 @@ other kinds of functions: one call to reflect.MakeFunc().
248247
Wrap functions serve the same role as middleware, but are usually
249248
easier to write.
250249
251-
Final functions
250+
Wrap functions that invoke inner() multiple times in parallel are
251+
are not well supported at this time and such invocations must have
252+
the wrap function decorated with Parallel().
253+
254+
# Final functions
252255
253256
Final functions are simply the last provider in the chain.
254257
They look like regular Go functions. Their input parameters come
@@ -280,11 +283,11 @@ because they internally control if the downstream chain is called.
280283
return nil
281284
}
282285
283-
Literal values
286+
# Literal values
284287
285288
Literal values are values in the provider chain that are not functions.
286289
287-
Invalid provider chains
290+
# Invalid provider chains
288291
289292
Provider chains can be invalid for many reasons: inputs of a type not
290293
provided earlier in the chain; annotations that cannot be honored
@@ -293,7 +296,7 @@ functions that take or return functions with an anymous type other than
293296
wrapper functions; A chain that does not terminate with a function; etc.
294297
Bind() and Run() will return error when presented with an invalid provider chain.
295298
296-
Panics
299+
# Panics
297300
298301
Bind() and Run() will return error rather than panic. After Bind()ing
299302
an init and invoke function, calling them will not panic unless a provider
@@ -322,7 +325,7 @@ can be added with Shun().
322325
323326
var ErrorOfLastResort = nject.Shun(func() error { return nil })
324327
325-
Chain evaluation
328+
# Chain evaluation
326329
327330
Bind() uses a complex and somewhat expensive O(n^2) set of rules to evaluate
328331
which providers should be included in a chain and which can be dropped. The goal
@@ -350,14 +353,14 @@ from the closest provider.
350353
Providers that have unmet dependencies will be eliminated from the chain
351354
unless they're Required.
352355
353-
Best practices
356+
# Best practices
354357
355358
The remainder of this document consists of suggestions for how to use nject.
356359
357360
Contributions to this section would be welcome. Also links to blogs or other
358361
discussions of using nject in practice.
359362
360-
For tests
363+
# For tests
361364
362365
The best practice for using nject inside a large project is to have a few
363366
common chains that everyone imports.
@@ -398,7 +401,7 @@ to write tests.
398401
})
399402
}
400403
401-
Displaying errors
404+
# Displaying errors
402405
403406
If nject cannot bind or run a chain, it will return error. The returned
404407
error is generally very good, but it does not contain the full debugging
@@ -417,7 +420,7 @@ Remove the comments to hide the original type names.
417420
log.Fatal(err)
418421
}
419422
420-
Reorder
423+
# Reorder
421424
422425
The Reorder() decorator allows injection chains to be fully or partially reordered.
423426
Reorder is currently limited to a single pass and does not know which injectors are
@@ -459,7 +462,7 @@ and used.
459462
OverrideThingOptions(thing.Option1, thing.Option2),
460463
)
461464
462-
Self-cleaning
465+
# Self-cleaning
463466
464467
Recommended best practice is to have injectors shutdown the things they themselves start. They
465468
should do their own cleanup.
@@ -500,7 +503,7 @@ defines. Wrapper functions have a small runtime performance penalty, so if you
500503
have more than a couple of providers that need cleanup, it makes sense to include
501504
something like CleaningService.
502505
503-
Forcing inclusion
506+
# Forcing inclusion
504507
505508
The normal direction of forced inclusion is that an upstream provider is required
506509
because a downstream provider uses a type produced by the upstream provider.
@@ -514,6 +517,5 @@ produce a type that is only consumed by the downstream provider.
514517
515518
Lastly, the providers can be grouped with Cluster so that they'll be included or
516519
excluded as a group.
517-
518520
*/
519521
package nject

generate.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package nject
33
import (
44
"fmt"
55
"reflect"
6+
"sync/atomic"
67
)
78

89
type valueCollection []reflect.Value
@@ -197,7 +198,7 @@ func generateWrappers(
197198
in0Type, reflective := getInZero(fv)
198199
fm.wrapWrapper = func(v valueCollection, next func(valueCollection)) {
199200
vCopy := v.Copy()
200-
callCount := 0
201+
var callCount int32
201202

202203
rTypes := make([]reflect.Type, len(fm.flows[receivedParams]))
203204
for i, tc := range fm.flows[receivedParams] {
@@ -206,12 +207,19 @@ func generateWrappers(
206207

207208
// for thread safety, this is not built outside WrapWrapper
208209
inner := func(i []reflect.Value) []reflect.Value {
209-
if callCount > 0 {
210-
copy(v, vCopy)
210+
if !fm.parallel {
211+
callCount++
212+
if callCount > 1 {
213+
v = vCopy.Copy()
214+
}
215+
outMap(v, i)
216+
next(v)
217+
} else {
218+
atomic.AddInt32(&callCount, 1)
219+
vc := vCopy.Copy()
220+
outMap(vc, i)
221+
next(vc)
211222
}
212-
callCount++
213-
outMap(v, i)
214-
next(v)
215223
r := retMap(v)
216224
for i, retV := range r {
217225
if rTypes[i].Kind() == reflect.Interface {

matrix_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package nject_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/muir/nject"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
type PT01 string
11+
type PT02 string
12+
type PT03 string
13+
type PT04 string
14+
15+
func TestParallelCallsToInner(t *testing.T) {
16+
t.Parallel()
17+
assert.NoError(t, nject.Run(t.Name(),
18+
t,
19+
nject.Parallel(func(inner func(*testing.T, PT01), t *testing.T) {
20+
for _, s := range []PT01{"A1", "A2", "A3", "A4"} {
21+
s := s
22+
t.Run(string(s), func(t *testing.T) {
23+
t.Log("branching")
24+
t.Parallel()
25+
inner(t, s)
26+
})
27+
}
28+
}),
29+
nject.Parallel(func(inner func(*testing.T, PT02), t *testing.T) {
30+
for _, s := range []PT02{"B1", "B2", "B3", "B4"} {
31+
s := s
32+
t.Run(string(s), func(t *testing.T) {
33+
t.Log("branching")
34+
t.Parallel()
35+
inner(t, s)
36+
})
37+
}
38+
}),
39+
nject.Parallel(func(inner func(*testing.T, PT03), t *testing.T) {
40+
for _, s := range []PT03{"C1", "C2", "C3", "C4"} {
41+
s := s
42+
t.Run(string(s), func(t *testing.T) {
43+
t.Log("branching")
44+
t.Parallel()
45+
inner(t, s)
46+
})
47+
}
48+
}),
49+
nject.Parallel(func(inner func(*testing.T, PT04), t *testing.T) {
50+
for _, s := range []PT04{"D1", "D2", "D3", "D4"} {
51+
s := s
52+
t.Run(string(s), func(t *testing.T) {
53+
t.Log("branching")
54+
t.Parallel()
55+
inner(t, s)
56+
})
57+
}
58+
}),
59+
func(t *testing.T, a PT01, b PT02, c PT03, d PT04) {
60+
assert.Equal(t, t.Name(), "TestParallelCallsToInner/"+string(a)+"/"+string(b)+"/"+string(c)+"/"+string(d))
61+
},
62+
))
63+
}

nject.go

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type provider struct {
3434
consumptionOptional bool
3535
singleton bool
3636
cluster int32
37+
parallel bool
3738

3839
// added by characterize
3940
memoized bool
@@ -94,6 +95,7 @@ func (fm *provider) copy() *provider {
9495
consumptionOptional: fm.consumptionOptional,
9596
singleton: fm.singleton,
9697
cluster: fm.cluster,
98+
parallel: fm.parallel,
9799
memoized: fm.memoized,
98100
class: fm.class,
99101
group: fm.group,

0 commit comments

Comments
 (0)