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

fx.Decorate not working w/ value groups #1104

Closed
aplr opened this issue Jul 26, 2023 · 3 comments
Closed

fx.Decorate not working w/ value groups #1104

aplr opened this issue Jul 26, 2023 · 3 comments

Comments

@aplr
Copy link

aplr commented Jul 26, 2023

Describe the bug

When using fx.Decorate with value groups, the decorated value (here: zap.Logger) is not supplied to the constructors (here: NewDummyHandler) which feed the values.

However, the decorator is fed to the other constructor in the group, NewMyService, which has no dependents.

This issue becomes evident in the logs below. As both the NewMyService as well as the NewDummyHandler constructors are provided within the same module, I expect them to both be fed with the same, decorated logger.

I expect the logs to look sth like the following. I reduced the output only to the relevant fields.

{"logger":"my_service.service","msg":"name should be `my_service.service`"}
{"logger":"my_service.dummy","msg":"name should be `my_service.dummy`"}

However, this is the actual, faulty output, again only the relevant fields:

{"logger":"my_service.service","msg":"name should be `my_service.service`"}
{"logger":"dummy","msg":"name should be `my_service.dummy`"}

In the second line, I expect the logger field to be my_service.dummy, however, the decorated logger seems to not be fed.

I don't know what's the cause, probably it's a race (don't know if registring happens in parallel), or if the dependency resolution is faulty, or if it's even expected.

Edit: Probably a dig issue, don't know if I should cross-post the issue there.

Full logs
> 
[Fx] SUPPLY     *zap.Logger
[Fx] PROVIDE    *main.ServiceHandler[group = "handlers"] <= main.NewDummyHandler() from module "my_service"
[Fx] PROVIDE    *main.MyService <= main.NewMyService() from module "my_service"
[Fx] PROVIDE    fx.Lifecycle <= go.uber.org/fx.New.func1()
[Fx] PROVIDE    fx.Shutdowner <= go.uber.org/fx.(*App).shutdowner-fm()
[Fx] PROVIDE    fx.DotGraph <= go.uber.org/fx.(*App).dotGraph-fm()
[Fx] DECORATE   *zap.Logger <= main.main.func1() from module "my_service"
[Fx] INVOKE             main.main.func2() from module "my_service"
[Fx] RUN        supply: stub(*zap.Logger)
[Fx] RUN        decorate: main.main.func1() from module "my_service"
[Fx] RUN        provide: main.NewDummyHandler() from module "my_service"
[Fx] RUN        provide: main.NewMyService() from module "my_service"

{"logger":"my_service.service","msg":"name should be `my_service.service`"}
{"logger":"dummy","msg":"name should be `my_service.dummy`"}

[Fx] RUNNING

To Reproduce

  1. clone the reproduction repo: https://github.com/aplr/fx_decorate_value_group_issue
  2. run go run main.go
  3. observe the output
main.go
package main

import (
	"go.uber.org/fx"
	"go.uber.org/zap"
)

// MARK: -- APP

func main() {
	log := zap.Must(zap.NewProduction())

	app := fx.New(
		fx.Supply(log),
		fx.Module("my_service",
			fx.Decorate(func(log *zap.Logger) *zap.Logger {
				return log.Named("my_service")
			}),
			fx.Provide(NewDummyHandler),
			fx.Provide(NewMyService),
			fx.Invoke(func(srv *MyService) {
				srv.Run()
			}),
		),
	)

	app.Run()
}

// MARK: -- HANDLER

type ServiceHandler struct {
	Name    string
	Handler func() error
}

type DummyHandlerParams struct {
	fx.In

	Log *zap.Logger
}

type HandlerResult struct {
	fx.Out

	Handler *ServiceHandler `group:"handlers"`
}

func NewDummyHandler(params DummyHandlerParams) HandlerResult {
	return HandlerResult{
		Handler: &ServiceHandler{
			Name: "dummy",
			Handler: func() error {
				params.Log.Named("dummy").Info("name should be `my_service.dummy`")
				return nil
			},
		},
	}
}

// MARK: -- SERVICE

type MyService struct {
	log      *zap.Logger
	handlers []*ServiceHandler
}

type ServiceParams struct {
	fx.In

	Log      *zap.Logger
	Handlers []*ServiceHandler `group:"handlers"`
}

func NewMyService(params ServiceParams) *MyService {
	return &MyService{
		log:      params.Log.Named("service"),
		handlers: params.Handlers,
	}
}

