Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] OverrideSupply and OverrideProvide #825

Closed
Wondertan opened this issue Dec 15, 2021 · 4 comments
Closed

[Feature Request] OverrideSupply and OverrideProvide #825

Wondertan opened this issue Dec 15, 2021 · 4 comments

Comments

@Wondertan
Copy link

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 possible to use fx to inject mocks? #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.

@abhinav
Copy link
Collaborator

abhinav commented Dec 16, 2021

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.

@Wondertan
Copy link
Author

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!

@yiminc
Copy link

yiminc commented Jan 6, 2022

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

@abhinav abhinav mentioned this issue Feb 9, 2022
sywhang added a commit that referenced this issue Feb 10, 2022
This adds `fx.Decorate`, which lets you specify decorators to an fx app. A decorator can take in one or more dependencies that have already been `Provide`d to the app, and produce one or more values that will be used as replacements in the object graph.

For example, suppose there is a simple app like this:
```go
fx.New(
  fx.Provide(func() *Logger {
    return &Logger{Name: "logger"}
  }),
  fx.Invoke(func(l *Logger) {
    fmt.Println(l.Name)
  }),
)
```

Running this app will print "logger" on the console.

Now let us suppose a decorator was provided:
```go
fx.New(
  fx.Provide(...), // Provide same function as above
  fx.Decorate(func(l *Logger) *Logger {
    return &Logger{Name: "decorated " + l.Name}
  }),
  fx.Invoke(...), // Invoke same function as above
)
```

The decorator here will take in the provided Logger and replace it with another logger whose `Name` is `decorated logger`. The `Invoke`d function is then executed with this replacement value, so running this app will print "decorated logger" on the console.

In terms of implementation, a decorator is represented by the target decorator function and the call stack it was provided from, similar to a provider. `module` contains a list of decorators that were specified within its scope.

The dig dependency had to be updated to the latest master branch of Dig to ensure the fix for uber-go/dig#316 is in.

Following this PR, there are two additional pieces I will be adding:
1. An eventing system for fx.Decorate. 
2. fx.Replace, which takes in a value instead of a function to replace a value in the object graph. This is similar to what fx.Supply is to fx.Provide.

This PR along with the two PRs above should make the long-awaited feature of graph modifications in fx finally possible.

---

Refs #653, #649, #825, uber-go/dig#230, GO-1203, GO-736
@sywhang
Copy link
Contributor

sywhang commented Mar 1, 2022

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants