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 does not add a missing object to the module #1144

Closed
abhinav opened this issue Jan 3, 2024 · 3 comments · Fixed by #1151
Closed

fx.Decorate does not add a missing object to the module #1144

abhinav opened this issue Jan 3, 2024 · 3 comments · Fixed by #1151
Assignees

Comments

@abhinav
Copy link
Collaborator

abhinav commented Jan 3, 2024

Describe the bug

If I add an fx.Decorate to my module that returns a type that doesn't exist in the container otherwise, the value isn't made available to the module.

To Reproduce

  1. Define a type and a constructor for it that returns named values only:
type DBConn struct{ name string }

// Note that a naked DBConn will not be added to the container.
// Only named variants.

func NewDB() (out struct {
	fx.Out
	RW *DBConn `name:"rw"`
	RO *DBConn `name:"ro"`
}) {
	out.RW = &DBConn{name: "rw"}
	out.RO = &DBConn{name: "ro"}
	return
}
  1. Add helper functions that a module can use to pick from one of the named variants.
// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RW *DBConn `name:"rw"`
	}) *DBConn {
		return i.RW
	})
}

// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RO *DBConn `name:"ro"`
	}) *DBConn {
		return i.RO
	})
}
  1. Attempt to use these in main.
func main() {
	fx.New(
		fx.Provide(
			NewDB,
		),
		fx.Module("uses-rw",
			UseRW(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-rw:", conn.name)
			}),
		),
		fx.Module("uses-ro",
			UseRO(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-ro:", conn.name)
			}),
		),
	).Run()
}
Full program
package main

import "go.uber.org/fx"

type DBConn struct{ name string }

// Note that a naked DBConn will not be added to the container.
// Only named variants.

func NewDB() (out struct {
	fx.Out
	RW *DBConn `name:"rw"`
	RO *DBConn `name:"ro"`
}) {
	out.RW = &DBConn{name: "rw"}
	out.RO = &DBConn{name: "ro"}
	return
}

// Helpers that use fx.Decorate to pick one of the named variants
// but only within the current module's scope.

// UseRW opts the current module to use the rw connection.
func UseRW() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RW *DBConn `name:"rw"`
	}) *DBConn {
		return i.RW
	})
}

// UseRO opts the current module to use the ro connection.
func UseRO() fx.Option {
	return fx.Decorate(func(i struct {
		fx.In
		RO *DBConn `name:"ro"`
	}) *DBConn {
		return i.RO
	})
}

func main() {
	fx.New(
		fx.Provide(
			NewDB,
		),
		fx.Module("uses-rw",
			UseRW(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-rw:", conn.name)
			}),
		),
		fx.Module("uses-ro",
			UseRO(),
			fx.Invoke(func(conn *DBConn) {
				println("uses-ro:", conn.name)
			}),
		),
	).Run()
}

Running this fails with the error:

% go run .
[Fx] PROVIDE    *main.DBConn[name = "rw"] <= main.NewDB()
[Fx] PROVIDE    *main.DBConn[name = "ro"] <= main.NewDB()
[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   *main.DBConn <= main.UseRW.func1() from module "uses-rw"
[Fx] DECORATE   *main.DBConn <= main.UseRO.func1() from module "uses-ro"
[Fx] INVOKE             main.main.func1() from module "uses-rw"
[Fx] ERROR              fx.Invoke(main.main.func1()) called from:
main.main
        [..]/fx-decorate-bug/main.go:53
runtime.main
        [..]/go/1.21.5/libexec/src/runtime/proc.go:267
Failed: missing dependencies for function "main".main.func1
        [..]/fx-decorate-bug/main.go:53:
missing type:
        - *main.DBConn (did you mean to Provide it?)
[Fx] ERROR              Failed to start: missing dependencies for function "main".main.func1
        [..]/fx-decorate-bug/main.go:53:
missing type:
        - *main.DBConn (did you mean to Provide it?)
exit status 1

Expected behavior

The program should run successfully.

Additional context

To get the desired behavior, a placeholder value is needed in the container.
This works, for example:

 func main() {
 	fx.New(
+		fx.Supply(&DBConn{name: "invalid"}),
		fx.Provide(

Library versions:

go.uber.org/fx v1.20.1
go.uber.org/dig v1.17.1

I don't think this is intended behavior, but I could be wrong.

@sywhang
Copy link
Contributor

sywhang commented Jan 11, 2024

@abhinav this is the intended design for fx.Decorate. There's a test asserting this behavior also: https://github.com/uber-go/fx/blob/master/decorate_test.go#L471

@sywhang
Copy link
Contributor

sywhang commented Jan 11, 2024

Thinking about it, that probably means we didn't document this behavior properly. Let me fix that.

@sywhang sywhang self-assigned this Jan 11, 2024
@abhinav
Copy link
Collaborator Author

abhinav commented Jan 23, 2024

Oops, you're right @sywhang. This is intended behavior.
I wasn't sure if this would work or not, and since the documentation didn't say otherwise, I assumed it would.
Agree with fixing the documentation for this.

abhinav added a commit to abhinav/fx that referenced this issue Feb 6, 2024
…graph

I created uber-go#1144 because I was not sure whether a decorator
could add new values to the graph.

This clarifies the behavior.
We already have tests to verify this behavior.

Resolves uber-go#1144
JacobOaks pushed a commit that referenced this issue Feb 6, 2024
…graph (#1151)

I created #1144 because I was not sure whether a decorator
could add new values to the graph.

This clarifies the behavior.
We already have tests to verify this behavior.

Resolves #1144
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

2 participants