capnproto / go-capnp

Cap'n Proto library and code generator for Go

Home Page:https://capnproto.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add Handler for unknown methods

hspaay opened this issue · comments

commented

Use-case: Proxying RPC requests is needed when the request has to travel through a gateway or via a sidecar. In this case the receiver of the request only has knowledge of the destination that can handle the request and pass it on unchanged.

The current implementation of server.Server works with a fixed list of methods. To be able to act as a proxy, a handler is needed that is invoked when the requested method is not in the methods list. The default handler can reject the request as it does today. By replacing the handler, other actions can be taken such as forwarding the request.

The proposed solution is to change Server.Recv to something like this:

func (srv *Server) Recv(ctx context.Context, r capnp.Recv) capnp.PipelineCaller {
   mm := srv.methods.find(r.Method)
	if mm == nil {
                return HandleUnknownMethod(ctx, r)
	}
	return srv.start(ctx, mm, r)
}

func (srv *Server) HandleUnknownMethod(ctx context.Context, r capnp.Recv) capnp.PipelineCaller {
		r.Reject(capnp.Unimplemented("unimplemented"))
               return nil
}

This allows a new Server implementation to define its own HandleUnknownMethod function to forward the request and returns the result.

There are probably a few pitfalls as to what is allowed in HandleUnknownMethod. I'll test it first to see if this allows the proxying of requests and if that works I'll put a PR in.

commented

Ran into the first wrench:
Override HandleUnknownMethod using embedding in a 'MyServer' struct is not possible. server.New() contains a waitgroup by value and starts a go-route to handle calls, which uses its callQueue.Recv. With embedding, the server instance gets copied and that messes up the waitgroup. Eg, this is invalid:

type MyServer struct {
   server.Server
}
func NewMyServer() *MyServer {
   mys := &MyServer{
      Server: *server.New(...)                           <- bad mojo happens when copying waitgroups like this
 }
 return &mys
}

So next attempt, is to make HandleUnknownMethod a variable that is set to the default implementation but can be replaced.

commented

Added the draft PR: #405

Just a thought: perhaps NewServer should take some kind of interface instead of []Method? This would allow us to supply implementations that override default behavior when an unknown method is invoked.

It also seems a bit "cleaner" insofar as []Method gets sorted into a sortedMethod type, upon which calls like find() are made. It seems like it would be more obvious to have something along the lines of:

type MethodFinder interface {
    FindMethod(capnp.Method) (*Method, error)
}
commented

@lthibault this an interesting alternative. I went for minimum changes but using a MethodFinder interface is cleaner.

It would mean that the sortedMethods type would have to become public and implement FindMethod. The behavior of SortedMethods is still useful when providing fallback behavior.

The PR draft now does this:

Recv(ctx context.Context, r capnp.Recv) capnp.PipelineCaller {
	mm := srv.methods.find(r.Method)
	if mm == nil && srv.HandleUnknownMethod != nil {
		mm = srv.HandleUnknownMethod(ctx, r)
	}
	if mm == nil {
		r.Reject(capnp.Unimplemented("unimplemented"))
		return nil
       }

Just curious, do you have a use-case in mind where using a custom MethodFinder works better than a hook that is called when method lookup fails, as in the above PR?

If we were going to take an interface, it should probably be something more like:

type MethodHandler interface {
        HandleMethod(context.Context, capnp.Method, *Call) error
}

...factoring out FindMethod feels too indirect to me; instead we could just move the lookup logic into SortedMethods itself.

commented

hmm, I'm starting to feel a bit lost :) Maybe there are multiple objectives?
I tried to keep the objective narrow by simply offering a hook that can dynamically create a Method that can do whatever is needed.

@lthibault Using an interface to find the method/implementation allows alternative ways to find a method, which makes sense, but isn't really needed. The other issue is that the srv.methods.find() is very private. Putting this behind an interface would require any alternative implementations to write its own sorted methods with find which is more work. Does this problem make sense or am I missing the point of the interface for this? Maybe you were thinking an interface for just the hook?

@zenhack I don't understand how to fit the MethodHandler interface as described. You are probably 5 steps ahead with this though :) The Recv function doesn't have the Call parameter so it is probably meant elsewhere?

Thank you for helping out with this. Sorry for being a bit dense :)

I think for the immediate feature we should just go with roughly what you were already working on. Maybe we'll tweak it before we tag 3.0, but it seems sane enough and is less invasive than the other things we're discussing.

My thinking was more we'd move around large parts of the logic, so the server implementation doesn't even look up a specific method, it just shells out to a MethodHandler to service whatever method it gets, and all of the logic for looking up an implementation would move into a new ServerMethods type which implements MethodHandler by looking up the method as we're doing now. Mostly this feels conceptually cleaner though, so it feels low priority.

commented

@zenhack thanks for elaborating. After working to find a good way to dynamically lookup a method I fully agree that factoring out the method lookup logic from the server processing makes a lot of sense. As you said, less of a priority.

Based on the current draft 405 can I go ahead and ask for a formal review, or would you guys prefer I change it to an interface?

commented

It is merged. Thank you all !