Feature request: allow sorting within a group of dependencies
talgendler opened this issue · 2 comments
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 for group
type Annotated struct {
Name string
Group string
Target interface{}
// Set an order weight for this dependency 0 < X < 100
Weight float32
}
OR
type result struct {
fx.Out
MyHandler http.Handler `group: "handlers,10.5"`
}
Weight range should be within (0,100) 0 < X < 100
.
The default weight will be 100 and sort will not be stable.
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:
- middleware is fed into a value group
- each middleware has a name
- the user has a separate YAML configuration file where they order the middleware by name
- middleware not listed in the file are not applied
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.