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

Allow modules to be provided multiple times application #892

Open
mikhasd opened this issue Jun 10, 2022 · 4 comments
Open

Allow modules to be provided multiple times application #892

mikhasd opened this issue Jun 10, 2022 · 4 comments
Labels

Comments

@mikhasd
Copy link

mikhasd commented Jun 10, 2022

I'm currently working on reusable modules for common infrastructure like database, MQ, ID provider, etc and these modules may also depends on other sub-modules, such as a config server.

Ideally, I'd like to declare my modules as follows:

package main

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

type ConfigSrvr struct{}

func NewConfigSrvr() *ConfigSrvr {
	return new(ConfigSrvr)
}

var ModConfig = fx.Module("config", fx.Provide(NewConfigSrvr))

type DbConn struct{}

func NewDbConn(_ *ConfigSrvr) *DbConn {
	return new(DbConn)
}

// ModDb requires *ConfigSrvr
var ModDb = fx.Module("db", fx.Provide(NewDbConn), ModConfig)

type WebApp struct{}

func NewWebApp(_ *ConfigSrvr, _ *DbConn) *WebApp {
	return new(WebApp)
}

// ModWebApp requires *ConfigSrvr and *DbConn.
// But ModWebApp developer is not aware *ConfigSrvr is already "imported" by ModDb 
var ModWebApp = fx.Module("webapp", fx.Provide(NewWebApp), ModDb, ModConfig)

func main() {
	fx.New(
		ModWebApp,
		fx.Invoke(func(_ *WebApp, s fx.Shutdowner) {
			fmt.Println("Web app running")
			s.Shutdown()
		}),
	).Run()
}

Currently, if I declare my modules as previously mentioned, I'll get an "already provided" error, which is not ideal because the user of the database and oidc modules would not be aware of it's dependencies.

The framework, could keep track of the modules that have already populated the container and skip it if already processed.

@sywhang
Copy link
Contributor

sywhang commented Jun 30, 2022

Hey there, apologies for the delay in response.

We do not allow the same type to be provided multiple times in the application. We're planning to work on a public documentation site for fx that will explain some of the core thoughts behind this, but just referring to the specific case you are bringing up here, it is easier if there is a top-level library that groups and provides the most commonly used dependencies (i.e. things like config, or logger, or metrics).

The framework, could keep track of the modules that have already populated the container and skip it if already processed.

The error is specifically coming from the same fx.Provide being provided multiple times. The reason we do not allow this currently is because Provides don't run "in-order" and is run "lazily" as needed by Invokes or other Provides that depend on them. If we blindly 'skipped' a constructor that's provided, it's hard to find out which one actually ran. In the example above, the constructors are provided by the same fx.Module, so there is no side effect. But if there are multiple provides, that's a separate story.

That being said, if we can track things by a Module basis, that would make more sense. I'll have to think about the implications of this some more, but I'm open to this idea.

@mikhasd
Copy link
Author

mikhasd commented Jul 7, 2022

I'm trying to achieve a functional behavior similar to spring boot's @Configuration (difficult not to compare) where I can import a configuration class in many different points of my application and the container will only instantiate it once.

My ultimate goal it to make it easier for developers to move from our java/spring-boot based applications to cloud/container/lambda friendly Go lang apps.

@djmitche
Copy link

djmitche commented Aug 9, 2022

I think the request here is that

fx.Provide(newFoo)

should be idempotent, so it can occur multiple times with the same effect as once. That's slightly different from

fx.Provide(newFoo1)
fx.Provide(newFoo2)

where both construct the same type -- this is a genuine conflict between two different constructors.

The first case is very useful in large applications for exactly the mentioned reason: it's a lot easier to structure code to just declare its dependencies, and allow repeated declarations, than to try to find the "right" place to put a single declaration that is needed in multiple places. In the implementation I'm building now, I've collected types into "bundles" and then each bundle has a doc comment saying what other bundles it depends on, relying the author of each fx.App to calcluate based on those comments the correct set of bundles to declare. But that's exactly that kind of transitive-closure functionality I want fx to perform for me!

Is there a way to distinguish the two named cases with reflect?

@mikhasd
Copy link
Author

mikhasd commented Aug 31, 2022

Another option to implement this feature, and allow the framework in general to be mode flexible, would be to allow users to create custom fx.Option implementations.

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

No branches or pull requests

3 participants