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

modify objects already provided in the graph #653

Closed
srikrsna opened this issue Oct 25, 2018 · 34 comments
Closed

modify objects already provided in the graph #653

srikrsna opened this issue Oct 25, 2018 · 34 comments

Comments

@srikrsna
Copy link
Contributor

srikrsna commented Oct 25, 2018

First of all, thank you for this excellent library. It is idiomatic and helped us a lot in deploying our services with ease.

One thing that will be nice to have (and I would like to contribute to) is to have a way to modify objects that are already provided in the graph. As we deployed our services such patterns were required more on more.

Example:

We wanted to add Open Census metrics and trace to our application. This can be done by wrapping the http handler in Open Census's http handler. Currently we modify the handler provider/invoker function to check if open census is enabled and wrap the http handler to return an Open Census enable http handler. If we had a way to modify provided objects especially interfaces i.e. provide a wrapped implementation things like tracing caching auth can be separated into their own layer.

This can be implemented without any breaking changes. It will require a new API, something like, fx.Modify(...) which will take inputs from providers and return providers that were already in the graph. Modifiers will only be applied if types returned by them have been requested by invokers.

What do you think of this?

@glibsm
Copy link
Collaborator

glibsm commented Oct 25, 2018

Linking in the related discussions: #531 #649

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 1, 2018

Option C of #531 is more or less what I am proposing and would elegantly solve my problem. One concern seems to be that the order in which such decorators will be applied. Can't they be like Invoke, i.e. in the order provided?

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 6, 2018

Any plans to implement this?

@abhinav
Copy link
Collaborator

abhinav commented Nov 6, 2018

