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.OnStart dependency missing, sometimes #1088

Closed
advdv opened this issue May 26, 2023 · 3 comments
Closed

fx.OnStart dependency missing, sometimes #1088

advdv opened this issue May 26, 2023 · 3 comments

Comments

@advdv
Copy link

advdv commented May 26, 2023

Describe the bug
fx.OnStart is not able to resolve my *net.TCPListener dependency in the expected way. It does work in an unexpected way, see below for explanation.

To Reproduce
Let's consider the example below for a webserver module.

// Package webserver implements the http serving
package webserver

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/netip"

	"go.uber.org/fx"
)

// NewListener provides a tcp connection listener for the webserver.
func NewListener() (*net.TCPListener, error) {
	ap, err := netip.ParseAddrPort("[::1]:8383")
	if err != nil {
		return nil, fmt.Errorf("failed to parse addr/port: %w", err)
	}

	ln, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(ap))
	if err != nil {
		return nil, fmt.Errorf("failed to listen: %w", err)
	}

	return ln, nil
}

// New inits the http server.
func New(h http.Handler) *http.Server {
	return &http.Server{
		Handler: h,
	}
}

// moduleName standardizes the module name.
const moduleName = "webhandler"

// Prod dependencies.
func Prod() fx.Option {
	return fx.Module(moduleName,
		fx.Provide(fx.Annotate(NewListener)),
		fx.Provide(fx.Annotate(New,
			fx.OnStart(func(ctx context.Context, ln *net.TCPListener, s *http.Server) error {
				go s.Serve(ln) //nolint:errcheck

				return nil
			}),
			fx.OnStop(func(ctx context.Context, s *http.Server) error {
				if err := s.Shutdown(ctx); err != nil {
					return fmt.Errorf("failed to shut down: %w", err)
				}

				return nil
			}))),
	)
}

Given a test file like this:

package webserver

import (
	"context"
	"net/http"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"go.uber.org/fx"
)

func TestWebserver(t *testing.T) {
	t.Parallel()
	RegisterFailHandler(Fail)
	RunSpecs(t, "web/webserver")
}

var _ = Describe("failed to handle webserver", func() {
	BeforeEach(func(ctx context.Context) {
		app := fx.New(Prod(),
			fx.Invoke(func(s *http.Server) {}),
			fx.Supply(fx.Annotate(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}),
				fx.As(new(http.Handler)))))
		Expect(app.Start(ctx)).To(Succeed())
		DeferCleanup(app.Done)
	})

	It("should server http", func() {
              // ....
	})
})

Running go test will result in an error like this:

error invoking hook installer: missing dependencies for function \"reflect\".makeFuncStub (/usr/local/go/src/reflect/asm_arm64.s:29): missing type: *net.TCPListener",

But, if I change the signature of the New function, like this:

// Package webserver implements the http serving
package webserver

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/netip"

	"go.uber.org/fx"
)

// NewListener provides a tcp connection listener for the webserver.
func NewListener() (*net.TCPListener, error) {
	ap, err := netip.ParseAddrPort("[::1]:8383")
	if err != nil {
		return nil, fmt.Errorf("failed to parse addr/port: %w", err)
	}

	ln, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(ap))
	if err != nil {
		return nil, fmt.Errorf("failed to listen: %w", err)
	}

	return ln, nil
}

// New inits the http server. NOTE: I've added the *net.TCPListener as a dependency even though 
// we don't need it to make the *http.Server
func New(h http.Handler, _ *net.TCPListener) *http.Server {
	return &http.Server{
		Handler: h,
	}
}

// moduleName standardizes the module name.
const moduleName = "webhandler"

// Prod dependencies.
func Prod() fx.Option {
	return fx.Module(moduleName,
		fx.Provide(fx.Annotate(NewListener)),
		fx.Provide(fx.Annotate(New,
			fx.OnStart(func(ctx context.Context, ln *net.TCPListener, s *http.Server) error {
				go s.Serve(ln) //nolint:errcheck

				return nil
			}),
			fx.OnStop(func(ctx context.Context, s *http.Server) error {
				if err := s.Shutdown(ctx); err != nil {
					return fmt.Errorf("failed to shut down: %w", err)
				}

				return nil
			}))),
	)
}

It will work.

Expected behavior
I would expect the first setup above to work without missing dependency error.

Additional context
I'm pretty sure this used to work in the past since I THINK I've written this code before.

@sywhang
Copy link
Contributor

sywhang commented May 30, 2023

Hey @advanderveer,

Hooks passed as OnStart hooks are not allowed to take dependencies outside of the provider's direct dependencies or result types.

See: https://pkg.go.dev/go.uber.org/fx#OnStart:
"The hook function passed into OnStart cannot take any arguments outside of the annotated constructor's existing dependencies or results, except a context.Context."

I'm pretty sure this used to work in the past since I THINK I've written this code before.

That was actually an unintentional effect of a bug that was fixed in #983.

@advdv
Copy link
Author

advdv commented May 31, 2023

That explains it then, thank you for the clear response. I guess a more clear error would be better (it could be detected that a requested dependency is invalid) but since it is already documented I guess this can also be closed.

@sywhang
Copy link
Contributor

sywhang commented Jun 1, 2023

I guess a more clear error would be better (it could be detected that a requested dependency is invalid)

That's a valid ask. I've filed #1090 to track that.

@sywhang sywhang closed this as completed Jun 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants