thriftrw / thriftrw-python

A Thrift encoding library for Python
MIT License
36 stars 10 forks source link

Client/Server API #103

Open abhinav opened 8 years ago

abhinav commented 8 years ago

Since we want thriftrw to better support HTTP (and presumably other transports that use Thrift message envelopes), let's get this discussion started.

Here's an inital API proposal for sync and async clients and servers.


For client-side support, we create a separate thriftrw-client Python package (probably a subdirectory under this project for now) that provides a thriftrw_client module with the following API:

import thriftrw
import requests
from thriftrw_client import Client

keyvalue = thriftrw.load('keyvalue.thrift')
client = Client(keyvalue.KeyValue, transport)
client.putValue("foo", keyvalue.Value("bar"))

try:
  client.getValue("foo")
except keyvalue.KeyNotFoundError:
  # ...

Where transport is any function that takes bytes to send the request and returns bytes of the response. This should suffice for synchronous clients and gevent based clients. We can provide default transport constructors like http_transport("http://.../thrift") will use httplib or something.

For asynchronous clients, we'll probably want to use thriftrw_client.tornado (and maybe later thriftrw_client.asyncio if needed) with the same interface except transport returns a future instead of the response as-is. These can be extras in the thriftrw-client's dependencies.

Open question: A generic Client class like that will have to do a bunch of introspection of the Thrift module at runtime. Do we want to do something similar to TChannel's client_for instead which generates the Client class specific to that service in one go?


For server-side, same story: A thriftrw-server package in a subdirectory with a thriftrw_server module that provides the following API:

import thriftrw
from thriftrw_server import Dispatcher

keyvalue = thriftrw.load('keyvalue.thrift')

dispatcher = Dispatcher(keyvalue.KeyValue)

@dispatcher.register
def getValue(self, key):
    # ...

@dispatcher.register
def putValue(self, key, value):
    # ...

# You can now do dispatcher(request_bytes) -> response_bytes 
#
# So, if using Flask:

@app.route('/thrift')
def keyvalue():
    return dispatcher(request.data)

Same API for async servers except calling dispatcher returns a future. It'll plug into Tornado like:

class ThriftRequestHandler(tornado.web.RequestHandler):

    @gen.coroutine
    def post(self):
        response = yield disptacher(self.request.body)
        self.write(response)

Open question: What to name Dispatcher?


Other things to consider while looking at this API:

abhinav commented 8 years ago

@blampe @breerly @junchaowu @kriskowal @prashantv @malandrew

abhinav commented 8 years ago

Server side API may need a little bit more thought. This API won't support per request state to be passed in. Like if you're implementing an HTTP service you may want to inject information from the raw HTTP request (headers, tracing information, etc.) into the endpoint handler.

Actually that brings up the same issue in the client API. We want users to be able to inject per request information from the call to the transport.

HelloGrayson commented 8 years ago

Looking good to me. One thing:

Seems like you want to always return concurrent.futures.Future in the case of sync - any reason we can't do that?

HelloGrayson commented 8 years ago

@abhinav I'm not sure that we need to support request metadata in this library - after all, Apache Thrift suffers from the same issue. We'll be solving this in our RPC library.