func (s *MyService) Run() error {
	s.log.Info("name should be `my_service.service`")

	for _, handler := range s.handlers {
		if err := handler.Handler(); err != nil {
			return err
		}
	}

	return nil
}

Additional context

Using the newest versions of both dependencies.

Dependency graph

graphviz

@JacobOaks
Copy link
Contributor

Hey, thanks for reporting this. I've made a slightly simpler repro on the playground: https://go.dev/play/p/GtqaH27ek0N

This seems to be some weird edge case issue with the combination of value groups + decorators + modules/scoping. I'm going to continue investigating, but for now I wanted to report that, other than not using value groups or modules, one workaround I found is to add the fx.Private option to the calls to fx.Provide.

Will keep you updated.

JacobOaks added a commit to JacobOaks/dig that referenced this issue Aug 2, 2023
We call simple providers with their original scope:
`err := n.Call(n.OrigScope())` (https://github.com/uber-go/dig/blob/master/param.go#L287)
This allows decorators to be applied even if the constructor
is exported via `dig.Export(true)`

However, we call value group value providers with whatever scope we found them in:
```
for _, c := range c.storesToRoot() {
    providers := c.getGroupProviders(pt.Group, pt.Type.Elem())
    // ...
    for _, n := range providers {
        if err := n.Call(c); err != nil { /* ... */ }
    }
}
```
This has the consequence of causing exported value group value providers
within a module to not be able to be decorated by decorators
within the same module, when using `dig.Export(true)`.
Since we use `dig.Export(true)` by default in Fx, this is the default
behavior for value group providers, see uber-go/fx#1104

This commit changes `callGroupProviders` to use the value group provider's
original scope as well, and adds a test to verify the behavior is fixed.
JacobOaks added a commit to JacobOaks/dig that referenced this issue Aug 2, 2023
We call simple providers with their original scope:
`err := n.Call(n.OrigScope())`
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L287)

This allows decorators to be applied even if the constructor
is exported via `dig.Export(true)`

However, we call value group value providers with whatever scope we found them in:
```
for _, c := range c.storesToRoot() {
    providers := c.getGroupProviders(pt.Group, pt.Type.Elem())
    // ...
    for _, n := range providers {
        if err := n.Call(c); err != nil { /* ... */ }
    }
}
```
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L609)

This has the consequence of causing exported value group value providers
within a module to not be able to be decorated by decorators
within the same module, when using `dig.Export(true)`.
Since we use `dig.Export(true)` by default in Fx, this is the default
behavior for value group providers, see uber-go/fx#1104

This commit changes `callGroupProviders` to use the value group provider's
original scope as well, and adds a test to verify the behavior is fixed.
JacobOaks added a commit to JacobOaks/dig that referenced this issue Aug 2, 2023
We call simple providers with their original scope.
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L287)
This allows decorators to be applied even if the constructor is exported
via `dig.Export(true)`.

However, we call value group value providers with whatever scope we found them in:
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L609)

This has the consequence of causing exported value group value providers
within a module to not be able to be decorated by decorators
within the same module, when using `dig.Export(true)`, unlike
their simple provider counterpart.
For example (playground link: https://go.dev/play/p/R2uqaTvT57P):

```go
type Foo struct{}
type FooResults struct {
	dig.Out

	Foo Foo `group:"foos"`
}

func NewFoo(s string) FooResults {
	fmt.Printf("String in NewFoo: %q\n", s)
	return FooResults{
		Foo: Foo{},
	}
}

type Bar struct{}

func NewBar(s string) Bar {
	fmt.Printf("String in NewBar: %q\n", s)
	return Bar{}
}

type UseFooAndBarParams struct {
	dig.In

	Foos []Foo `group:"foos"`
	Bar  Bar
}

func UseFooAndBar(UseFooAndBarParams) {}

func main() {
	c := dig.New()
	c.Provide(func() string { return "base" })
	child := c.Scope("child")
	child.Decorate(func(s string) string {
		return s + "-decorated"
	})
	child.Provide(NewFoo, dig.Export(true))
	child.Provide(NewBar, dig.Export(true))
}

// Output:
// String in NewFoo: "base"
// String in NewBar: "base-decorated"
```

Since we use `dig.Export(true)` by default in Fx, this is the default
behavior for value group providers, see uber-go/fx#1104

This commit changes `callGroupProviders` to use the value group provider's
original scope as well, and adds a test to verify the behavior is fixed.
JacobOaks added a commit to uber-go/dig that referenced this issue Aug 4, 2023
We call simple providers with their original scope.
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L287)
This allows decorators to be applied even if the constructor is exported
via `dig.Export(true)`.

However, we call value group value providers with whatever scope we found them in:
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L609)

This has the consequence of causing exported value group value providers
within a module to not be able to be decorated by decorators
within the same module, when using `dig.Export(true)`, unlike
their simple provider counterpart.
For example (playground link: https://go.dev/play/p/R2uqaTvT57P):

```go
type Foo struct{}
type FooResults struct {
	dig.Out

	Foo Foo `group:"foos"`
}

func NewFoo(s string) FooResults {
	fmt.Printf("String in NewFoo: %q\n", s)
	return FooResults{
		Foo: Foo{},
	}
}

type Bar struct{}

func NewBar(s string) Bar {
	fmt.Printf("String in NewBar: %q\n", s)
	return Bar{}
}

type UseFooAndBarParams struct {
	dig.In

	Foos []Foo `group:"foos"`
	Bar  Bar
}

func UseFooAndBar(UseFooAndBarParams) {}

func main() {
	c := dig.New()
	c.Provide(func() string { return "base" })
	child := c.Scope("child")
	child.Decorate(func(s string) string {
		return s + "-decorated"
	})
	child.Provide(NewFoo, dig.Export(true))
	child.Provide(NewBar, dig.Export(true))
}

// Output:
// String in NewFoo: "base"
// String in NewBar: "base-decorated"
```

Since we use `dig.Export(true)` by default in Fx, this is the default
behavior for value group providers, see uber-go/fx#1104

This commit changes `callGroupProviders` to use the value group provider's
original scope as well, and adds a test to verify the behavior is fixed.
@JacobOaks
Copy link
Contributor

I verified this change in Dig fixes your repro: uber-go/dig#393.
So next release should contain this fix, or you can pin Dig to master for now. Thanks again for reporting this!

@aplr
Copy link
Author

aplr commented Aug 6, 2023

Sure thing, thanks @JacobOaks for fixing that so quickly!

alexisvisco pushed a commit to alexisvisco/dig that referenced this issue Aug 31, 2023
We call simple providers with their original scope.
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L287)
This allows decorators to be applied even if the constructor is exported
via `dig.Export(true)`.

However, we call value group value providers with whatever scope we found them in:
(Ref: https://github.com/uber-go/dig/blob/master/param.go#L609)

This has the consequence of causing exported value group value providers
within a module to not be able to be decorated by decorators
within the same module, when using `dig.Export(true)`, unlike
their simple provider counterpart.
For example (playground link: https://go.dev/play/p/R2uqaTvT57P):

```go
type Foo struct{}
type FooResults struct {
	dig.Out

	Foo Foo `group:"foos"`
}

func NewFoo(s string) FooResults {
	fmt.Printf("String in NewFoo: %q\n", s)
	return FooResults{
		Foo: Foo{},
	}
}

type Bar struct{}

func NewBar(s string) Bar {
	fmt.Printf("String in NewBar: %q\n", s)
	return Bar{}
}

type UseFooAndBarParams struct {
	dig.In

	Foos []Foo `group:"foos"`
	Bar  Bar
}

func UseFooAndBar(UseFooAndBarParams) {}

func main() {
	c := dig.New()
	c.Provide(func() string { return "base" })
	child := c.Scope("child")
	child.Decorate(func(s string) string {
		return s + "-decorated"
	})
	child.Provide(NewFoo, dig.Export(true))
	child.Provide(NewBar, dig.Export(true))
}

// Output:
// String in NewFoo: "base"
// String in NewBar: "base-decorated"
```

Since we use `dig.Export(true)` by default in Fx, this is the default
behavior for value group providers, see uber-go/fx#1104

This commit changes `callGroupProviders` to use the value group provider's
original scope as well, and adds a test to verify the behavior is fixed.
JacobOaks added a commit to JacobOaks/fx that referenced this issue Dec 6, 2023
This upgrades Fx to use the latest Dig version 1.17.1,
which contains a bug fix that would solve issues uber-go#1104 and uber-go#1137
(uber-go/dig#393).
JacobOaks added a commit that referenced this issue Dec 6, 2023
This upgrades Fx to use the latest Dig version 1.17.1,
which contains a bug fix that would solve issues #1104 and #1137
(uber-go/dig#393).
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

2 participants