uber-go/fx

Allow modules to be provided multiple times application

mikhasd opened this issue · 4 comments

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.

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.

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.

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?

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.