uber-go/fx

Startup order isn't preserved when using modules

Opened this issue · 2 comments

Describe the bug
I've bundled some commonly used dependencies in modules for convenience, for example db initialisation and web server start up. In one of my web services I implemented them as follows (with the intention that the data migration function runs before the web server starts):

	fx.New(
		...
		service.DatastoreModule,  // Configures and connects to Google cloud datastore
		fx.Invoke(runMigrations), // Runs data migrations
		service.ServerModule,     // Configures routing and starts web server
	).Run()

But on startup the web server always starts first and the web server's OnStart hook is called first:

[Fx] INVOKE             ***/service.startServer() from module "server"
...
[Fx] INVOKE             main.runMigrations()
...
[Fx] HOOK OnStart               ***/service.startServer.func1() executing (caller: ***/service.startServer)
[Fx] HOOK OnStart               ***/service.startServer.func1() called by ***/service.startServer ran successfully in 3.48µs
[Fx] HOOK OnStart               ***/service.newDatastoreClient.func1() executing (caller: ***/service.newDatastoreClient)
[country] 2024/02/07 02:36:50.404627 Starting HTTP server on 0.0.0.0:8080
[Fx] HOOK OnStart               ***/service.newDatastoreClient.func1() called by ***/service.newDatastoreClient ran successfully in 435.478µs
[Fx] HOOK OnStart               main.runMigrations.func1() executing (caller: main.runMigrations)
[Fx] HOOK OnStart               main.runMigrations.func1() called by main.runMigrations ran successfully in 1.45µs

However if I wrap the migrations in a module as well:

	fx.New(
		...
		service.DatastoreModule,
		migrationsModule,
		service.ServerModule,
	).Run()

Now it works as expected:

[Fx] INVOKE             main.runMigrations() from module "migrations"
...
[Fx] INVOKE             github.com/boxes-ltd/service-common/service.startServer() from module "server"
...
[Fx] HOOK OnStart               ***/service.newDatastoreClient.func1() executing (caller: ***/service.newDatastoreClient)
[Fx] HOOK OnStart               ***/service.newDatastoreClient.func1() called by ***/service.newDatastoreClient ran successfully in 406.037µs
[Fx] HOOK OnStart               main.runMigrations.func1() executing (caller: main.runMigrations)
[Fx] HOOK OnStart               main.runMigrations.func1() called by main.runMigrations ran successfully in 1.42µs
[Fx] HOOK OnStart               ***/service.startServer.func1() executing (caller: ***/service.startServer)
[Fx] HOOK OnStart               ***/service.startServer.func1() called by ***/service.startServer ran successfully in 780ns

Since documentation states:

Startup hooks, also referred to as OnStart hooks. These run in the order they were appended.

It seems like this is a bug, I couldn't see anything that states that a module takes precedence.

To Reproduce

Add an invoke function before a module that contains an invoke function.

Expected behavior

I would have expected functions to be called in the order they are defined.

Additional context
Add any other context about the problem here.

I wonder if #918 is related?

Hey @Philio, Invoke run order is specified differently than Hook run order. This is a documented behavior. See fx.Invoke documentation.

Invokes registered in Modules are run before the ones registered at the scope of the parent. Invokes within the same Module are run in the order they were provided.

Based on this and the code you're showing, things seem to be running in the expected order.

As a more general comment, if you want to enforce that certain things are run before others without having to worry about the order in which you list them, specify explicit dependencies between them appropriately. I.e., have the migration functionality produce a result that your Invoke in ServerModule depends on.