Hey, we haven't had a chance to fully evaluate how this affects the rest of
Fx. Decorators can likely be applied in-order but we need to evaluate how they
interact with other existing and upcoming features of Fx like named values,
value groups, and fx.Annotated (#610).

We may have some time to look into this in a few weeks.

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 8, 2018

I am unable to understand how named values and value groups differ from the graph's perspective they are like any other value, aren't they? Could you please explain.

And I suppose this should be supported by dig first. Should I open an issue there?

@abhinav
Copy link
Collaborator

abhinav commented Nov 8, 2018

Yes, happy to clarify! From the point of view of fx.Decorate, named values
and value groups open up some questions:

  • When I provide a decorator for type F, does that apply to all instances of
    F or only the unnamed ones?

        fx.Provide(
            fx.Annotated{Name: "foo", Target: func() F { ... }},
        ),
        fx.Decorate(
            func(F) F { ... },
        ),
    

    If it applies only to unnamed ones, that means decorators have to explicitly
    specify which named value they apply to.

  • Similarly, if F is actually being fed into a value group, does the
    decorator for F apply to all Fs provided to that group?

        fx.Provide(
            fx.Annotated{Group: "foo", Target: producer1},
            fx.Annotated{Group: "foo", Target: producer2},
            fx.Annotated{Group: "foo", Target: producer3},
        ),
        fx.Decorate(
            func(F) F { ... },
        ),
    
  • Going further, does a decorator for []F apply only to explicitly provided
    []Fs or also to the value group-version consumed by the fx.In?

        type Param struct {
            fx.In
            
            Fs []F `group:"foo"`
        }
        
        fx.New(
            fx.Provide(func(p Param) Bar { ... }),
            fx.Decorate([]F) []F { ... },
        )
    

I think we would have to figure out a good plan for the above and any other
related questions that come up before this can be implemented.


Yes, this would need to be implemented in dig first. We don't need to
necessarily open the issue there since the two repos are pretty coupled
together anyway. (We've been considering merging dig into fx.)

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 8, 2018

So I was thinking something more like this,

Since the purpose of decorators is to modify values already available and instantiated in the graph, named values, value groups (the actual slice), and normal values all count as a unique value available in the graph. Let me elaborate with examples and try to lay out rules to better explain what I am proposing.

  • Decorators are useful for modifying input values for invokers and linked providers.
  • Decision that a decorator is to be applied or not is decided by its output type.
  • Its a lot like provide but unlike provide it can also optionally (although most likely) accept its own output types. Example:
fx.Provide(
    func (...) X { 
    // ...
    },
    func (...) F {
     // ...
    },
),
fx.Decorator(
   func(x X, f F) F { ... },
)
  • Decorator functions can accept fx.In and also return fx.Out types.
  • They can accept Lifecycle type.
  • In case of a value group the value group should be requested and be returned as a slice. i.e.
   fx.Provide(
        fx.Annotated{Group: "foo", Target: producer1},
        fx.Annotated{Group: "foo", Target: producer2},
        fx.Annotated{Group: "foo", Target: producer3},
    ),
    fx.Decorate(
        func(in struct {
         fx.In
         Fs []F `group:"foo"` 
        }) struct{
          fx.Out
          Fs []F `group:"foo"`
        } { ... },
    ),

This is OK because currently fx does not make any guarantee in terms of order of the values so modifying them all at once should not be a problem. The first point also supports this argument as we are only trying to modify the inputs, value groups are resolved to slices when provided and hence should be modified in that form.

What do you think of this?

@abhinav
Copy link
Collaborator

abhinav commented Nov 8, 2018

That sounds like a good plan. Decorators will reference values/named values
explicitly, and value groups can modify all values in a loop because of the
unspecified-ordering guarantee. Modifying a single value of a value group
isn't supported because at that point, you should just modify the
corresponding constructor directly.

The implementation of Decorate will almost definitely need a check which
verifies that you're not producing anything you didn't consume (so you can't
add new provides from a decorator), and it'll have a special-cased behavior
for value groups where producing a slice []Foo for a value group fills the
same value group instead of [][]Foo. Oh, and this also makes it possible to
"expand" a value group by returning more items in the fx.Out version than the
number received in the fx.In of a decorator.

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 8, 2018

@abhinav shall I start implementing what we just discussed?

@glibsm
Copy link
Collaborator

glibsm commented Nov 8, 2018

Oh, and this also makes it possible to "expand" a value group by returning more items in the fx.Out version.

This worries me. We can technically runtime safeguard against this, but not sure if that's something we should stick our hand in.

@abhinav
Copy link
Collaborator

abhinav commented Nov 8, 2018

@srikrsna We're having some active discussion about fx.Decorate ordering. I
would hold off on implementing this until that's resolved.

@akshayjshah @glibsm Let's discuss ordering for the case where multiple
fx.Decorates are provided.

Also, @rhang-uber, do you think we would change the visualization in Dig for
something like this or keep it as-is?

@akshayjshah
Copy link
Contributor

@srikrsna After a bit more than a year of Fx use, our internal ecosystem has a pretty deep graph of dependencies, across multiple teams (and multiple offices around the world). Many Fx modules are written by engineers who don't have the time or inclination to read all the Fx docs. Today, this works pretty well because fx.Provide is order-independent, and most modules only provide types. It's pretty hard to mess up. I'm worried that decorators will introduce something similar to aspect-oriented programming, where we enable spooky action at a distance. If that action is also order-dependent, we'll end up with a more brittle module system that's harder for users to debug.

On the other hand, this is a really valuable feature - we've discussed it multiple times now.

Before you put a ton of effort into implementing this, let me think on this for a bit.

@glibsm
Copy link
Collaborator

glibsm commented Nov 8, 2018

Just thinking out loud here, what about fx.Replace?

Instead of dealing with the instantiated objects to modify their state, we would allow to replace the constructor that provided the type? Instead of massively complicating the dig/fx internals it would put the onus on library authors to make sure their modules use public constructors (as per our guidelines).

fx.Provide(func() A  { ... })
fx.Provide(func() B { ... })
fx.Replace(func () B { .. some other B function }) // errors out if B doesn't exist or signatures don't match
fx.Invoke(func (a A, b B) { ... })

I'm thinking that dealing purely with constructor functions manipulation, rather than dropping down to instantiated objects may be a better road to take and should still allow the usages that fx.Decorate is meant to solve.

Dig could queue up all replaces and run them in between Provide and Invoke.

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 9, 2018

fx.Replace in my humble opinion will result in more duplication. I will try an explain a real world situation that I will face,

We use gRPC for developing all our services. And almost all services are written in Go. I developed a simple authorisation library (internal) that generates gRPC server implementation that check if the user has the right to access a specific rpc and call the underlying server implementation.

type RightsEchoServer struct {
     pb.EchoServer // interface
     ....
}

func NewRightsEchoServer(s pb.EchoServer, ...) pb.EchoServer {
     return &RightsEchoServer{s, ...}
}

func (s *RightsEchoServer) Echo(ctx context.Context, req pb.EchoRequest) (*pb.EchoResponse, error) {
     var hasRight bool
      // Check Rights ...
     if !hasRight {
            return nil, status.Error(codes.Unauthorized, ...)
     }

      return s.EchoServer.Echo(ctx, req)
}

Now in case of proposed fx.Decorate, the code would look like this

     fx.Provide(NewEchoServer),
     fx.Decorate(NewRightsEchoServer),

In case of fx.Replace this would be

     fx.Provide(NewEchoServer),
     fx.Replace(func (d D, ...) (pb.EchoServer, error) {
            s, err := NewEchoServer(d)
            if err != nil {
                  return 
            }
            return NewRightsServer(s, ...), nil
     }),

And in case of multiple such decorators the code would only increase. But in case of ordered decorators it would be just adding them to the list.

@srikrsna
Copy link
Contributor Author

srikrsna commented Nov 9, 2018

If that action is also order-dependent, we'll end up with a more brittle module system that's harder for users to debug.

Decorator pattern itself is order dependent, isn't it?

@srikrsna
Copy link
Contributor Author

Did you guys get any time to evaluate this?

@abhinav
Copy link
Collaborator

abhinav commented Dec 4, 2018

Hello, @srikrsna! Sorry about the delay. We've had a busy few weeks.

We discussed this a bit further. To recap: We like the idea of supporting
decorators but we're concerned about ordering issues. For some context,
our internal Fx ecosystem has a number of libraries providing Fx modules which
are just collections of fx.Options. A concern is that fx.Decorates in
libraries A and B can unintentionally affect values provided by library C, and
the order in which they affect library C depends on whether the user placed
A.Module first in the fx.New or B.Module.

One solution we're considering is having a way of scoping the effect of
fx.Decorate.

fx.Something(
    fx.Provide(...),
    fx.Invoke(...)
    
    // Decorators specified here will apply only to types provided in
    // this scope.
    fx.Decorate(...),
)

The blast radius of an fx.Decorate is limited to the scope it was called in
and any of the child scopes. So the top-level fx.New has the ability to
decorate types provided by all modules.

func main() {
    fx.New(
        foo.Module,
        bar.Module,
        
        fx.Provide(
            ...
        ),
        
        // Can decorate types provided by any module along with types
        // provided above.
        fx.Decorate(...),
    ).Run()
}

We foresee need for specifying a name for this scope. (This would, for
example, enable support for replacing all the types provided by these named
scopes wholesale. That's unrelated to this feature and a discussion for
later.) So the API would be similar to,

fx.Something(
    "mymodule",
    fx.Provide(...),
    fx.Invoke(...)
    fx.Decorate(...),
)

Since we've already been using the term "Fx module" everywhere, it might make
sense to call this fx.Module.

package fx

func Module(name string, opts ...Option) Option

Again, fx.Decorate in main() would continue to work for all values
provided by all modules.

Would this satisfy your needs?

CC @akshayjshah @glibsm


(Sorry, just to be explicit about this: We're just spitballing ideas here.
There's a chance we abandon this route if we see issues with it.)

@srikrsna
Copy link
Contributor Author

srikrsna commented Dec 4, 2018

I like the idea, the example of decorators effecting other modules will definitely be a concern and really hard to track down. Limiting the scope seems like a great idea.

@srikrsna
Copy link
Contributor Author

Any update on this?

@srikrsna
Copy link
Contributor Author

srikrsna commented Feb 9, 2019

@glibsm @abhinav Just making sure you are getting these. Any update?

@akshayjshah
Copy link
Contributor

We're getting these, but we've been a little underwater - sorry! Please bear with us.

We'll try to come back to this issue in the next week and clarify what the next steps are.

@srikrsna
Copy link
Contributor Author

Thank you for the update and sorry for pinging so much! Looking forward to next week!

@algobardo
Copy link

algobardo commented Feb 18, 2019

If anyone is really desperate and need something similar right now, the following might work:

// Replace replaces the given target with the passed value.
// The target needs to be a pointer to the interface that needs to be replaced.
func Replace(target interface{}, replacement interface{}) fx.Option {
	var sh *fx.Shutdowner
	shutdownerType := reflect.TypeOf(sh).Elem() // the plan is to steal the app from the shutdowner

	targetType := reflect.TypeOf(target).Elem()

	targetTypes := []reflect.Type{targetType, shutdownerType}

	// Build a function signature that looks like:
	// func(t1 targetType, shutdowner fx.Shutdowner)
	fnType := reflect.FuncOf(targetTypes, nil, false /* variadic */)
	fn := reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {

		values := args[1].
			Elem().
			Elem().
			FieldByName("app").
			Elem().
			FieldByName("container").
			Elem().
			FieldByName("values")

		values = trickReflection(values)

		keys := values.MapKeys()
		found := false
		for _, k := range keys {
			vType := values.MapIndex(k).Type()
			if vType.Implements(targetType) {
				values.SetMapIndex(k, reflect.ValueOf(reflect.ValueOf(replacement)))
				found = true
				break
			}
		}
		if !found {
			panic("Value to replace not found in the values map")
		}

		return nil
	})

	return fx.Invoke(fn.Interface())
}

// trickReflection tricks reflection to hide the fact that the value has been obtained through an unexported field
func trickReflection(v reflect.Value) reflect.Value {
	return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
}

@srikrsna
Copy link
Contributor Author

Is there a decision on this?

@abhinav
Copy link
Collaborator

abhinav commented Apr 25, 2019

Is there a decision on this?

Hey! We generally have agreement that the design from this comment is the
way to go. Specifically,

func Module(name string, opts ...Option) Option

func Decorate(funcs ...interface{}) Option

Where Decorates are scoped to the innermost module, and fx.New implicitly
acting as the top-level module with an empty name. Unfortunately, we have
little bandwidth to work on this right now.

@srikrsna
Copy link
Contributor Author

Hey! We generally have agreement that the design from this comment is the
way to go. Specifically,

That's great.

Unfortunately, we have
little bandwidth to work on this right now.

I can work on this. I should probably start with dig?

@abhinav
Copy link
Collaborator

abhinav commented Apr 29, 2019

I can work on this. I should probably start with dig?

That would be excellent! Yes, you would have to start with dig. We haven't
fully evaluated what changes are needed in dig so I'll spitball here a bit.

There are two pieces of functionality needed here: fx.Module and
fx.Decorate.

fx.Module likely looks like this.

// Module defines a named set of Fx options that collectively provide
// functionality to an application. Name must not be empty.
//
//   var LoggingModule = fx.Module("logging", fx.Provide(newLogger))
//
// Modules may be nested arbitrarily deep.
//
//   var ServiceModule = fx.Module("service", LoggingModule, MetricsModule)
//
// The top-level fx.New call implicitly acts as a module with an empty name.
func Module(name string, opts ...Option) Option

For something like this, my first guess is that dig will need a concept of
named subgraphs. Maybe something like,

// Child returns a named child of this container. The child container has
// full access to the parent's types, and any types provided to the child
// will be made available to the parent.
//
// The name of the child is for observability purposes only. As such, it
// does not have to be unique across different children of the container.
func (c *Container) Child(name string) *Container

So perhaps fx.Module("foo", opts) maps to a call similar to,

child := c.Child("foo")
for _, opt := range opts {
    opt.apply(child)
}

Similarly, fx.Decorate likely looks like this.

// Decorate is an Fx option that allows modifying or replacing objects in the
// container. Decorate is called with zero or more functions, each of which
// accepts some number of objects from the container and produces objects to
// replace all or some of them.
//
//  fx.Decorate(
//      func(rt http.RoundTripper, metrics *Metrics) http.RoundTripper {
//        return &metricsRoundTripper{rt: rt, metrics: metrics}
//      },
//  )
//
// As with constructors, decoration may optionally fail with an error,
// causing the application startup to halt.
//
// A decoration is scoped to the innermost module in which the fx.Decorate
// call appeared. That is, an fx.Decorate call can only affect values
// produced by the module in which the call appears or its descendants. So in
// the following example, the fx.Decorate call can only affect values
// produced by the "bar" and "baz" modules.
//
//  fx.Module("foo",
//      fx.Provide(...),
//      fx.Module("bar",
//          fx.Provide(...),
//          fx.Decorate(...),
//          fx.Module("baz", ...),
//      ),
//  )
//
// Note that because the top-level fx.New is implicitly a module, an
// fx.Decorate call appearing at the top-level is able to affect any value in
// the application.
//
//  fx.New(
//      MetricsModule,
//      LoggingModule,
//      fx.Decorate(...),
//  )
func Decorate(funcs ...interface{}) Option

The corresponding API in dig is more straightforward.

// Decorate allows modifying or replacing a value in the container. It
// must be provided a function f which accepts one or more values from the
// container and produces objects to replace all or some of them. f may
// return an error as its last result to indicate that decoration failed.
// f may not return any types that are not already in the container.
//
// Decoration applies to this container only. Parent containers will not
// be affected.
func (*Container) Decorate(f interface{}, opts ...DecorateOption) error

(As with Provide and Invoke, we likely want to preemptively add functional
options in anticipation of future need.)

So an fx.Decorate(fs...) call will be translate to,

for _, f := range fs {
    if err := c.Decorate(f); err != nil {
        ...
    }
}

These two pieces of functionality can be implemented independently but we
wouldn't want to release Decorate without Module/Child support to control the
scope of the operation.

@srikrsna
Copy link
Contributor Author

Thank you that's really helpful. I'll start working on it.

@mfateev
Copy link

mfateev commented Aug 12, 2019

@srikrsna Are you working on this or this task is up for grabs?

@mfateev
Copy link

mfateev commented Aug 12, 2019

One possible extension is to have the support for optional objects like Spring does. The idea is that the framework provides objects, but if application level code specifies the conflicting provider the framework provided object is automatically ignored. This would avoid need to call Decorate for default objects if there is no need to wrap them.

The more advanced feature is to provide support for conditional objects.

@srikrsna
Copy link
Contributor Author

@mfateev I somehow missed this. I have a working implementation. We are using it inside our org for some internal deployments. I am just waiting for review.

@mfateev
Copy link

mfateev commented Sep 24, 2021

@srikrsna Do you ever plan to release this to the open source?

@srikrsna
Copy link
Contributor Author

srikrsna commented Sep 24, 2021

@mfateev

replace go.uber.org/fx => github.com/appointy/fx v1.9.1-0.20190624110333-490d04d33ef6

replace go.uber.org/dig => github.com/paullen/dig v1.7.1-0.20190624104937-6e47ebbbdcf6

@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

Hey all,

This has been implemented with fx.Module and fx.Decorate/fx.Replace. These have been released with v1.17.0 release. Will close this issue now but do let me know if you have any questions about these APIs.

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

7 participants