This guide is current as of version 0.18.0. See the :ref:`Upgrade Guide` if you're running an older version.
The code matching this guide is here.
Create a directory called keyvalue
to work inside of:
$ mkdir ~/keyvalue
$ cd ~/keyvalue
Inside of this directory we're also going to create a keyvalue
module, which
requires an __init__.py
and a setup.py
at the root:
$ mkdir keyvalue
$ touch keyvalue/__init__.py
Setup a virtual environment for your service and install the Tornado and Tchannel packages:
$ virtualenv env
$ source env/bin/activate
$ pip install 'tchannel<0.19'
Create a Thrift file under
thrift/keyvalue.thrift
that defines an interface for your service:
$ mkdir thrift
$ vim thrift/keyvalue.thrift
$ cat thrift/keyvalue.thrift
exception NotFoundError {
1: required string key,
}
service KeyValue {
string getValue(
1: string key,
) throws (
1: NotFoundError notFound,
)
void setValue(
1: string key,
2: string value,
)
}
This defines a service named KeyValue
with two functions:
getValue
- a function which takes one string parameter, and returns a string.
setValue
- a void function that takes in two parameters.
TChannel has some custom behavior so it can't use the code generated by the Apache Thrift code generator. Instead we’re going to dynamically generate our Thrift types.
Open up keyvalue/thrift.py
:
$ cat > keyvalue/thrift.py
from tchannel import thrift
service = thrift.load(path='thrift/keyvalue.thrift', service='keyvalue')
Let’s make sure everything is working:
$ python -m keyvalue.thrift
You shouldn’t see any errors. A lot of magic just happened :)
To serve an application we need to instantiate a TChannel instance, which we
will register handlers against. Open up keyvalue/server.py
and write
something like this:
from __future__ import absolute_import
from tornado import ioloop
from tornado import gen
from tchannel import TChannel
from keyvalue.thrift import service
tchannel = TChannel('keyvalue-server')
@tchannel.thrift.register(service.KeyValue)
def getValue(request):
pass
@tchannel.thrift.register(service.KeyValue)
def setValue(request):
pass
def run():
tchannel.listen()
print('Listening on %s' % tchannel.hostport)
if __name__ == '__main__':
run()
ioloop.IOLoop.current().start()
Here we have created a TChannel instance and registered two no-op handlers with it. The name of these handlers map directly to the Thrift service we defined earlier.
A TChannel server only has one requirement: a name for itself. By default an ephemeral port will be chosen to listen on (although an explicit port can be provided).
(As your application becomes more complex, you won't want to put everything in a single file like this. Good code structure is beyond the scope of this guide.)
Let's make sure this server is in a working state:
python -m keyvalue.server
Listening on localhost:8889
^C
The process should hang until you kill it, since it's listening for requests to handle. You shouldn't get any exceptions.
To implement our service's endpoints let's create an in-memory dictionary that our endpoints will manipulate:
values = {}
@tchannel.thrift.register(service.KeyValue)
def getValue(request):
key = request.body.key
value = values.get(key)
if value is None:
raise service.NotFoundError(key)
return value
@tchannel.thrift.register(service.KeyValue)
def setValue(request):
key = request.body.key
value = request.body.value
values[key] = value
You can see that the return value of getValue
will be coerced into the
expected Thrift shape. If we needed to return an additional field, we could
accomplish this by returning a dictionary.
This example service doesn't do any network IO work. If we wanted to take advantage of Tornado's asynchronous capabilities, we could define our handlers as coroutines and yield to IO operations:
@tchannel.register(service.KeyValue)
@gen.coroutine
def setValue(request):
key = request.body.key
value = request.body.value
# Simulate some non-blocking IO work.
yield gen.sleep(1.0)
values[key] = value
In addition to the call arguments and headers, the request
object also
provides some additional information about the current request under the
request.transport
object:
transport.flags
- Request flags used by the protocol for fragmentation and streaming.
transport.ttl
- The time (in milliseconds) within which the caller expects a response.
transport.headers
- Protocol level headers for the request. For more information on transport headers check the Transport Headers section of the protocol document.
As mentioned earlier, our service is listening on an ephemeral port, so we are going to register it with the Hyperbahn routing mesh. Clients will use this Hyperbahn mesh to determine how to communicate with your service.
Let's change our run method to advertise our service with a local Hyperbahn instance:
import json
import os
@gen.coroutine
def run():
tchannel.listen()
print('Listening on %s' % tchannel.hostport)
if os.path.exists('/path/to/hyperbahn_hostlist.json'):
with open('/path/to/hyperbahn_hostlist.json', 'r') as f:
hyperbahn_hostlist = json.load(f)
yield tchannel.advertise(routers=hyperbahn_hostlist)
The advertise method takes a seed list of Hyperbahn routers and the name of the service that clients will call into. After advertising, the Hyperbahn will connect to your process and establish peers for service-to-service communication.
Consult the Hyperbahn documentation for instructions on how to start a process locally.
Let's spin up the service and make a request to it through Hyperbahn. Python
provides tcurl.py
script, but we need to use the Node
version for now since it has Thrift support.
$ python keyvalue/server.py &
$ tcurl -H /path/to/hyperbahn_host_list.json -t ~/keyvalue/thrift/keyvalue.thrift keyvalue-server KeyValue::setValue -3 '{"key": "hello", "value": "world"}'
$ tcurl -H /path/to/hyperbahn_host_list.json -t ~/keyvalue/thrift/keyvalue.thrift keyvalue-server KeyValue::getValue -3 '{"key": "hello"}'
$ tcurl -H /path/to/hyperbahn_host_list.json -t ~/keyvalue/thrift/keyvalue.thrift keyvalue-server KeyValue::getValue -3 '{"key": "hi"}'
Your service can now be accessed from any language over Hyperbahn + TChannel!
Let's make a client call from Python in keyvalue/client.py
:
from tornado import gen, ioloop
from tchannel import TChannel, thrift
tchannel = TChannel('keyvalue-consumer')
service = thrift.load(
path='examples/guide/keyvalue/service.thrift',
service='keyvalue-server',
hostport='localhost:8889',
)
@gen.coroutine
def run():
yield tchannel.thrift(
service.KeyValue.setValue("foo", "Hello, world!"),
)
response = yield tchannel.thrift(
service.KeyValue.getValue("foo"),
)
print response.body
if __name__ == '__main__':
ioloop.IOLoop.current().run_sync(run)