uber-go / fx

A dependency injection based application framework for Go.
https://uber-go.github.io/fx/
MIT License
5.83k stars 290 forks source link

RFC: fx.Context as a struct #126

Closed abhinav closed 7 years ago

abhinav commented 7 years ago

Following up our discussion yesterday: I would like to propose the following:

Specifically,

package fx

type Context struct{ context.Context }

func (ctx Context) Logger() Logger {
    return GetLogger(ctx)
}

func GetLogger(ctx context.Context) Logger {
    // ..
}

This has a few advantages:

The only thing to keep in mind is that accessors have to have reasonable behavior for the case where a context.Context does not have UberFx information on it.

Thoughts?

CC @breerly @kriskowal @willhug @sectioneight

prashantv commented 7 years ago

There may be performance implications to using a struct, since we'll be passing the struct by value, and when a by-value object is converted to an interface, it requires an allocation. Since the fx.Context will be passed to methods that expect a context.Context interface, it's possible that each of these will require an extra allocation to do the interface conversion.

We should ensure there's no performance hit if we want to do this.

ascandella commented 7 years ago

cc @anuptalwalkar who is doing the implementation

ascandella commented 7 years ago

And yes, the interface conversion was the primary motivation for us using an interface in he first place.

abhinav commented 7 years ago

If this affects perf negatively, an interface can still be used to get context.Context compatibility provided that UberFx maintains control of all implementations by adding a private method to the interface and providing a public function to upgrade a context.Context back to an fx.Context.

package fx

type Context interface {
    context.Context

    // unexported function to ensure only this package implements this
    // interface.
    fx()

    // ...
}

func Upgrade(context.Context) Context {  // Better name TBD
    return &context{...}
}

func NewContext(context.Context, Host) Context {
    // ...
}

type context struct {
    // ..
}

func (c *context) fx() {}

func (c *context) Logger() Logger {
    return GetLogger(c.Context)
}

This gets rid of the fx.Context -> context.Context alloc.

ascandella commented 7 years ago

FWIW I was imagining Logger() and friends would be impure (e.g. have side effects of caching the return value) On Wed, Dec 14, 2016 at 11:26 AM Abhinav Gupta notifications@github.com wrote:

If this affects perf negatively, an interface can still be used to get context.Context compatibility provided that UberFx maintains control of all implementations by adding a private method to the interface and providing a public function to upgrade a context.Context back to an fx.Context.

package fx type Context interface { context.Context

// unexported function to ensure only this package implements this
// interface.
fx()

// ...

} func Upgrade(context.Context) Context { // Better name TBD return &context{...} } func NewContext(context.Context, Host) Context { // ... } type context struct { // .. } func (c context) fx() {} func (c context) Logger() Logger { return GetLogger(c.Context) }

This gets rid of the fx.Context -> context.Context alloc.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/uber-go/fx/issues/126#issuecomment-267130611, or mute the thread https://github.com/notifications/unsubscribe-auth/AAF7P4KCgTYQ9TQNuUlfXV5IQEWQGQNJks5rIELTgaJpZM4LNTGE .

prashantv commented 7 years ago

Private method on the interface is a good idea. I'd avoid adding the Upgrade -- my understanding is that this shouldn't be needed, and the uberfx team actively wants to avoid wrapping.

anuptalwalkar commented 7 years ago

I like the private method idea to keep the implementation restricted. Is there a performance impact on doing fxCtx := ctx.(fx.Context) ?

abhinav commented 7 years ago

@anuptalwalkar ctx.(fx.Context) will not work if context.WithValue was used on an fx.Context.

fxctx := fx.NewContext(...)
ctx := context.WithValue(fxctx, ...)

fxctx2 := ctx.(fx.Context)  // this will panic

The two solutions recommended here would solve that.

@prashantv The upgrade function is needed to solve the problem mentioned above. On second thought, it doesn't have to be public, but we definitely need it.

Imagine that an UberFx InboundMiddleware adds UberFx information to incoming contexts:

func (m *ctxMiddleware) Handle(
    ctx context.Context, ..., h transport.UnaryHandler,
) error {
    fxctx := fx.NewContext(ctx, m.Host)
    return h.Handle(fxctx, ...)
}

We get this context as a context.Context at the encoding layer. We need to convert this back to an fx.Context to call the user handler. This would be done with UberFx's Thrift function wrapper.

func WrapUnary(h fxthrift.UnaryHandlerFunc) yarpcthrift.UnaryHandler {
    return func(ctx context.Context, ...) error {
       fxctx := upgrade(ctx)
       return h(fxctx, ...)
    }
}
anuptalwalkar commented 7 years ago

I am following the same approach in the refactoring. Well, ctx.(fx.Context) works in WrapUnary since it is injected in the inboundMiddleware (unless we write a middleware to do WithValue in between the calls), but in this scenario upgrade works just as well. We definitely don't want to do unnecessary wrapping.

abhinav commented 7 years ago

ctx.(fx.Context) won't work if anything between the UberFx middleware and WrapUnary calls context.WithValue on the context because the returned ctx will not have the added fx.Context functions anymore.

ascandella commented 7 years ago

Let's go with Abhinavs suggestion of pure functions and structs and write some benchmarks. It seems cleanest. On Wed, Dec 14, 2016 at 2:36 PM Abhinav Gupta notifications@github.com wrote:

ctx.(fx.Context) won't work if anything between the UberFx middleware and WrapUnary calls context.WithValue on the context because the returned ctx will not have the added fx.Context functions anymore.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/uber-go/fx/issues/126#issuecomment-267178864, or mute the thread https://github.com/notifications/unsubscribe-auth/AAF7P7z_dipuKfWLDmbZRRIafUkjKdGvks5rIG9ugaJpZM4LNTGE .

anuptalwalkar commented 7 years ago

Added benchmarks for Context conversion - https://github.com/uber-go/fx/pull/131