thriftpy icon indicating copy to clipboard operation
thriftpy copied to clipboard

Tornado client does not handle out of order responses correctly

Open matthiasvegh opened this issue 9 years ago • 1 comments

Given a thrift file:

service TestService {
    i32 sleep (1: i32 ms)
}

thriftpy.tornado client:

#!/usr/bin/env python2

import thriftpy
import thriftpy.tornado
import tornado
import tornado.ioloop
import tornado.gen

thriftpy.install_import_hook()
import test_thrift

import functools

@tornado.gen.coroutine
def run(ioloop):
    client = yield thriftpy.tornado.make_client(
        test_thrift.TestService, '127.0.0.1', 9090, io_loop=ioloop)
    results = yield [client.sleep(50), client.sleep(100), client.sleep(10)]
    print results

def main():
    ioloop = tornado.ioloop.IOLoop.current()
    ioloop.run_sync(functools.partial(run, ioloop))

if __name__ == "__main__":
    main()

and a thrift server that reponds to sleep(n) by returning n after n ms has elapsed, the expected behavior is [50, 100, 10] in the order as called. Instead I get [10, 50, 100] which is the order of responses.

I have also tried using tornado's Future.add_done_callback, and tornado's WaitIterator, in the hopes that yielding lists in this manner aren't supposed to work, but both result in the same issue: the Future of the call is not matched to the response read.

Investigation into the code and some network-level debugging shows that the sequenceID of the thrift requests is not being incremented by thriftpy's tornado client. The first request should use 0, the second 1 and so on. When the response arrives, this sequenceID can be used to match the response to the future of the request. This omission is fine for synchronous clients, where only one request can be outstanding, in an asynchronous client, several requests may be posted at once. The problem would likely be even worse if unrelated functions (returning different types) are used in this manner.

I tried using a thriftpy.tornado based Server, but handler functions can not be invoked simultaneously, as the first response is waited for before reading the next request from the socket as shown here

Twisted thrift server that reorders requests: Build thrift file with thrift --gen py:twisted test.thrift

#!/usr/bin/env python2

import os
import sys

sys.path.insert(1, os.path.join(os.path.dirname(__file__), "gen-py.twisted"))

import test
import test.TestService
import twisted
import twisted.internet
import twisted.internet.task
import twisted.internet.defer
import twisted.internet.reactor
import twisted.python
import twisted.python.log

import thrift
import thrift.transport
import thrift.transport.TTwisted
import thrift.protocol
import thrift.protocol.TBinaryProtocol

import zope
import zope.interface

class TestHandler:
    zope.interface.implements(test.TestService.Iface)

    def __init__(self):
        self.reactor = twisted.internet.reactor

    def sleep(self, ms):
        print('got request: ' + str(ms))
        d = twisted.internet.defer.Deferred()
        twisted.internet.reactor.callLater(ms/1000.0, d.callback, ms)
        return d

def main():
    twisted.python.log.startLogging(sys.stdout)
    handler = TestHandler()
    processor = test.TestService.Processor(handler)
    factory = thrift.protocol.TBinaryProtocol.TBinaryProtocolFactory()

    server = twisted.internet.reactor.listenTCP(9090,
        thrift.transport.TTwisted.ThriftServerFactory(processor, factory))
    twisted.internet.reactor.run()

if __name__ == "__main__":
    main()

˙ Thanks for your help!

matthiasvegh avatar Aug 30 '16 19:08 matthiasvegh

It would be really nice to have a fix for this. Is there somebody out there who could help by at least outlining what needs to be done with this?

Ferenc- avatar Dec 07 '16 19:12 Ferenc-