uber-go/fx

fx.Options and fx.Module behave differently

djmitche opened this issue · 4 comments

Describe the bug

It appears that fx.Invoke's that are in an fx.Module do not run until after all fx.Invoke's outside the module.

This is not documented in fx.Module, and fx.Invoke's docs say

Invoke registers functions that are executed eagerly on application start. Arguments for these invocations are built using the constructors registered by Provide. Passing multiple Invoke options appends the new invocations to the application's existing list.

Unlike constructors, invocations are always executed, and they're always run in order. Invocations may have any number of returned values. If the final returned object is an error, it indicates whether the operation was successful. All other returned values are discarded.

emphasis on in order :)

To Reproduce
https://go.dev/play/p/SP93TiMiji9

package main

import (
	"context"
	"fmt"

	"go.uber.org/fx"
)

func main() {
	app := fx.New(
		fx.Invoke(func() { fmt.Printf("Outer invoke 1\n") }),
		fx.Module(
			"mod",
			fx.Invoke(func() { fmt.Printf("Module invoke\n") }),
		),
		fx.Invoke(func() { fmt.Printf("Outer invoke 2\n") }),
	)
	app.Start(context.Background())
	app.Stop(context.Background())
}

)

Actual behavior

Outer invoke 1
Outer invoke 2
Module invoke

Expected behavior

Outer invoke 1
Module invoke
Outer invoke 2

Additional context
Changing fx.Module to fx.Options (and removing the "mod") makes this behave as expected.

In the use-case I'm working on, I want to write

app := fx.New(
  someModule,
  anotherModule,
  fx.Invoke(func(lc fx.Lifecycle) {
    lc.Append(fx.Hook{OnStart: ...})
  }),
)

and know that, because it's the last invocation, that fx.Hook is the last thing started in the lifecycle. As it is, everything in the modules is starting after my top-level hook.

This is a valid issue, thanks for reporting this.

Tracking internal issue: GO-1591.

BTW, just to clarify what the issue is - the issue isn't that fx.Options and fx.Module behaves differently. They do behave differently because they're meant for doing different things :)

There are two issues that this issue is tracking:

  1. Lack of documentation on the invoke order when fx.Module is involved.
  2. fx.Module invokes are run after the parent-level invokes.

Fixed with #925.