uber-go/fx

[Feature Request] OverrideSupply and OverrideProvide

Wondertan opened this issue ยท 4 comments

What

Currently, the FX errors if you Provide/Supply for an unnamed/ungrouped type multiple times. However, there are use cases where overriding an existing Provide or Supply becomes useful, like in testing(#649). Also, this becomes useful for big applications with a liberal API allowing their users to override the default setup of modules. That is, the API devs prepare the default set of modules, while a user can swap out a module of their choice through overriding.

Why

Essentially, the feature would allow APIs like:

some.New(Modules, WithModule(custom.Module))

Currently, users have to decouple Modules, understand what needs to drop out, and pass their custom one. With this feature, users would add desired things on top.

How

API

// OverrideSupply overwrites values passed to Supply and constructed with Provide.
// It mimics Supply with one difference - it does not error if there are multiple instances of a type
// are supplied/provided. Instead, OverrideSupply forces the Supply.
// Only one override can happen for a type.
func OverrideSupply(vals ...interface{}) Option

// OverrideProvide mimics Provide with one difference - it does not error if there are multiple instances of a type
// are provided or supplied. Instead, OverrideProvide forces the provide.
// Only one override can happen for a type.
func OverrideProvide(vals ...interface{}) Option

Implementation ideas

  • A naive idea would be to collect all the overrides and exclude Provides and Supplies with conflicting types. However, the problem arises when provided ctor returns multiple types, and overriding happens for only one of them, as the whole ctor is excluded. The issue might be ok for some cases and easily detected, thus errored. The good thing is that this can be implemented beneath FX only, without digging into dig. Also, this should be fine enough for testing needs in #649.

  • A more thorough approach without the issue of the approach above would go dipper into dig and somehow be part of graph construction. Unfortunately, I am not that familiar with dig implementation to understand the complexity and feasibility, so kindly ask you to help. Ideally, the implementation should still execute a conflicting ctor but exclude an overridden type from the dependency graph.

Happy to submit a patch once there is a decision on how to proceed.

Hey there! Apologies for the delay in responding. This has indeed been a long-standing feature request in this or other forms. There's also some discussion #653. We've been unable to reach a satisfactory design for a while, especially because there's risk of random modules stomping on your injected entities.

For example, consider a random redisfx module:

package redisfx

var Module = fx.Options(
    fx.Provide(New),
)

func New(log *zap.Logger) *redis.Client {
    log.Info("hello from redis")
    // ...
}

You include it in your application like so:

func main() {
    fx.New(
        zapfx.Module,
        redisfx.Module,
        // ...
    )
}

Now if at some point, redisfx decides to do this:

var Module = fx.Options(
    fx.Provide(New),
    fx.OverrideSupply(zap.NewNop()),
)

Your logger is suddenly overridden without your knowledge or permission.
Further, we do want to support the idea that your redisfx module should be able to augment the logger like so, but only for its own use.

var Module = fx.Options(
    fx.Provide(New),
    fx.SomeFunction(func(original *zap.Logger) *zap.Logger {
        return original.With(zap.String("component", "redis"))
    }),
)

The proposal above would not cover these cases.


However, all's not lost! We've reached a design for this that we're happy with
(we'll try to post a copy of it publicly soon) and we're actively executing on it.

The approach does go deeper into dig as you suggested. You'll see that we've
been doing a bunch of work on Dig recently.
The current open PR uber-go/dig#305 adds a dig.Scope
concept that will allow us to scope replacements or decorations to single
module like RedisFx. The fx.New implicitly acts as a top-level module, so
any replacements at that level apply "globally"--which is what you'd want for
testing.

Thank you for your response! I am really in favor of augmenting the feature and can think of multiple cases where I would use it.

We've reached a design for this that we're happy with
(we'll try to post a copy of it publicly soon) and we're actively executing on it.

Yay ๐ŸŽ‰. Like your approach and looking forward to it!

I understand that this is been actively worked on, but what would be a realistic expectation for when this feature will be available? @abhinav @sywhang

@Wondertan @yiminc Fx version 1.17.0 has been released with fx.Decorate that lets you modify the provides/supplies to the dependency graph.

Closing this issue for now, but let me know if you have any questions regarding these. I know you had some questions/feedback regarding the way this works @yiminc but I'll respond in the corresponding thread.