Closed talgendler closed 3 years ago
Hello, @talgendler, thanks for the proposal. Sorry, we don't think Dig/Fx should support weight-based value groups. However, there's a pattern we can recommend instead. Read on for details.
Background: Value groups are intended to be used for unordered collections only. We specifically made that decision when designing the API, and we intentionally randomize the order in which the value group is filled: https://github.com/uber-go/dig/blob/173b7b1935ec5ea6b26ebaf0b513658752f58c79/dig.go#L479
We felt that for cases when ordering mattered, it really mattered. For those cases, it was desirable for the application to have full control on the order. An API based on weights/z-indexing allows any included module anywhere to alter the ordering without informing the consumer.
Recommendation: Internally, we have use value groups to inject middleware into one of our systems. Middleware ordering matters: whether the rate limiting middleware runs before or after the retry middleware is important; whether the logging middleware runs before or after retry middleware changes how many times the request is logged.
The rough design we went with there is:
So the consumer looks roughly like the following:
package foo
type Middleware interface {
// Name specifies the name of the middleware.
// It must be unique in an application.
Name() string
// Wrap applies the middleware on the method.
Wrap(Method) (Method, error)
}
type Config struct {
// List of middleware that should run in-order.
Middleware []string `yaml:"middleware"`
}
var Module = fx.Provide(New)
type Params struct {
fx.In
Config Config
Middlewares []Middleware `group:"myapp"`
}
func New(p Params) (Result, error) {
// Collate middleware by name.
mws := make(map[string]Middleware)
for _, m := range p.Middlewares {
name := m.Name()
if _, ok := mws[name]; ok {
return nil, fmt.Errorf("received middleware %q multiple times", name)
}
mws[name] = m
}
var result []Middleware // ordered list of requested middleware
for _, name := range p.Config.Middleware {
mw, ok := mws[name]
if !ok {
return nil, fmt.Errorf("unknown middleware %q requested in config", name)
}
result = append(result, mw)
}
return applyMiddlewares(..., result)
}
Various middleware modules look roughly like this:
package retry
const Name = "retry"
var Module = fx.Provide(
fx.Annotated{
Target: New,
Group: "myapp",
},
)
type middleware struct {
// ...
}
func New(...) foo.Middleware {
// ...
}
func (mw *middleware) Name() string {
return Name
}
// ...
And finally, the application does something similar:
fx.New(
foo.Module,
retry.Module,
config.Module,
logging.Module,
// ...
).Run()
Where config.Module
parses the YAML configuration and feeds foo.Config into the container.
So the user can do:
middlewares: [retry, logging, ratelimit]
Hope this helps!
Thanks that was a great example.
Currently when adding a dependency to a group the order within the group is not guaranteed.
How about adding a
Weight
field within the Annotated struct and a structure tag option forgroup
OR
Weight range should be within (0,100)
0 < X < 100
. The default weight will be 100 and sort will not be stable.