diff --git a/.coveragerc b/.coveragerc index 63ef4362..7abef57e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,7 +10,7 @@ exclude_lines = omit = tchannel/zipkin/thrift/* tchannel/testing/vcr/proxy/* - tests/data/generated/ThriftTest/* + tchannel/testing/data/* [xml] output = coverage.xml diff --git a/.gitignore b/.gitignore index 98e229ef..456a8bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ node_modules/ env/ *.xml *.egg-info -.coverage +*.coverage* *.pyc .phutil_module_cache build/ diff --git a/CHANGES.rst b/CHANGES.rst index 7cbd2a10..2c390439 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,10 +2,19 @@ Changelog ========= -0.14.1 (unreleased) +0.15.0 (unreleased) ------------------- -- Nothing changed yet. +- Introduced new top level ``tchannel.TChannel`` object, with new request methods + ``call``, ``raw``, ``json``, and ``thrift``. This will eventually replace the + akward ``request`` / ``send`` calling pattern. +- Introduced ``tchannel.from_thrift_module`` function for creating a request builder + to be used with the ``tchannel.TChannel.thrift`` function. +- Introduced new simplified examples under the ``examples/simple`` directory, moved + the Guide's examples to ``examples/guide``, and deleted the remaining examples. +- Added ThriftTest.thrift and generated Thrift code to ``tchannel.testing.data`` for + use with examples and playing around with TChannel. +- Fix JSON arg2 (headers) being returned a string instead of a dict. 0.14.0 (2015-08-03) diff --git a/UPGRADE.rst b/UPGRADE.rst index 8a2a53cd..73e99b09 100644 --- a/UPGRADE.rst +++ b/UPGRADE.rst @@ -4,6 +4,22 @@ Version Upgrade Guide Migrating to a version of TChannel with breaking changes? This guide documents what broke and how to safely migrate to newer versions. +From 0.14 to 0.15 +----------------- + +- No breaking changes. + +From 0.13 to 0.14 +----------------- + +- No breaking changes. + +From 0.12 to 0.13 +----------------- + +- No breaking changes. + + From 0.11 to 0.12 ----------------- diff --git a/examples/keyvalue/keyvalue/__init__.py b/examples/guide/keyvalue/keyvalue/__init__.py similarity index 100% rename from examples/keyvalue/keyvalue/__init__.py rename to examples/guide/keyvalue/keyvalue/__init__.py diff --git a/examples/keyvalue/keyvalue/client.py b/examples/guide/keyvalue/keyvalue/client.py similarity index 100% rename from examples/keyvalue/keyvalue/client.py rename to examples/guide/keyvalue/keyvalue/client.py diff --git a/examples/keyvalue/keyvalue/server.py b/examples/guide/keyvalue/keyvalue/server.py similarity index 100% rename from examples/keyvalue/keyvalue/server.py rename to examples/guide/keyvalue/keyvalue/server.py diff --git a/examples/keyvalue/keyvalue/service/KeyValue-remote b/examples/guide/keyvalue/keyvalue/service/KeyValue-remote similarity index 100% rename from examples/keyvalue/keyvalue/service/KeyValue-remote rename to examples/guide/keyvalue/keyvalue/service/KeyValue-remote diff --git a/examples/keyvalue/keyvalue/service/KeyValue.py b/examples/guide/keyvalue/keyvalue/service/KeyValue.py similarity index 100% rename from examples/keyvalue/keyvalue/service/KeyValue.py rename to examples/guide/keyvalue/keyvalue/service/KeyValue.py diff --git a/examples/keyvalue/keyvalue/service/__init__.py b/examples/guide/keyvalue/keyvalue/service/__init__.py similarity index 100% rename from examples/keyvalue/keyvalue/service/__init__.py rename to examples/guide/keyvalue/keyvalue/service/__init__.py diff --git a/examples/keyvalue/keyvalue/service/constants.py b/examples/guide/keyvalue/keyvalue/service/constants.py similarity index 100% rename from examples/keyvalue/keyvalue/service/constants.py rename to examples/guide/keyvalue/keyvalue/service/constants.py diff --git a/examples/keyvalue/keyvalue/service/ttypes.py b/examples/guide/keyvalue/keyvalue/service/ttypes.py similarity index 100% rename from examples/keyvalue/keyvalue/service/ttypes.py rename to examples/guide/keyvalue/keyvalue/service/ttypes.py diff --git a/examples/keyvalue/setup.py b/examples/guide/keyvalue/setup.py similarity index 100% rename from examples/keyvalue/setup.py rename to examples/guide/keyvalue/setup.py diff --git a/examples/keyvalue/thrift/service.thrift b/examples/guide/keyvalue/thrift/service.thrift similarity index 100% rename from examples/keyvalue/thrift/service.thrift rename to examples/guide/keyvalue/thrift/service.thrift diff --git a/examples/handlers.py b/examples/handlers.py deleted file mode 100644 index 6b22b761..00000000 --- a/examples/handlers.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import random - -import tornado.gen - - -@tornado.gen.coroutine -def say_hi(request, response, proxy): - yield response.write_body("Hello, world!") - - -@tornado.gen.coroutine -def echo(request, response, proxy): - # stream args right back to request side - response.set_header_s(request.get_header_s()) - response.set_body_s(request.get_body_s()) - - -@tornado.gen.coroutine -def slow(request, response, proxy): - yield tornado.gen.sleep(random.random()) - yield response.write_body("done") - response.flush() - - -def register_example_endpoints(tchannel): - tchannel.register(endpoint="hi", scheme="raw", handler=say_hi) - tchannel.register(endpoint="echo", scheme="raw", handler=echo) - tchannel.register(endpoint="slow", scheme="raw", handler=slow) - - @tchannel.register("bye", scheme="raw") - def say_bye(request, response, proxy): - print (yield request.get_header()) - print (yield request.get_body()) - - response.write_body("world") diff --git a/examples/json_client.py b/examples/json_client.py deleted file mode 100755 index a48065fa..00000000 --- a/examples/json_client.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import tornado.ioloop -import tornado.iostream - -from options import get_args -from tchannel.tornado import TChannel -from tchannel.tornado.broker import ArgSchemeBroker -from tchannel.scheme import JsonArgScheme - - -@tornado.gen.coroutine -def main(): - - args = get_args() - - tchannel = TChannel(name='json-client') - - # TODO: Make this API friendly. - request = tchannel.request( - hostport='%s:%s' % (args.host, args.port), - ) - - response = yield ArgSchemeBroker(JsonArgScheme()).send( - request, - 'hi-json', - None, - None, - ) - - body = yield response.get_body() - - print body['hi'] - - -if __name__ == '__main__': - tornado.ioloop.IOLoop.instance().run_sync(main) diff --git a/examples/json_server.py b/examples/json_server.py deleted file mode 100755 index 4ee0e150..00000000 --- a/examples/json_server.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import tornado.ioloop - -from handlers import register_example_endpoints -from options import get_args -from tchannel.tornado import TChannel - - -def main(): - args = get_args() - - app = TChannel( - name='json-server', - hostport='%s:%d' % (args.host, args.port), - ) - - register_example_endpoints(app) - - def say_hi_json(request, response, proxy): - response.write_body({'hi': 'Hello, world!'}) - - app.register(endpoint="hi-json", scheme="json", handler=say_hi_json) - - app.listen() - - tornado.ioloop.IOLoop.instance().start() - - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/examples/options.py b/examples/options.py deleted file mode 100644 index daa362c6..00000000 --- a/examples/options.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import argparse - - -def get_parser(): - parser = argparse.ArgumentParser() - parser.add_argument( - "--port", - dest="port", default=8888, type=int, - ) - parser.add_argument( - "--host", - dest="host", default="localhost" - ) - return parser - - -def get_args(): - parser = get_parser() - return parser.parse_args() diff --git a/examples/raw_client.py b/examples/raw_client.py deleted file mode 100755 index af2024de..00000000 --- a/examples/raw_client.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import tornado.ioloop -import tornado.iostream - -from options import get_args -from tchannel.tornado import TChannel - - -@tornado.gen.coroutine -def main(): - - args = get_args() - - tchannel = TChannel(name='raw-client') - - request = tchannel.request( - hostport='%s:%s' % (args.host, args.port), - ) - - response = yield request.send('hi', None, None) - - body = yield response.get_body() - - print body - - -if __name__ == '__main__': - tornado.ioloop.IOLoop.instance().run_sync(main) diff --git a/examples/raw_server.py b/examples/raw_server.py deleted file mode 100755 index 8efd7a71..00000000 --- a/examples/raw_server.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import sys - -import tornado.ioloop - -from handlers import register_example_endpoints -from options import get_args -from tchannel.tornado import TChannel - - -def main(): - args = get_args() - - app = TChannel( - name='raw-server', - hostport='%s:%d' % (args.host, args.port), - ) - - register_example_endpoints(app) - - app.listen() - - print("listening on %s" % app.hostport) - sys.stdout.flush() - - tornado.ioloop.IOLoop.instance().start() - - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/examples/simple/json/client.py b/examples/simple/json/client.py new file mode 100644 index 00000000..76deb63e --- /dev/null +++ b/examples/simple/json/client.py @@ -0,0 +1,39 @@ +import json + +from tornado import gen, ioloop + +from tchannel import TChannel + + +tchannel = TChannel('json-client') + + +@gen.coroutine +def make_request(): + + resp = yield tchannel.json( + service='json-server', + endpoint='endpoint', + body={ + 'req': 'body' + }, + headers={ + 'req': 'header' + }, + hostport='localhost:54496', + ) + + raise gen.Return(resp) + + +resp = ioloop.IOLoop.current().run_sync(make_request) + +assert resp.headers == { + 'resp': 'header', +} +assert resp.body == { + 'resp': 'body', +} + +print json.dumps(resp.body) +print json.dumps(resp.headers) diff --git a/examples/simple/json/server.py b/examples/simple/json/server.py new file mode 100644 index 00000000..5aadcd87 --- /dev/null +++ b/examples/simple/json/server.py @@ -0,0 +1,35 @@ +from tornado import gen, ioloop + +from tchannel import TChannel, schemes + + +app = TChannel('json-server', hostport='localhost:54496') + + +@app.register('endpoint', schemes.JSON) +@gen.coroutine +def endpoint(request, response, proxy): + + header = yield request.get_header() + body = yield request.get_body() + + assert header == { + 'req': 'header', + } + assert body == { + 'req': 'body', + } + + response.write_header({ + 'resp': 'header', + }) + response.write_body({ + 'resp': 'body', + }) + + +app.listen() + +print app.hostport + +ioloop.IOLoop.current().start() diff --git a/examples/simple/raw/client.py b/examples/simple/raw/client.py new file mode 100644 index 00000000..e207db87 --- /dev/null +++ b/examples/simple/raw/client.py @@ -0,0 +1,29 @@ +from tornado import gen, ioloop + +from tchannel import TChannel + + +tchannel = TChannel('raw-client') + + +@gen.coroutine +def make_request(): + + resp = yield tchannel.raw( + service='raw-server', + endpoint='endpoint', + body='req body', + headers='req headers', + hostport='localhost:54495', + ) + + raise gen.Return(resp) + + +resp = ioloop.IOLoop.current().run_sync(make_request) + +assert resp.headers == 'resp headers' +assert resp.body == 'resp body' + +print resp.body +print resp.headers diff --git a/examples/simple/raw/server.py b/examples/simple/raw/server.py new file mode 100644 index 00000000..85521fe0 --- /dev/null +++ b/examples/simple/raw/server.py @@ -0,0 +1,27 @@ +from tornado import gen, ioloop + +from tchannel import TChannel + + +app = TChannel('raw-server', hostport='localhost:54495') + + +@app.register('endpoint') +@gen.coroutine +def endpoint(request, response, proxy): + + header = yield request.get_header() + body = yield request.get_body() + + assert header == 'req headers' + assert body == 'req body' + + response.write_header('resp headers') + response.write_body('resp body') + + +app.listen() + +print app.hostport + +ioloop.IOLoop.current().start() diff --git a/examples/simple/thrift/client.py b/examples/simple/thrift/client.py new file mode 100644 index 00000000..ba22672c --- /dev/null +++ b/examples/simple/thrift/client.py @@ -0,0 +1,38 @@ +import json + +from tornado import gen, ioloop +from tchannel import TChannel, from_thrift_module +from tchannel.testing.data.generated.ThriftTest import ThriftTest + + +tchannel = TChannel('thrift-client') + +service = from_thrift_module( + service='thrift-server', + thrift_module=ThriftTest, + hostport='localhost:54497' +) + + +@gen.coroutine +def make_request(): + + resp = yield tchannel.thrift( + request=service.testString(thing="req"), + headers={ + 'req': 'header', + }, + ) + + raise gen.Return(resp) + + +resp = ioloop.IOLoop.current().run_sync(make_request) + +assert resp.headers == { + 'resp': 'header', +} +assert resp.body == 'resp' + +print resp.body +print json.dumps(resp.headers) diff --git a/examples/simple/thrift/server.py b/examples/simple/thrift/server.py new file mode 100644 index 00000000..b85bae45 --- /dev/null +++ b/examples/simple/thrift/server.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import + +from tornado import gen, ioloop +from tchannel import TChannel +from tchannel.testing.data.generated.ThriftTest import ThriftTest + + +app = TChannel('thrift-server', hostport='localhost:54497') + + +@app.register(ThriftTest) +@gen.coroutine +def testString(request, response, tchannel): + + assert request.headers == [['req', 'header']] + assert request.args.thing == 'req' + + response.write_header('resp', 'header') + response.write_result('resp') + + +app.listen() + +print app.hostport + +ioloop.IOLoop.current().start() diff --git a/examples/stream_client.py b/examples/stream_client.py deleted file mode 100755 index 96481780..00000000 --- a/examples/stream_client.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import os -import sys - -import tornado -import tornado.ioloop - -from options import get_parser -from tchannel.tornado import TChannel -from tchannel.tornado.stream import PipeStream - - -@tornado.gen.coroutine -def send_stream(arg1, arg2, arg3, host): - tchannel = TChannel( - name='stream-client', - ) - - response = yield tchannel.request(host).send( - arg1, - arg2, - arg3, - ) - - # Call get_body() to wait for the call to conclude; use - # get_body_s to read the stream as it comes. - body = yield response.get_body() - print body - - -def main(): - parser = get_parser() - parser.add_argument( - "--file", - dest="filename" - ) - args = parser.parse_args() - - hostport = "%s:%s" % (args.host, args.port) - - arg1 = 'hi-stream' - arg2 = None - arg3 = None - - ioloop = tornado.ioloop.IOLoop.current() - - if args.filename == "stdin": - arg3 = PipeStream(sys.stdin.fileno()) - send_stream(arg1, arg2, arg3, hostport) - return ioloop.start() - elif args.filename: - f = os.open(args.filename, os.O_RDONLY) - arg3 = PipeStream(f) - else: - arg3 = 'foo' - - ioloop.run_sync(lambda: send_stream(arg1, arg2, arg3, hostport)) - - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/examples/stream_server.py b/examples/stream_server.py deleted file mode 100644 index c9bc31bc..00000000 --- a/examples/stream_server.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from __future__ import absolute_import - -import tornado.ioloop - -from handlers import register_example_endpoints -from options import get_args -from tchannel.tornado import TChannel -from tchannel.tornado.stream import InMemStream - - -def main(): - args = get_args() - - app = TChannel( - name='stream-server', - hostport='%s:%d' % (args.host, args.port), - ) - - register_example_endpoints(app) - - @tornado.gen.coroutine - def say_hi_stream(request, response, proxy): - out_stream = InMemStream() - response.set_body_s(out_stream) - - # TODO: Need to be able to flush without closing the stream. - for character in 'Hello, world!': - yield out_stream.write(character) - - app.register(endpoint="hi-stream", handler=say_hi_stream) - - app.listen() - - tornado.ioloop.IOLoop.instance().start() - - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/examples/thrift_examples/client.py b/examples/thrift_examples/client.py deleted file mode 100644 index f9c0149f..00000000 --- a/examples/thrift_examples/client.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from tornado import gen -from tornado import ioloop - -from hello import HelloService -from tchannel.thrift import client_for -from tchannel.tornado import TChannel - - -@gen.coroutine -def run(): - app = TChannel(name='thrift-client') - - client = client_for('hello', HelloService)(app, 'localhost:8888') - - response = yield client.hello("world") - - print response - - -if __name__ == '__main__': # pragma: no cover - ioloop.IOLoop.current().run_sync(run) diff --git a/examples/thrift_examples/hello.thrift b/examples/thrift_examples/hello.thrift deleted file mode 100644 index 649800d6..00000000 --- a/examples/thrift_examples/hello.thrift +++ /dev/null @@ -1,3 +0,0 @@ -service HelloService { - string hello(1: string name) -} diff --git a/examples/thrift_examples/hello/HelloService.py b/examples/thrift_examples/hello/HelloService.py deleted file mode 100644 index 09ed63fa..00000000 --- a/examples/thrift_examples/hello/HelloService.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# -# Autogenerated by Thrift Compiler (0.9.2) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py:tornado,dynamic,utf8strings -# - -from thrift.protocol.TBase import TBase -from thrift.protocol.TBase import TExceptionBase -from thrift.Thrift import TApplicationException -from thrift.Thrift import TException -from thrift.Thrift import TMessageType -from thrift.Thrift import TProcessor -from thrift.Thrift import TType -from thrift.transport import TTransport -from tornado import concurrent -from tornado import gen - -from ttypes import * - - -class Iface(object): - def hello(self, name): - """ - Parameters: - - name - """ - pass - - -class Client(Iface): - def __init__(self, transport, iprot_factory, oprot_factory=None): - self._transport = transport - self._iprot_factory = iprot_factory - self._oprot_factory = (oprot_factory if oprot_factory is not None - else iprot_factory) - self._seqid = 0 - self._reqs = {} - self._transport.io_loop.spawn_callback(self._start_receiving) - - @gen.engine - def _start_receiving(self): - while True: - try: - frame = yield self._transport.readFrame() - except TTransport.TTransportException as e: - for future in self._reqs.itervalues(): - future.set_exception(e) - self._reqs = {} - return - tr = TTransport.TMemoryBuffer(frame) - iprot = self._iprot_factory.getProtocol(tr) - (fname, mtype, rseqid) = iprot.readMessageBegin() - future = self._reqs.pop(rseqid, None) - if not future: - # future has already been discarded - continue - method = getattr(self, 'recv_' + fname) - try: - result = method(iprot, mtype, rseqid) - except Exception as e: - future.set_exception(e) - else: - future.set_result(result) - - def hello(self, name): - """ - Parameters: - - name - """ - self._seqid += 1 - future = self._reqs[self._seqid] = concurrent.Future() - self.send_hello(name) - return future - - def send_hello(self, name): - oprot = self._oprot_factory.getProtocol(self._transport) - oprot.writeMessageBegin('hello', TMessageType.CALL, self._seqid) - args = hello_args() - args.name = name - args.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - - def recv_hello(self, iprot, mtype, rseqid): - if mtype == TMessageType.EXCEPTION: - x = TApplicationException() - x.read(iprot) - iprot.readMessageEnd() - raise x - result = hello_result() - result.read(iprot) - iprot.readMessageEnd() - if result.success is not None: - return result.success - raise TApplicationException(TApplicationException.MISSING_RESULT, "hello failed: unknown result"); - - -class Processor(Iface, TProcessor): - def __init__(self, handler): - self._handler = handler - self._processMap = {} - self._processMap["hello"] = Processor.process_hello - - def process(self, iprot, oprot): - (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap: - iprot.skip(TType.STRUCT) - iprot.readMessageEnd() - x = TApplicationException(TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % (name)) - oprot.writeMessageBegin(name, TMessageType.EXCEPTION, seqid) - x.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - return - else: - return self._processMap[name](self, seqid, iprot, oprot) - - @gen.coroutine - def process_hello(self, seqid, iprot, oprot): - args = hello_args() - args.read(iprot) - iprot.readMessageEnd() - result = hello_result() - result.success = yield gen.maybe_future(self._handler.hello(args.name)) - oprot.writeMessageBegin("hello", TMessageType.REPLY, seqid) - result.write(oprot) - oprot.writeMessageEnd() - oprot.trans.flush() - - -# HELPER FUNCTIONS AND STRUCTURES - -class hello_args(TBase): - """ - Attributes: - - name - """ - - thrift_spec = ( - None, # 0 - (1, TType.STRING, 'name', None, None, ), # 1 - ) - - def __init__(self, name=None,): - self.name = name - - def __hash__(self): - value = 17 - value = (value * 31) ^ hash(self.name) - return value - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.iteritems()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) - -class hello_result(TBase): - """ - Attributes: - - success - """ - - thrift_spec = ( - (0, TType.STRING, 'success', None, None, ), # 0 - ) - - def __init__(self, success=None,): - self.success = success - - def __hash__(self): - value = 17 - value = (value * 31) ^ hash(self.success) - return value - - def __repr__(self): - L = ['%s=%r' % (key, value) - for key, value in self.__dict__.iteritems()] - return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not (self == other) diff --git a/examples/thrift_examples/hello/__init__.py b/examples/thrift_examples/hello/__init__.py deleted file mode 100644 index f5554aa6..00000000 --- a/examples/thrift_examples/hello/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -__all__ = ['ttypes', 'constants', 'HelloService'] diff --git a/examples/thrift_examples/hello/constants.py b/examples/thrift_examples/hello/constants.py deleted file mode 100644 index 3d4cf85c..00000000 --- a/examples/thrift_examples/hello/constants.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# -# Autogenerated by Thrift Compiler (0.9.2) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py:tornado,dynamic,utf8strings -# - -from thrift.Thrift import TApplicationException -from thrift.Thrift import TException -from thrift.Thrift import TMessageType -from thrift.Thrift import TType - -from ttypes import * diff --git a/examples/thrift_examples/hello/ttypes.py b/examples/thrift_examples/hello/ttypes.py deleted file mode 100644 index 61e947ed..00000000 --- a/examples/thrift_examples/hello/ttypes.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# -# Autogenerated by Thrift Compiler (0.9.2) -# -# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING -# -# options string: py:tornado,dynamic,utf8strings -# - -from thrift.protocol.TBase import TBase -from thrift.protocol.TBase import TExceptionBase -from thrift.Thrift import TApplicationException -from thrift.Thrift import TException -from thrift.Thrift import TMessageType -from thrift.Thrift import TType diff --git a/examples/thrift_examples/server.py b/examples/thrift_examples/server.py deleted file mode 100644 index f7c3b272..00000000 --- a/examples/thrift_examples/server.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -from tornado import ioloop - -from hello import HelloService -from tchannel.tornado import TChannel - -app = TChannel('thrift-server', 'localhost:8888') - - -@app.register(HelloService) -def hello(request, response, tchannel): - name = request.args.name - print "Hello, %s!" % name - return "Hello, %s!" % name - - -def run(): - app.listen() - ioloop.IOLoop.current().start() - - -if __name__ == '__main__': - run() diff --git a/setup.cfg b/setup.cfg index 61479714..9ea8197f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,8 @@ create-wheel = yes [flake8] # Ignore files generated by Thrift -exclude = examples/keyvalue/keyvalue/service/*,examples/thrift_examples/hello/*,tchannel/zipkin/thrift/*,tchannel/testing/vcr/proxy/*,tests/data/generated/ThriftTest/* +exclude = + examples/guide/keyvalue/keyvalue/service/*, + tchannel/zipkin/thrift/*, + tchannel/testing/vcr/proxy/*, + tchannel/testing/data/generated/ThriftTest/* diff --git a/tchannel/__init__.py b/tchannel/__init__.py index e69de29b..0c1b8014 100644 --- a/tchannel/__init__.py +++ b/tchannel/__init__.py @@ -0,0 +1,6 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .tchannel import TChannel # noqa +from .thrift import from_thrift_module # noqa diff --git a/tests/data/generated/__init__.py b/tchannel/dep/__init__.py similarity index 100% rename from tests/data/generated/__init__.py rename to tchannel/dep/__init__.py diff --git a/tchannel/thrift/scheme.py b/tchannel/dep/thrift_arg_scheme.py similarity index 88% rename from tchannel/thrift/scheme.py rename to tchannel/dep/thrift_arg_scheme.py index cfa007da..476468df 100644 --- a/tchannel/thrift/scheme.py +++ b/tchannel/dep/thrift_arg_scheme.py @@ -23,12 +23,12 @@ from thrift.protocol import TBinaryProtocol from thrift.transport import TTransport -from .. import io -from .. import rw -from .. import scheme +from tchannel import io +from tchannel import rw +from tchannel import scheme -class ThriftArgScheme(scheme.ArgScheme): +class DeprecatedThriftArgScheme(scheme.ArgScheme): """Represents the ``thrift`` arg scheme. It requires a reference to the result type for deserialized objects. @@ -42,12 +42,6 @@ class ThriftArgScheme(scheme.ArgScheme): ) def __init__(self, deserialize_type): - """Initialize a new ThriftArgScheme. - - :param deserialize_type: - Type of Thrift object contained in the body. This object will be - deserialized from the stream. - """ self.deserialize_type = deserialize_type def type(self): diff --git a/tchannel/errors.py b/tchannel/errors.py index 98077a13..e6f0441f 100644 --- a/tchannel/errors.py +++ b/tchannel/errors.py @@ -119,3 +119,8 @@ def __init__(self, code, args): self.code = code self.args = args + + +class OneWayNotSupportedError(TChannelError): + """Raised when oneway Thrift procedure is called.""" + pass diff --git a/tchannel/glossary.py b/tchannel/glossary.py index 48c7423a..36bff036 100644 --- a/tchannel/glossary.py +++ b/tchannel/glossary.py @@ -24,6 +24,4 @@ MAX_MESSAGE_ID = 0xfffffffe # CallRequestMessage uses it as the default TTL value for the message. -DEFAULT_TTL = 1000 # ms -MAX_ATTEMPT_TIMES = 3 -RETRY_DELAY = 0.3 # 300 ms +DEFAULT_TIMEOUT = 1000 # ms diff --git a/tchannel/messages/call_request.py b/tchannel/messages/call_request.py index b1a56344..1fe0792d 100644 --- a/tchannel/messages/call_request.py +++ b/tchannel/messages/call_request.py @@ -22,7 +22,7 @@ from . import common from .. import rw -from ..glossary import DEFAULT_TTL +from ..glossary import DEFAULT_TIMEOUT from .call_request_continue import CallRequestContinueMessage from .types import Types @@ -41,7 +41,7 @@ class CallRequestMessage(CallRequestContinueMessage): def __init__( self, flags=0, - ttl=DEFAULT_TTL, + ttl=DEFAULT_TIMEOUT, tracing=None, service=None, headers=None, diff --git a/tchannel/messages/cancel.py b/tchannel/messages/cancel.py index db5a2594..c36f04df 100644 --- a/tchannel/messages/cancel.py +++ b/tchannel/messages/cancel.py @@ -22,7 +22,7 @@ from . import common from .. import rw -from ..glossary import DEFAULT_TTL +from ..glossary import DEFAULT_TIMEOUT from .base import BaseMessage @@ -33,7 +33,7 @@ class CancelMessage(BaseMessage): 'why', ) - def __init__(self, ttl=DEFAULT_TTL, tracing=None, why=None, id=0): + def __init__(self, ttl=DEFAULT_TIMEOUT, tracing=None, why=None, id=0): super(CancelMessage, self).__init__(id) self.ttl = ttl self.tracing = tracing or common.Tracing(0, 0, 0, 0) diff --git a/tchannel/messages/claim.py b/tchannel/messages/claim.py index f5f0d2f5..5400a09e 100644 --- a/tchannel/messages/claim.py +++ b/tchannel/messages/claim.py @@ -22,7 +22,7 @@ from . import common from .. import rw -from ..glossary import DEFAULT_TTL +from ..glossary import DEFAULT_TIMEOUT from .base import BaseMessage @@ -32,7 +32,7 @@ class ClaimMessage(BaseMessage): 'tracing', ) - def __init__(self, ttl=DEFAULT_TTL, tracing=None, id=0): + def __init__(self, ttl=DEFAULT_TIMEOUT, tracing=None, id=0): super(ClaimMessage, self).__init__(id) self.ttl = ttl self.tracing = tracing or common.Tracing(0, 0, 0, 0) diff --git a/tchannel/response.py b/tchannel/response.py new file mode 100644 index 00000000..da31c8df --- /dev/null +++ b/tchannel/response.py @@ -0,0 +1,27 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from . import transport + +__all__ = ['Response'] + + +class Response(object): + + # TODO add __slots__ + # TODO implement __repr__ + + def __init__(self, headers, body, transport): + self.headers = headers + self.body = body + self.transport = transport + + +class ResponseTransportHeaders(transport.TransportHeaders): + + # TODO add __slots__ + # TODO implement __repr__ + + """Response-specific Transport Headers""" + pass diff --git a/tchannel/retry.py b/tchannel/retry.py new file mode 100644 index 00000000..ea701a10 --- /dev/null +++ b/tchannel/retry.py @@ -0,0 +1,12 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +NEVER = 'n' +CONNECTION_ERROR = 'c' +TIMEOUT = 't' +CONNECTION_ERROR_AND_TIMEOUT = 'ct' +DEFAULT = CONNECTION_ERROR + +DEFAULT_RETRY_LIMIT = 3 +DEFAULT_RETRY_DELAY = 0.3 # 300 ms diff --git a/tchannel/schemes/__init__.py b/tchannel/schemes/__init__.py new file mode 100644 index 00000000..edb3e5fc --- /dev/null +++ b/tchannel/schemes/__init__.py @@ -0,0 +1,26 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +RAW = 'raw' +JSON = 'json' +THRIFT = 'thrift' +DEFAULT = RAW + +DEFAULT_NAMES = ( + RAW, + JSON, + THRIFT +) + +from .raw import RawArgScheme # noqa +from .json import JsonArgScheme # noqa +from .thrift import ThriftArgScheme # noqa + +DEFAULT_SCHEMES = ( + RawArgScheme, + JsonArgScheme, + ThriftArgScheme +) + +# TODO move constants to schemes/glossary and import here diff --git a/tchannel/schemes/json.py b/tchannel/schemes/json.py new file mode 100644 index 00000000..334370c6 --- /dev/null +++ b/tchannel/schemes/json.py @@ -0,0 +1,89 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import json + +from tornado import gen + +from . import JSON + + +class JsonArgScheme(object): + """Semantic params and serialization for json.""" + + NAME = JSON + + def __init__(self, tchannel): + self._tchannel = tchannel + + @gen.coroutine + def __call__(self, service, endpoint, body=None, headers=None, + timeout=None, retry_on=None, retry_limit=None, hostport=None): + """Make JSON TChannel Request. + + .. code-block: python + + from tchannel import TChannel + + tchannel = TChannel('my-service') + + resp = tchannel.json( + service='some-other-service', + endpoint='get-all-the-crackers', + body={ + 'some': 'dict', + }, + ) + + :param string service: + Name of the service to call. + :param string endpoint: + Endpoint to call on service. + :param string body: + A raw body to provide to the endpoint. + :param string headers: + A raw headers block to provide to the endpoint. + :param int timeout: + How long to wait before raising a ``TimeoutError`` - this + defaults to ``tchannel.glossary.DEFAULT_TIMEOUT``. + :param string retry_on: + What events to retry on - valid values can be found in + ``tchannel.retry``. + :param string retry_limit: + How many times to retry before + :param string hostport: + A 'host:port' value to use when making a request directly to a + TChannel service, bypassing Hyperbahn. + :return Response: + """ + + # TODO should we not default these? + if headers is None: + headers = {} + + # TODO dont default? + if body is None: + body = {} + + # serialize + headers = json.dumps(headers) + body = json.dumps(body) + + response = yield self._tchannel.call( + scheme=self.NAME, + service=service, + arg1=endpoint, + arg2=headers, + arg3=body, + timeout=timeout, + retry_on=retry_on, + retry_limit=retry_limit, + hostport=hostport, + ) + + # deserialize + response.headers = json.loads(response.headers) + response.body = json.loads(response.body) + + raise gen.Return(response) diff --git a/tchannel/schemes/raw.py b/tchannel/schemes/raw.py new file mode 100644 index 00000000..e3b5b560 --- /dev/null +++ b/tchannel/schemes/raw.py @@ -0,0 +1,63 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from . import RAW + + +class RawArgScheme(object): + """Semantic params and serialization for raw.""" + + NAME = RAW + + def __init__(self, tchannel): + self._tchannel = tchannel + + def __call__(self, service, endpoint, body=None, headers=None, + timeout=None, retry_on=None, retry_limit=None, hostport=None): + """Make Raw TChannel Request. + + .. code-block: python + + from tchannel import TChannel + + tchannel = TChannel('my-service') + + resp = tchannel.raw( + service='some-other-service', + endpoint='get-all-the-crackers', + ) + + :param string service: + Name of the service to call. + :param string endpoint: + Endpoint to call on service. + :param string body: + A raw body to provide to the endpoint. + :param string headers: + A raw headers block to provide to the endpoint. + :param int timeout: + How long to wait before raising a ``TimeoutError`` - this + defaults to ``tchannel.glossary.DEFAULT_TIMEOUT``. + :param string retry_on: + What events to retry on - valid values can be found in + ``tchannel.retry``. + :param string retry_limit: + How many times to retry before + :param string hostport: + A 'host:port' value to use when making a request directly to a + TChannel service, bypassing Hyperbahn. + :return Response: + """ + + return self._tchannel.call( + scheme=self.NAME, + service=service, + arg1=endpoint, + arg2=headers, + arg3=body, + timeout=timeout, + retry_on=retry_on, + retry_limit=retry_limit, + hostport=hostport, + ) diff --git a/tchannel/schemes/thrift.py b/tchannel/schemes/thrift.py new file mode 100644 index 00000000..f19433af --- /dev/null +++ b/tchannel/schemes/thrift.py @@ -0,0 +1,66 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from tornado import gen + +from . import THRIFT +from tchannel.thrift import serializer + + +class ThriftArgScheme(object): + + NAME = THRIFT + + def __init__(self, tchannel): + self._tchannel = tchannel + + @gen.coroutine + def __call__(self, request=None, headers=None, timeout=None, + retry_on=None, retry_limit=None): + + if headers is None: + headers = {} + + # serialize + headers = serializer.serialize_headers(headers=headers) + body = serializer.serialize_body(call_args=request.call_args) + + response = yield self._tchannel.call( + scheme=self.NAME, + service=request.service, + arg1=request.endpoint, + arg2=headers, + arg3=body, + timeout=timeout, + retry_on=retry_on, + retry_limit=retry_limit, + hostport=request.hostport + ) + + # deserialize... + + response.headers = serializer.deserialize_headers( + headers=response.headers + ) + body = serializer.deserialize_body( + body=response.body, + result_type=request.result_type + ) + result_spec = request.result_type.thrift_spec + + # raise application exception, if present + for exc_spec in result_spec[1:]: + exc = getattr(body, exc_spec[2]) + if exc is not None: + raise exc + + # success - non-void + if len(result_spec) >= 1 and result_spec[0] is not None: + response.body = body.success + raise gen.Return(response) + + # success - void + else: + response.body = None + raise gen.Return(response) diff --git a/tchannel/sync/thrift.py b/tchannel/sync/thrift.py index 53b8b7a9..e751d2aa 100644 --- a/tchannel/sync/thrift.py +++ b/tchannel/sync/thrift.py @@ -20,7 +20,7 @@ from __future__ import absolute_import -from tchannel.thrift.util import get_service_methods +from tchannel.thrift.reflection import get_service_methods from tchannel.thrift.client import client_for as async_client_for diff --git a/tchannel/tchannel.py b/tchannel/tchannel.py new file mode 100644 index 00000000..b07cca24 --- /dev/null +++ b/tchannel/tchannel.py @@ -0,0 +1,149 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from tornado import gen + +from . import schemes, transport, retry +from .glossary import DEFAULT_TIMEOUT +from .response import Response, ResponseTransportHeaders +from .tornado import TChannel as DeprecatedTChannel + +__all__ = ['TChannel'] + + +class TChannel(object): + """Make requests to TChannel services.""" + + def __init__(self, name, hostport=None, process_name=None, + known_peers=None, trace=False): + + # until we move everything here, + # lets compose the old tchannel + self._dep_tchannel = DeprecatedTChannel( + name=name, + hostport=hostport, + process_name=process_name, + known_peers=known_peers, + trace=trace + ) + + self.name = name + + # set arg schemes + self.raw = schemes.RawArgScheme(self) + self.json = schemes.JsonArgScheme(self) + self.thrift = schemes.ThriftArgScheme(self) + + @gen.coroutine + def call(self, scheme, service, arg1, arg2=None, arg3=None, + timeout=None, retry_on=None, retry_limit=None, hostport=None): + """Make low-level requests to TChannel services. + + This method uses TChannel's protocol terminology for param naming. + + For high level requests with automatic serialization and semantic + param names, use ``raw``, ``json``, and ``thrift`` methods instead. + + :param string scheme: + Name of the Arg Scheme to be sent as the Transport Header ``as``; + eg. 'raw', 'json', 'thrift' are all valid values. + :param string service: + Name of the service that is being called. This is used + internally to route requests through Hyperbahn, and for grouping + of connection, and labelling stats. Note that when hostport is + provided, requests are not routed through Hyperbahn. + :param string arg1: + Value for ``arg1`` as specified by the TChannel protocol - this + varies by Arg Scheme, but is typically used for endpoint name. + :param string arg2: + Value for ``arg2`` as specified by the TChannel protocol - this + varies by Arg Scheme, but is typically used for app-level headers. + :param string arg3: + Value for ``arg3`` as specified by the TChannel protocol - this + varies by Arg Scheme, but is typically used for the request body. + :param int timeout: + How long to wait before raising a ``TimeoutError`` - this + defaults to ``tchannel.glossary.DEFAULT_TIMEOUT``. + :param string retry_on: + What events to retry on - valid values can be found in + ``tchannel.retry``. + :param string retry_limit: + How many times to retry before + :param string hostport: + A 'host:port' value to use when making a request directly to a + TChannel service, bypassing Hyperbahn. + """ + + # TODO - dont use asserts for public API + assert format, "format is required" + assert service, "service is required" + assert arg1, "arg1 is required" + + # default args + if arg2 is None: + arg2 = "" + if arg3 is None: + arg3 = "" + if timeout is None: + timeout = DEFAULT_TIMEOUT + if retry_on is None: + retry_on = retry.DEFAULT + if retry_limit is None: + retry_limit = retry.DEFAULT_RETRY_LIMIT + + # TODO - allow filters/steps for serialization, tracing, etc... + + # calls tchannel.tornado.peer.PeerClientOperation.__init__ + operation = self._dep_tchannel.request( + service=service, + hostport=hostport, + arg_scheme=scheme, + retry=retry_on, + ) + + # fire operation + transport_headers = { + transport.SCHEME: scheme, + transport.CALLER_NAME: self.name, + } + response = yield operation.send( + arg1=arg1, + arg2=arg2, + arg3=arg3, + headers=transport_headers, + attempt_times=retry_limit, + ttl=timeout, + ) + + # unwrap response + header = yield response.get_header() + body = yield response.get_body() + t = transport.to_kwargs(response.headers) + t = ResponseTransportHeaders(**t) + + result = Response(header, body, t) + + raise gen.Return(result) + + def listen(self, port=None): + return self._dep_tchannel.listen(port) + + @property + def hostport(self): + return self._dep_tchannel.hostport + + def register(self, endpoint, scheme=None, handler=None, **kwargs): + return self._dep_tchannel.register( + endpoint=endpoint, + scheme=scheme, + handler=handler, + **kwargs + ) + + def advertise(self, routers, name=None, timeout=None): + return self._dep_tchannel.advertise( + routers=routers, + name=name, + timeout=timeout + ) diff --git a/tchannel/testing/data/__init__.py b/tchannel/testing/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/data/generated/ThriftTest/SecondService-remote b/tchannel/testing/data/generated/ThriftTest/SecondService-remote similarity index 100% rename from tests/data/generated/ThriftTest/SecondService-remote rename to tchannel/testing/data/generated/ThriftTest/SecondService-remote diff --git a/tests/data/generated/ThriftTest/SecondService.py b/tchannel/testing/data/generated/ThriftTest/SecondService.py similarity index 100% rename from tests/data/generated/ThriftTest/SecondService.py rename to tchannel/testing/data/generated/ThriftTest/SecondService.py diff --git a/tests/data/generated/ThriftTest/ThriftTest-remote b/tchannel/testing/data/generated/ThriftTest/ThriftTest-remote similarity index 100% rename from tests/data/generated/ThriftTest/ThriftTest-remote rename to tchannel/testing/data/generated/ThriftTest/ThriftTest-remote diff --git a/tests/data/generated/ThriftTest/ThriftTest.py b/tchannel/testing/data/generated/ThriftTest/ThriftTest.py similarity index 100% rename from tests/data/generated/ThriftTest/ThriftTest.py rename to tchannel/testing/data/generated/ThriftTest/ThriftTest.py diff --git a/tests/data/generated/ThriftTest/__init__.py b/tchannel/testing/data/generated/ThriftTest/__init__.py similarity index 100% rename from tests/data/generated/ThriftTest/__init__.py rename to tchannel/testing/data/generated/ThriftTest/__init__.py diff --git a/tests/data/generated/ThriftTest/constants.py b/tchannel/testing/data/generated/ThriftTest/constants.py similarity index 100% rename from tests/data/generated/ThriftTest/constants.py rename to tchannel/testing/data/generated/ThriftTest/constants.py diff --git a/tests/data/generated/ThriftTest/ttypes.py b/tchannel/testing/data/generated/ThriftTest/ttypes.py similarity index 100% rename from tests/data/generated/ThriftTest/ttypes.py rename to tchannel/testing/data/generated/ThriftTest/ttypes.py diff --git a/tchannel/testing/data/generated/__init__.py b/tchannel/testing/data/generated/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tchannel/testing/vcr/patch.py b/tchannel/testing/vcr/patch.py index 4ece9ce4..299732e8 100644 --- a/tchannel/testing/vcr/patch.py +++ b/tchannel/testing/vcr/patch.py @@ -26,12 +26,12 @@ import contextlib2 from tornado import gen +from tchannel import schemes from tchannel.errors import ProtocolError from tchannel.tornado import TChannel from tchannel.tornado.response import Response from tchannel.tornado.stream import maybe_stream from tchannel.tornado.stream import read_full -from tchannel.transport_header import ArgSchemeType from .proxy import VCRProxy @@ -63,7 +63,7 @@ def __init__( self.vcr_client = vcr_client self.hostport = hostport self.service = service or '' - self.arg_scheme = arg_scheme or ArgSchemeType.DEFAULT + self.arg_scheme = arg_scheme or schemes.DEFAULT # TODO what to do with retry, parent_tracing and score_threshold diff --git a/tchannel/thrift/__init__.py b/tchannel/thrift/__init__.py index bef78dac..186ea838 100644 --- a/tchannel/thrift/__init__.py +++ b/tchannel/thrift/__init__.py @@ -21,4 +21,5 @@ from __future__ import absolute_import from .client import client_for # noqa +from .module import from_thrift_module # noqa from .server import register # noqa diff --git a/tchannel/thrift/client.py b/tchannel/thrift/client.py index b0466d10..dfa9c2c4 100644 --- a/tchannel/thrift/client.py +++ b/tchannel/thrift/client.py @@ -26,10 +26,10 @@ from thrift import Thrift from tornado import gen +from tchannel.errors import OneWayNotSupportedError from tchannel.tornado.broker import ArgSchemeBroker - -from .scheme import ThriftArgScheme -from .util import get_service_methods +from tchannel.dep.thrift_arg_scheme import DeprecatedThriftArgScheme +from .reflection import get_service_methods # Generated clients will use this base class. _ClientBase = namedtuple( @@ -120,11 +120,18 @@ def generate_method(service_module, service_name, method_name): assert method_name args_type = getattr(service_module, method_name + '_args') - result_type = getattr(service_module, method_name + '_result') - # TODO result_type is None when the method is oneway. - # We don't support oneway yet. - - arg_scheme = ThriftArgScheme(result_type) + result_type = getattr(service_module, method_name + '_result', None) + + # oneway not currently supported + # TODO - write test for this + if result_type is None: + def not_supported(self, *args, **kwags): + raise OneWayNotSupportedError( + 'TChannel+Thrift does not currently support oneway procedues' + ) + return not_supported + + arg_scheme = DeprecatedThriftArgScheme(result_type) result_spec = result_type.thrift_spec # result_spec is a tuple of tuples in the form: # @@ -162,7 +169,7 @@ def send(self, *args, **kwargs): ), endpoint, {}, - call_args, + call_args, # body protocol_headers=self.protocol_headers, traceflag=self.trace ) diff --git a/tchannel/thrift/module.py b/tchannel/thrift/module.py new file mode 100644 index 00000000..59930af3 --- /dev/null +++ b/tchannel/thrift/module.py @@ -0,0 +1,188 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import inspect +import types + +from tchannel.errors import OneWayNotSupportedError +from .reflection import get_service_methods, get_module_name + + +def from_thrift_module(service, thrift_module, hostport=None, + thrift_class_name=None): + """Create a ThriftRequestMaker from a Thrift generated module. + + The service this creates is meant to be used with TChannel like so: + + .. code-block:: python + + from tchannel import TChannel, from_thrift_module + from some_other_service_thrift import some_other_service + + tchannel = TChannel('my-service') + + some_service = from_thrift_module( + service='some-other-service', + thrift_module=some_other_service + ) + + resp = tchannel.thrift( + some_service.fetchPotatoes() + ) + + :param string service: + Name of Thrift service to call. This is used internally for + grouping and stats, but also to route requests over Hyperbahn. + :param thrift_module: + The top-level module of the Apache Thrift generated code for + the service that will be called. + :param string hostport: + When calling the Thrift service directly, and not over Hyperbahn, + this 'host:port' value should be provided. + :param string thrift_class_name: + When the Apache Thrift generated Iface class name does not match + thrift_module, then this should be provided. + """ + + # start with a request maker instance + maker = ThriftRequestMaker( + service=service, + thrift_module=thrift_module, + hostport=hostport, + thrift_class_name=thrift_class_name + ) + + # create methods that mirror thrift client + # and each return ThriftRequest + methods = _create_methods(thrift_module) + + # then attach to instane + for name, method in methods.iteritems(): + method = types.MethodType(method, maker, ThriftRequestMaker) + setattr(maker, name, method) + + return maker + + +class ThriftRequestMaker(object): + + def __init__(self, service, thrift_module, + hostport=None, thrift_class_name=None): + + self.service = service + self.thrift_module = thrift_module + self.hostport = hostport + + if thrift_class_name is not None: + self.thrift_class_name = thrift_class_name + else: + self.thrift_class_name = get_module_name(self.thrift_module) + + def _make_request(self, method_name, args, kwargs): + + result_type = self._get_result_type(method_name) + + if result_type is None: + raise OneWayNotSupportedError( + 'TChannel+Thrift does not currently support oneway procedues' + ) + + endpoint = self._get_endpoint(method_name) + call_args = self._get_call_args(method_name, args, kwargs) + + request = ThriftRequest( + service=self.service, + endpoint=endpoint, + result_type=result_type, + call_args=call_args, + hostport=self.hostport + ) + + return request + + def _get_endpoint(self, method_name): + + endpoint = '%s::%s' % (self.thrift_class_name, method_name) + + return endpoint + + def _get_args_type(self, method_name): + + args_type = getattr(self.thrift_module, method_name + '_args') + + return args_type + + def _get_result_type(self, method_name): + + # if None then result_type is oneway + result_type = getattr( + self.thrift_module, method_name + '_result', None + ) + + return result_type + + def _get_call_args(self, method_name, args, kwargs): + + args_type = self._get_args_type(method_name) + + params = inspect.getcallargs( + getattr(self.thrift_module.Iface, method_name), + self, + *args, + **kwargs + ) + params.pop('self') # self is already known + + call_args = args_type() + for name, value in params.items(): + setattr(call_args, name, value) + + return call_args + + +class ThriftRequest(object): + + # TODO - add __slots__ + # TODO - implement __repr__ + + def __init__(self, service, endpoint, result_type, + call_args, hostport=None): + + self.service = service + self.endpoint = endpoint + self.result_type = result_type + self.call_args = call_args + self.hostport = hostport + + +def _create_methods(thrift_module): + + # TODO - this method isn't needed, instead, do: + # + # for name in get_service_methods(...): + # method = _create_method(...) + # # ... + # + + methods = {} + method_names = get_service_methods(thrift_module.Iface) + + for method_name in method_names: + + method = _create_method(method_name) + methods[method_name] = method + + return methods + + +def _create_method(method_name): + + # TODO - copy over entire signature using @functools.wraps(that_function) + # or wrapt on Iface. + + def method(self, *args, **kwargs): + # TODO switch to __make_request + return self._make_request(method_name, args, kwargs) + + return method diff --git a/tchannel/thrift/util.py b/tchannel/thrift/reflection.py similarity index 94% rename from tchannel/thrift/util.py rename to tchannel/thrift/reflection.py index cf947f87..b1dc1203 100644 --- a/tchannel/thrift/util.py +++ b/tchannel/thrift/reflection.py @@ -38,3 +38,10 @@ def get_service_methods(iface): return set( name for (name, method) in methods if not name.startswith('__') ) + + +def get_module_name(module): + + name = module.__name__.rsplit('.', 1)[-1] + + return name diff --git a/tchannel/thrift/serializer.py b/tchannel/thrift/serializer.py new file mode 100644 index 00000000..83e85e93 --- /dev/null +++ b/tchannel/thrift/serializer.py @@ -0,0 +1,60 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from thrift.protocol import TBinaryProtocol +from thrift.transport import TTransport + +from .. import io +from .. import rw + +_headers_rw = rw.headers( + rw.number(2), + rw.len_prefixed_string(rw.number(2)), + rw.len_prefixed_string(rw.number(2)), +) + + +def serialize_headers(headers): + + result = _headers_rw.write(headers, io.BytesIO()).getvalue() + + return result + + +def deserialize_headers(headers): + + headers = io.BytesIO(headers) + headers = _headers_rw.read(headers) + result = dict(headers) + + return result + + +def serialize_body(call_args): + + # TODO - use fastbinary directly + # + # fastbinary.encode_binary( + # call_args, (call_args.__class__, call_args.thrift_spec) + # ) + # fastbinary.decode_binary( + # result, TMemoryBuffer(body), (result_type, result_type.thrift_spec) + # ) + # + trans = TTransport.TMemoryBuffer() + proto = TBinaryProtocol.TBinaryProtocolAccelerated(trans) + call_args.write(proto) + result = trans.getvalue() + + return result + + +def deserialize_body(body, result_type): + + trans = TTransport.TMemoryBuffer(body) + proto = TBinaryProtocol.TBinaryProtocolAccelerated(trans) + + result = result_type() + result.read(proto) + return result diff --git a/tchannel/thrift/server.py b/tchannel/thrift/server.py index f7e0d55b..f6c090b8 100644 --- a/tchannel/thrift/server.py +++ b/tchannel/thrift/server.py @@ -25,12 +25,11 @@ from tornado import gen +from tchannel.dep.thrift_arg_scheme import DeprecatedThriftArgScheme from tchannel.tornado.broker import ArgSchemeBroker from tchannel.tornado.request import TransportMetadata from tchannel.tornado.response import StatusCode -from .scheme import ThriftArgScheme - def register(dispatcher, service_module, handler, method=None, service=None): """Registers a Thrift service method with the given RequestDispatcher. @@ -83,7 +82,7 @@ def hello(request, response, tchannel): endpoint = '%s::%s' % (service, method) args_type = getattr(service_module, method + '_args') result_type = getattr(service_module, method + '_result') - broker = ArgSchemeBroker(ThriftArgScheme(args_type)) + broker = ArgSchemeBroker(DeprecatedThriftArgScheme(args_type)) dispatcher.register( endpoint, diff --git a/tchannel/tornado/broker.py b/tchannel/tornado/broker.py index 4d0d49bb..3136c7d0 100644 --- a/tchannel/tornado/broker.py +++ b/tchannel/tornado/broker.py @@ -64,7 +64,11 @@ def handle_call(self, req, resp, proxy): return handler(req, resp, proxy) @tornado.gen.coroutine - def send(self, client, endpoint, header, body, + def send(self, + client, + endpoint, + header, + body, protocol_headers=None, traceflag=None, attempt_times=None, diff --git a/tchannel/tornado/peer.py b/tchannel/tornado/peer.py index ac28e208..e1037af1 100644 --- a/tchannel/tornado/peer.py +++ b/tchannel/tornado/peer.py @@ -18,7 +18,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from __future__ import absolute_import +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) import logging from collections import deque @@ -27,17 +29,16 @@ from tornado import gen +from ..schemes import DEFAULT as DEFAULT_SCHEME +from ..retry import ( + DEFAULT as DEFAULT_RETRY, DEFAULT_RETRY_LIMIT, DEFAULT_RETRY_DELAY +) from tchannel.event import EventType -from tchannel.glossary import DEFAULT_TTL -from tchannel.glossary import MAX_ATTEMPT_TIMES -from tchannel.glossary import RETRY_DELAY - +from tchannel.glossary import DEFAULT_TIMEOUT from ..errors import NoAvailablePeerError from ..errors import ProtocolError from ..errors import TimeoutError from ..handler import CallableRequestHandler -from ..transport_header import ArgSchemeType -from ..transport_header import RetryType from ..zipkin.annotation import Endpoint from ..zipkin.trace import Trace from .connection import StreamConnection @@ -451,9 +452,12 @@ def __init__(self, peer, service, arg_scheme=None, self.peer = peer self.service = service self.parent_tracing = parent_tracing + + # TODO the term headers are reserved for application headers, + # these are transport headers, self.headers = { - 'as': arg_scheme or ArgSchemeType.DEFAULT, - 're': retry or RetryType.DEFAULT, + 'as': arg_scheme or DEFAULT_SCHEME, + 're': retry or DEFAULT_RETRY, 'cn': self.peer.tchannel.name, } @@ -501,9 +505,9 @@ def send(self, arg1, arg2, arg3, maybe_stream(arg1), maybe_stream(arg2), maybe_stream(arg3) ) - attempt_times = attempt_times or MAX_ATTEMPT_TIMES - ttl = ttl or DEFAULT_TTL - retry_delay = retry_delay or RETRY_DELAY + attempt_times = attempt_times or DEFAULT_RETRY_LIMIT + ttl = ttl or DEFAULT_TIMEOUT + retry_delay = retry_delay or DEFAULT_RETRY_DELAY # hack to get endpoint from arg_1 for trace name arg1.close() endpoint = yield read_full(arg1) @@ -600,6 +604,7 @@ def _send(self, connection, req): @gen.coroutine def send_with_retry(self, request, peer, attempt_times, retry_delay): + # black list to record all used peers, so they aren't chosen again. blacklist = set() response = None diff --git a/tchannel/tornado/request.py b/tchannel/tornado/request.py index 23903208..59bbf321 100644 --- a/tchannel/tornado/request.py +++ b/tchannel/tornado/request.py @@ -25,11 +25,11 @@ import tornado import tornado.gen -from ..glossary import DEFAULT_TTL +from tchannel import retry +from ..glossary import DEFAULT_TIMEOUT from ..messages import ErrorCode from ..messages.common import FlagsType from ..messages.common import StreamState -from ..transport_header import RetryType from ..zipkin.trace import Trace from .stream import InMemStream from .util import get_arg @@ -47,7 +47,7 @@ def __init__( self, id=None, flags=FlagsType.none, - ttl=DEFAULT_TTL, + ttl=DEFAULT_TIMEOUT, tracing=None, service=None, headers=None, @@ -167,9 +167,9 @@ def should_retry_on_error(self, error): # not retry for streaming request return False - retry_flag = self.headers.get('re', RetryType.DEFAULT) + retry_flag = self.headers.get('re', retry.DEFAULT) - if retry_flag == RetryType.NEVER: + if retry_flag == retry.NEVER: return False if error.code in [ErrorCode.bad_request, ErrorCode.cancelled, @@ -178,11 +178,11 @@ def should_retry_on_error(self, error): elif error.code in [ErrorCode.busy, ErrorCode.declined]: return True elif error.code is ErrorCode.timeout: - return retry_flag is not RetryType.CONNECTION_ERROR + return retry_flag is not retry.CONNECTION_ERROR elif error.code in [ErrorCode.network_error, ErrorCode.fatal, ErrorCode.unexpected]: - return retry_flag is not RetryType.TIMEOUT + return retry_flag is not retry.TIMEOUT else: return False diff --git a/tchannel/transport.py b/tchannel/transport.py new file mode 100644 index 00000000..8a5110b4 --- /dev/null +++ b/tchannel/transport.py @@ -0,0 +1,41 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from . import schemes + +CALLER_NAME = "cn" +CLAIM_AT_START = "cas" +CLAIM_AT_FINISH = "caf" +FAILURE_DOMAIN = "fd" +RETRY_FLAGS = "re" +SCHEME = "as" +SHARD_KEY = "sk" +SPECULATIVE_EXE = "se" + + +def to_kwargs(data): + + args = {} + args['caller_name'] = data.get(CALLER_NAME) + args['claim_at_start'] = data.get(CLAIM_AT_START) + args['claim_at_finish'] = data.get(CLAIM_AT_FINISH) + args['failure_domain'] = data.get(FAILURE_DOMAIN) + args['retry_flags'] = data.get(RETRY_FLAGS) + args['scheme'] = data.get(SCHEME) + args['shard_key'] = data.get(SHARD_KEY) + args['speculative_exe'] = data.get(SPECULATIVE_EXE) + + return args + + +class TransportHeaders(object): + """Transport Headers common between Request & Response""" + + def __init__(self, scheme=None, failure_domain=None, **kwargs): + + if scheme is None: + scheme = schemes.DEFAULT + + self.scheme = scheme + self.failure_domain = failure_domain diff --git a/tchannel/transport_header.py b/tchannel/transport_header.py deleted file mode 100644 index 6f5b32be..00000000 --- a/tchannel/transport_header.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2015 Uber Technologies, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - - -class RetryType(object): - """ Retry Type in the protocol header - For details, look at protocol definition. - https://github.com/uber/tchannel/blob/master/docs/protocol.md - """ - NEVER = 'n' - CONNECTION_ERROR = 'c' - TIMEOUT = 't' - CONNECTION_ERROR_AND_TIMEOUT = 'ct' - DEFAULT = CONNECTION_ERROR - - -class ArgSchemeType(object): - RAW = 'raw' - JSON = 'json' - HTTP = 'http' - THRIFT = 'thrift' - DEFAULT = RAW diff --git a/tests/integration/test_retry.py b/tests/integration/test_retry.py index d8acdb43..daec8afe 100644 --- a/tests/integration/test_retry.py +++ b/tests/integration/test_retry.py @@ -25,15 +25,14 @@ import tornado.gen from mock import patch +from tchannel import retry from tchannel.errors import ProtocolError from tchannel.errors import TChannelError from tchannel.errors import TimeoutError -from tchannel.glossary import MAX_ATTEMPT_TIMES from tchannel.messages import ErrorCode from tchannel.tornado import Request from tchannel.tornado import TChannel from tchannel.tornado.stream import InMemStream -from tchannel.transport_header import RetryType @tornado.gen.coroutine @@ -86,7 +85,7 @@ def test_retry_timeout(): "test", "test", headers={ - 're': RetryType.CONNECTION_ERROR_AND_TIMEOUT + 're': retry.CONNECTION_ERROR_AND_TIMEOUT }, ttl=0.005, attempt_times=3, @@ -113,7 +112,7 @@ def test_retry_on_error_fail(): "test", "test", headers={ - 're': RetryType.CONNECTION_ERROR_AND_TIMEOUT + 're': retry.CONNECTION_ERROR_AND_TIMEOUT }, ttl=0.02, attempt_times=3, @@ -122,7 +121,7 @@ def test_retry_on_error_fail(): assert mock_should_retry_on_error.called assert mock_should_retry_on_error.call_count == ( - MAX_ATTEMPT_TIMES) + retry.DEFAULT_RETRY_LIMIT) assert e.value.code == ErrorCode.busy @@ -150,7 +149,7 @@ def test_retry_on_error_success(): "test", "test", headers={ - 're': RetryType.CONNECTION_ERROR_AND_TIMEOUT, + 're': retry.CONNECTION_ERROR_AND_TIMEOUT, }, ttl=0.01, attempt_times=3, @@ -165,20 +164,20 @@ def test_retry_on_error_success(): @pytest.mark.gen_test @pytest.mark.parametrize('retry_flag, error_code, result', [ - (RetryType.CONNECTION_ERROR, ErrorCode.busy, True), - (RetryType.CONNECTION_ERROR, ErrorCode.declined, True), - (RetryType.CONNECTION_ERROR, ErrorCode.timeout, False), - (RetryType.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.timeout, True), - (RetryType.TIMEOUT, ErrorCode.unexpected, False), - (RetryType.TIMEOUT, ErrorCode.network_error, False), - (RetryType.CONNECTION_ERROR, ErrorCode.network_error, True), - (RetryType.NEVER, ErrorCode.network_error, False), - (RetryType.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.cancelled, False), - (RetryType.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.bad_request, False), - (RetryType.CONNECTION_ERROR, ErrorCode.fatal, True), - (RetryType.TIMEOUT, ErrorCode.fatal, False), - (RetryType.TIMEOUT, ErrorCode.unhealthy, False), - (RetryType.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.unhealthy, False), + (retry.CONNECTION_ERROR, ErrorCode.busy, True), + (retry.CONNECTION_ERROR, ErrorCode.declined, True), + (retry.CONNECTION_ERROR, ErrorCode.timeout, False), + (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.timeout, True), + (retry.TIMEOUT, ErrorCode.unexpected, False), + (retry.TIMEOUT, ErrorCode.network_error, False), + (retry.CONNECTION_ERROR, ErrorCode.network_error, True), + (retry.NEVER, ErrorCode.network_error, False), + (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.cancelled, False), + (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.bad_request, False), + (retry.CONNECTION_ERROR, ErrorCode.fatal, True), + (retry.TIMEOUT, ErrorCode.fatal, False), + (retry.TIMEOUT, ErrorCode.unhealthy, False), + (retry.CONNECTION_ERROR_AND_TIMEOUT, ErrorCode.unhealthy, False), ], ids=lambda arg: str(arg) ) diff --git a/tests/schemes/test_json.py b/tests/schemes/test_json.py new file mode 100644 index 00000000..c9c020b8 --- /dev/null +++ b/tests/schemes/test_json.py @@ -0,0 +1,59 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest +import tornado + +from tchannel import TChannel, schemes +from tchannel import response + + +@pytest.mark.gen_test +@pytest.mark.call +def test_call_should_get_response(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register('endpoint', schemes.JSON) + @tornado.gen.coroutine + def endpoint(request, response, proxy): + + headers = yield request.get_header() + body = yield request.get_body() + + assert headers == {'req': 'headers'} + assert body == {'req': 'body'} + + response.write_header({ + 'resp': 'headers' + }) + response.write_body({ + 'resp': 'body' + }) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + resp = yield tchannel.json( + service='server', + endpoint='endpoint', + headers={'req': 'headers'}, + body={'req': 'body'}, + hostport=server.hostport, + ) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == {'resp': 'headers'} + assert resp.body == {'resp': 'body'} + + # verify response transport headers + assert isinstance(resp.transport, response.ResponseTransportHeaders) + assert resp.transport.scheme == schemes.JSON + assert resp.transport.failure_domain is None diff --git a/tests/schemes/test_raw.py b/tests/schemes/test_raw.py new file mode 100644 index 00000000..a4524516 --- /dev/null +++ b/tests/schemes/test_raw.py @@ -0,0 +1,55 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest +import tornado + +from tchannel import TChannel, schemes +from tchannel import response + + +@pytest.mark.gen_test +@pytest.mark.call +def test_call_should_get_response(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register('endpoint', schemes.RAW) + @tornado.gen.coroutine + def endpoint(request, response, proxy): + + headers = yield request.get_header() + body = yield request.get_body() + + assert headers == 'raw req headers' + assert body == 'raw req body' + + response.write_header('raw resp headers') + response.write_body('raw resp body') + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + resp = yield tchannel.raw( + service='server', + endpoint='endpoint', + headers='raw req headers', + body='raw req body', + hostport=server.hostport, + ) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == 'raw resp headers' + assert resp.body == 'raw resp body' + + # verify response transport headers + assert isinstance(resp.transport, response.ResponseTransportHeaders) + assert resp.transport.scheme == schemes.RAW + assert resp.transport.failure_domain is None diff --git a/tests/schemes/test_thrift.py b/tests/schemes/test_thrift.py new file mode 100644 index 00000000..6d8c7ec1 --- /dev/null +++ b/tests/schemes/test_thrift.py @@ -0,0 +1,1113 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest +from tornado import gen + +from tchannel import ( + TChannel, from_thrift_module, + schemes, response +) +from tchannel.errors import OneWayNotSupportedError +from tchannel.thrift import client_for +from tchannel.testing.data.generated.ThriftTest import SecondService +from tchannel.testing.data.generated.ThriftTest import ThriftTest +from tchannel.errors import ProtocolError + + +# TODO - where possible, in req/res style test, create parameterized tests, +# each test should test w headers and wout +# and potentially w retry and timeout as well. +# note this wont work with complex scenarios + +@pytest.mark.gen_test +@pytest.mark.call +def test_void(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testVoid(request, response, proxy): + pass + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift(service.testVoid()) + + assert resp.headers == {} + assert resp.body is None + + +@pytest.mark.gen_test +@pytest.mark.call +def test_void_with_headers(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testVoid(request, response, proxy): + response.write_header('resp', 'header') + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift(service.testVoid()) + + assert resp.headers == { + 'resp': 'header' + } + assert resp.body is None + + +@pytest.mark.gen_test +@pytest.mark.call +def test_string(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testString(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testString('howdy') + ) + + assert resp.headers == {} + assert resp.body == 'howdy' + + +@pytest.mark.gen_test +@pytest.mark.call +def test_byte(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testByte(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testByte(63) + ) + + assert resp.headers == {} + assert resp.body == 63 + + +@pytest.mark.gen_test +@pytest.mark.call +def test_i32(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testI32(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + # case #1 + resp = yield tchannel.thrift( + service.testI32(-1) + ) + assert resp.headers == {} + assert resp.body == -1 + + # case #2 + resp = yield tchannel.thrift( + service.testI32(1) + ) + assert resp.headers == {} + assert resp.body == 1 + + +@pytest.mark.gen_test +@pytest.mark.call +def test_i64(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testI64(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testI64(-34359738368) + ) + + assert resp.headers == {} + assert resp.body == -34359738368 + + +@pytest.mark.gen_test +@pytest.mark.call +def test_double(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testDouble(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testDouble(-5.235098235) + ) + + assert resp.headers == {} + assert resp.body == -5.235098235 + + +@pytest.mark.gen_test +@pytest.mark.call +def test_binary(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testBinary(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testBinary( + # this is ThriftTest.Xtruct(string_thing='hi') + '\x0c\x00\x00\x0b\x00\x01\x00\x00\x00\x0bhi\x00\x00' + ) + ) + + assert resp.headers == {} + assert ( + resp.body == + '\x0c\x00\x00\x0b\x00\x01\x00\x00\x00\x0bhi\x00\x00' + ) + + +@pytest.mark.gen_test +@pytest.mark.call +def test_struct(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testStruct(request, response, proxy): + + assert request.args.thing.string_thing == 'req string' + + return ThriftTest.Xtruct( + string_thing="resp string" + ) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='service', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + resp = yield tchannel.thrift( + service.testStruct(ThriftTest.Xtruct("req string")) + ) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == ThriftTest.Xtruct("resp string") + + +@pytest.mark.gen_test +@pytest.mark.call +def test_struct_with_headers(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testStruct(request, response, proxy): + + # TODO server getting headers in non-friendly format, + # create a top-level request that has friendly headers :) + # assert request.headers == {'req': 'headers'} + assert request.headers == [['req', 'header']] + assert request.args.thing.string_thing == 'req string' + + # TODO should this response object be shared w client case? + # TODO are we ok with the approach here? it's diff than client... + # response.write_header({ + # 'resp': 'header' + # }) + response.write_header('resp', 'header') + + return ThriftTest.Xtruct( + string_thing="resp string" + ) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='service', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + resp = yield tchannel.thrift( + service.testStruct(ThriftTest.Xtruct("req string")), + headers={'req': 'header'}, + ) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == {'resp': 'header'} + assert resp.body == ThriftTest.Xtruct("resp string") + + +@pytest.mark.gen_test +@pytest.mark.call +def test_nest(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testNest(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + xstruct = ThriftTest.Xtruct( + string_thing='hi', + byte_thing=1, + i32_thing=-1, + i64_thing=-34359738368, + ) + xstruct2 = ThriftTest.Xtruct2( + byte_thing=1, + struct_thing=xstruct, + i32_thing=1, + ) + + resp = yield tchannel.thrift( + service.testNest(thing=xstruct2) + ) + + assert resp.headers == {} + assert resp.body == xstruct2 + + +@pytest.mark.gen_test +@pytest.mark.call +def test_map(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testMap(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = { + 0: 1, + 1: 2, + 2: 3, + 3: 4, + -1: -2, + } + + resp = yield tchannel.thrift( + service.testMap(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_string_map(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testStringMap(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = { + 'hello': 'there', + 'my': 'name', + 'is': 'shirly', + } + + resp = yield tchannel.thrift( + service.testStringMap(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_set(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testSet(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = set([8, 1, 42]) + + resp = yield tchannel.thrift( + service.testSet(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_list(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testList(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = [1, 4, 9, -42] + + resp = yield tchannel.thrift( + service.testList(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_enum(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testEnum(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = ThriftTest.Numberz.FIVE + + resp = yield tchannel.thrift( + service.testEnum(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_type_def(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testTypedef(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + x = 0xffffffffffffff # 7 bytes of 0xff + + resp = yield tchannel.thrift( + service.testTypedef(thing=x) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_map_map(): + + # Given this test server: + + server = TChannel(name='server') + + map_map = { + -4: { + -4: -4, + -3: -3, + -2: -2, + -1: -1, + }, + 4: { + 1: 1, + 2: 2, + 3: 3, + 4: 4, + }, + } + + @server.register(ThriftTest) + def testMapMap(request, response, proxy): + return map_map + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift( + service.testMapMap(1) + ) + + assert resp.headers == {} + assert resp.body == map_map + + +@pytest.mark.gen_test +@pytest.mark.call +def test_insanity(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testInsanity(request, response, proxy): + result = { + 1: { + 2: request.args.argument, + 3: request.args.argument, + }, + 2: { + 6: ThriftTest.Insanity(), + }, + } + return result + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + x = ThriftTest.Insanity( + userMap={ + ThriftTest.Numberz.EIGHT: 0xffffffffffffff, + }, + xtructs=[ + ThriftTest.Xtruct( + string_thing='Hello2', + byte_thing=74, + i32_thing=0xff00ff, + i64_thing=-34359738368, + ), + ], + ) + + resp = yield tchannel.thrift( + service.testInsanity(x) + ) + + assert resp.headers == {} + assert resp.body == { + 1: { + 2: x, + 3: x, + }, + 2: { + 6: ThriftTest.Insanity(), + }, + } + + +@pytest.mark.gen_test +@pytest.mark.call +def test_multi(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testMulti(request, response, proxy): + return ThriftTest.Xtruct( + string_thing='Hello2', + byte_thing=request.args.arg0, + i32_thing=request.args.arg1, + i64_thing=request.args.arg2, + ) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + x = ThriftTest.Xtruct( + string_thing='Hello2', + byte_thing=74, + i32_thing=0xff00ff, + i64_thing=0xffffffffd0d0, + ) + + resp = yield tchannel.thrift( + service.testMulti( + arg0=x.byte_thing, + arg1=x.i32_thing, + arg2=x.i64_thing, + arg3={0: 'abc'}, + arg4=ThriftTest.Numberz.FIVE, + arg5=0xf0f0f0, + ) + ) + + assert resp.headers == {} + assert resp.body == x + + +@pytest.mark.gen_test +@pytest.mark.call +def test_exception(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testException(request, response, proxy): + + if request.args.arg == 'Xception': + raise ThriftTest.Xception( + errorCode=1001, + message=request.args.arg + ) + elif request.args.arg == 'TException': + # TODO - what to raise here? We dont want dep on Thrift + # so we don't have thrift.TException available to us... + raise Exception() + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='service', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + # case #1 + with pytest.raises(ThriftTest.Xception) as e: + yield tchannel.thrift( + service.testException(arg='Xception') + ) + assert e.value.errorCode == 1001 + assert e.value.message == 'Xception' + + # case #2 + with pytest.raises(ProtocolError): + yield tchannel.thrift( + service.testException(arg='TException') + ) + + # case #3 + resp = yield tchannel.thrift( + service.testException(arg='something else') + ) + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body is None + + +@pytest.mark.gen_test +@pytest.mark.call +def test_multi_exception(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testMultiException(request, response, proxy): + + if request.args.arg0 == 'Xception': + raise ThriftTest.Xception( + errorCode=1001, + message='This is an Xception', + ) + elif request.args.arg0 == 'Xception2': + raise ThriftTest.Xception2( + errorCode=2002 + ) + + return ThriftTest.Xtruct(string_thing=request.args.arg1) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='service', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + # case #1 + with pytest.raises(ThriftTest.Xception) as e: + yield tchannel.thrift( + service.testMultiException(arg0='Xception', arg1='thingy') + ) + assert e.value.errorCode == 1001 + assert e.value.message == 'This is an Xception' + + # case #2 + with pytest.raises(ThriftTest.Xception2) as e: + yield tchannel.thrift( + service.testMultiException(arg0='Xception2', arg1='thingy') + ) + assert e.value.errorCode == 2002 + + # case #3 + resp = yield tchannel.thrift( + service.testMultiException(arg0='something else', arg1='thingy') + ) + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == ThriftTest.Xtruct('thingy') + + +@pytest.mark.gen_test +@pytest.mark.call +def test_oneway(): + + # Given this test server: + + server = TChannel(name='server') + + # TODO - server should raise same exception as client + with pytest.raises(AssertionError): + @server.register(ThriftTest) + def testOneway(request, response, proxy): + pass + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + with pytest.raises(OneWayNotSupportedError): + yield tchannel.thrift(service.testOneway(1)) + + +@pytest.mark.gen_test +@pytest.mark.call +def test_second_service_blah_blah(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testString(request, response, proxy): + return request.args.thing + + @server.register(SecondService) + def blahBlah(request, response, proxy): + pass + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + second_service = from_thrift_module( + service='server', + thrift_module=SecondService, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift(service.testString('thing')) + + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == 'thing' + + resp = yield tchannel.thrift(second_service.blahBlah()) + + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body is None + + +@pytest.mark.gen_test +@pytest.mark.call +def test_second_service_second_test_string(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testString(request, response, proxy): + return request.args.thing + + @server.register(SecondService) + @gen.coroutine + def secondtestString(request, response, proxy): + + # TODO - is this really how our server thrift story looks? + ThriftTestService = client_for( + service='server', + service_module=ThriftTest + ) + service = ThriftTestService( + tchannel=proxy, + hostport=server.hostport, + ) + + resp = yield service.testString(request.args.thing) + + response.write_result(resp) + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport + ) + + second_service = from_thrift_module( + service='server', + thrift_module=SecondService, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift(service.testString('thing')) + + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == 'thing' + + resp = yield tchannel.thrift( + second_service.secondtestString('second_string') + ) + + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == 'second_string' + + +@pytest.mark.gen_test +@pytest.mark.call +def test_call_response_should_contain_transport_headers(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testString(request, response, proxy): + return request.args.thing + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + resp = yield tchannel.thrift(service.testString('hi')) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == {} + assert resp.body == 'hi' + + # verify response transport headers + assert isinstance(resp.transport, response.ResponseTransportHeaders) + assert resp.transport.scheme == schemes.THRIFT + assert resp.transport.failure_domain is None + + +@pytest.mark.gen_test +@pytest.mark.call +def test_call_unexpected_error_should_result_in_protocol_error(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register(ThriftTest) + def testMultiException(request, response, proxy): + raise Exception('well, this is unfortunate') + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + service = from_thrift_module( + service='server', + thrift_module=ThriftTest, + hostport=server.hostport, + ) + + with pytest.raises(ProtocolError): + yield tchannel.thrift( + service.testMultiException(arg0='Xception', arg1='thingy') + ) diff --git a/tests/sync/__init__.py b/tests/sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_examples.py b/tests/test_examples.py index cba88b56..66313419 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -19,6 +19,7 @@ # THE SOFTWARE. import contextlib +import json import os import subprocess @@ -37,8 +38,11 @@ def popen(path, wait_for_listen=False): if wait_for_listen: # It would be more correct to check ``conn.status == # psutil.CONN_LISTEN`` but this works - while process.is_running() and not process.connections(): - pass + try: + while process.is_running() and not process.connections(): + pass + except psutil.Error: + raise AssertionError(process.stderr.read()) try: yield process @@ -62,30 +66,64 @@ def examples_dir(): @pytest.mark.parametrize( - 'example_type', - [ - 'raw_', - 'json_', - 'thrift_examples/', - 'keyvalue/keyvalue/', - 'stream_', - ] + 'scheme, path', + ( + ('raw', 'simple/raw/'), + ('json', 'simple/json/'), + ('thrift', 'simple/thrift/'), + ('guide', 'guide/keyvalue/keyvalue/'), + ) ) -def test_example(examples_dir, example_type): +def test_example(examples_dir, scheme, path): """Smoke test example code to ensure it still runs.""" server_path = os.path.join( examples_dir, - example_type + 'server.py', + path + 'server.py', ) client_path = os.path.join( examples_dir, - example_type + 'client.py', + path + 'client.py', ) with popen(server_path, wait_for_listen=True): with popen(client_path) as client: - assert ( - client.stdout.read() == 'Hello, world!\n' - ), client.stderr.read() + + out = client.stdout.read() + + # TODO the guide test should be the same as others + if scheme == 'guide': + assert out == 'Hello, world!\n' + return + + try: + body, headers = out.split(os.linesep)[:-1] + except ValueError: + raise AssertionError(out + client.stderr.read()) + + if scheme == 'raw': + + assert body == 'resp body' + assert headers == 'resp headers' + + elif scheme == 'json': + + body = json.loads(body) + headers = json.loads(headers) + + assert body == { + 'resp': 'body' + } + assert headers == { + 'resp': 'header' + } + + elif scheme == 'thrift': + + headers = json.loads(headers) + + assert body == 'resp' + assert headers == { + 'resp': 'header', + } diff --git a/tests/test_tchannel.py b/tests/test_tchannel.py new file mode 100644 index 00000000..dfe0c799 --- /dev/null +++ b/tests/test_tchannel.py @@ -0,0 +1,69 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest +import tornado + +from tchannel import TChannel, schemes +from tchannel import response + +# TODO - need integration tests for timeout and retries, use testing.vcr + + +@pytest.mark.call +def test_should_have_default_schemes(): + + tchannel = TChannel(name='test') + + for f in schemes.DEFAULT_SCHEMES: + scheme = getattr(tchannel, f.NAME) + assert scheme, "default scheme not found" + assert isinstance(scheme, f) + + +@pytest.mark.gen_test +@pytest.mark.call +def test_call_should_get_response(): + + # Given this test server: + + server = TChannel(name='server') + + @server.register('endpoint', schemes.RAW) + @tornado.gen.coroutine + def endpoint(request, response, proxy): + + headers = yield request.get_header() + body = yield request.get_body() + + assert headers == 'raw req headers' + assert body == 'raw req body' + + response.write_header('raw resp headers') + response.write_body('raw resp body') + + server.listen() + + # Make a call: + + tchannel = TChannel(name='client') + + resp = yield tchannel.call( + scheme=schemes.RAW, + service='server', + arg1='endpoint', + arg2='raw req headers', + arg3='raw req body', + hostport=server.hostport, + ) + + # verify response + assert isinstance(resp, response.Response) + assert resp.headers == 'raw resp headers' + assert resp.body == 'raw resp body' + + # verify response transport headers + assert isinstance(resp.transport, response.ResponseTransportHeaders) + assert resp.transport.scheme == schemes.RAW + assert resp.transport.failure_domain is None diff --git a/tests/thrift/test_module.py b/tests/thrift/test_module.py new file mode 100644 index 00000000..49675e92 --- /dev/null +++ b/tests/thrift/test_module.py @@ -0,0 +1,54 @@ +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import inspect + +import pytest + +from tchannel import from_thrift_module +from tchannel.thrift.module import ThriftRequestMaker, ThriftRequest +from tchannel.testing.data.generated.ThriftTest import ThriftTest + + +@pytest.mark.call +def test_from_thrift_class_should_return_request_maker(): + + maker = from_thrift_module('thrift_test', ThriftTest) + + assert isinstance(maker, ThriftRequestMaker) + + +@pytest.mark.call +def test_maker_should_have_thrift_iface_methods(): + + maker = from_thrift_module('thrift_test', ThriftTest) + + # extract list of maker methods + maker_methods = [ + m[0] for m in + inspect.getmembers(maker, predicate=inspect.ismethod) + ] + + # extract list of iface methods + iface_methods = [ + m[0] for m in + inspect.getmembers(ThriftTest.Iface, predicate=inspect.ismethod) + ] + + # verify all of iface_methods exist in maker_methods + assert set(iface_methods) < set(maker_methods) + + +@pytest.mark.call +def test_request_maker_should_return_request(): + + maker = from_thrift_module('thrift_test', ThriftTest) + + request = maker.testString('hi') + + assert isinstance(request, ThriftRequest) + assert request.service == 'thrift_test' + assert request.endpoint == 'ThriftTest::testString' + assert request.result_type == ThriftTest.testString_result + assert request.call_args == ThriftTest.testString_args(thing='hi') diff --git a/tests/thrift/test_util.py b/tests/thrift/test_reflection.py similarity index 95% rename from tests/thrift/test_util.py rename to tests/thrift/test_reflection.py index fa8ae99e..9193d194 100644 --- a/tests/thrift/test_util.py +++ b/tests/thrift/test_reflection.py @@ -20,7 +20,7 @@ from __future__ import absolute_import -from tchannel.thrift.util import get_service_methods +from tchannel.thrift.reflection import get_service_methods def test_get_service_methods(): diff --git a/tests/thrift/test_server.py b/tests/thrift/test_server.py index 068104fa..8e566721 100644 --- a/tests/thrift/test_server.py +++ b/tests/thrift/test_server.py @@ -24,7 +24,7 @@ import pytest from thrift.Thrift import TType -from tchannel.thrift.scheme import ThriftArgScheme +from tchannel.dep.thrift_arg_scheme import DeprecatedThriftArgScheme from tchannel.thrift.server import build_handler from tchannel.tornado.request import Request from tchannel.tornado.response import Response @@ -108,7 +108,7 @@ def call(treq, tres, tchan): InMemStream('\00\00'), # no headers InMemStream('\00'), # empty struct ], - scheme=ThriftArgScheme(FakeResult), + scheme=DeprecatedThriftArgScheme(FakeResult), headers={'cn': 'test_caller', 'as': 'thrift'}, ) req.close_argstreams() @@ -119,7 +119,7 @@ def call(treq, tres, tchan): response_header, response_body, ], - scheme=ThriftArgScheme(FakeResult), + scheme=DeprecatedThriftArgScheme(FakeResult), ) tchannel = mock.Mock() @@ -161,7 +161,7 @@ def call(treq, tres, tchan): InMemStream('\00\00'), # no headers InMemStream('\00'), # empty struct ], - scheme=ThriftArgScheme(FakeResult), + scheme=DeprecatedThriftArgScheme(FakeResult), ) req.close_argstreams() @@ -171,7 +171,7 @@ def call(treq, tres, tchan): InMemStream(), response_body, ], - scheme=ThriftArgScheme(FakeResult), + scheme=DeprecatedThriftArgScheme(FakeResult), ) tchannel = mock.Mock()