4

I am implementing a SOAP web service using tornado (and the third party tornadows module). One of the operations in my service needs to call another so I have the chain:

  1. External request in (via SOAPUI) to operation A
  2. Internal request (via requests module) in to operation B
  3. Internal response from operation B
  4. External response from operation A

Because it is all running in one service it is being blocked somewhere though. I'm not familiar with tornado's async functionality.

There is only one request handling method (post) because everything comes in on the single url and then the specific operation (method doing processing) is called based on the SOAPAction request header value. I have decorated my post method with @tornado.web.asynchronous and called self.finish() at the end but no dice.

Can tornado handle this scenario and if so how can I implement it?

EDIT (added code):

class SoapHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def post(self):
        """ Method post() to process of requests and responses SOAP messages """
        try:
            self._request = self._parseSoap(self.request.body)
            soapaction = self.request.headers['SOAPAction'].replace('"','')
            self.set_header('Content-Type','text/xml')
            for operations in dir(self):
                operation = getattr(self,operations)
                method = ''
                if callable(operation) and hasattr(operation,'_is_operation'):
                    num_methods = self._countOperations()
                    if hasattr(operation,'_operation') and soapaction.endswith(getattr(operation,'_operation')) and num_methods > 1:
                        method = getattr(operation,'_operation')
                        self._response = self._executeOperation(operation,method=method)
                        break
                    elif num_methods == 1:
                        self._response = self._executeOperation(operation,method='')
                        break
            soapmsg = self._response.getSoap().toprettyxml()
            self.write(soapmsg)
            self.finish()
        except Exception as detail:
            #traceback.print_exc(file=sys.stdout)
            wsdl_nameservice = self.request.uri.replace('/','').replace('?wsdl','').replace('?WSDL','')
            fault = soapfault('Error in web service : {fault}'.format(fault=detail), wsdl_nameservice)
            self.write(fault.getSoap().toxml())
            self.finish()

This is the post method from the request handler. It's from the web services module I'm using (so not my code) but I added the async decorator and self.finish(). All it basically does is call the correct operation (as dictated in the SOAPAction of the request).

class CountryService(soaphandler.SoapHandler):
    @webservice(_params=GetCurrencyRequest, _returns=GetCurrencyResponse)
    def get_currency(self, input):
        result = db_query(input.country, 'currency')
        get_currency_response = GetCurrencyResponse()
        get_currency_response.currency = result
        headers = None
        return headers, get_currency_response

    @webservice(_params=GetTempRequest, _returns=GetTempResponse)
    def get_temp(self, input):
        get_temp_response = GetTempResponse()
        curr = self.make_curr_request(input.country)
        get_temp_response.temp = curr
        headers = None
        return headers, get_temp_response

    def make_curr_request(self, country):

        soap_request = """<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' xmlns:coun='CountryService'>
   <soapenv:Header/>
   <soapenv:Body>
      <coun:GetCurrencyRequestget_currency>
         <country>{0}</country>
      </coun:GetCurrencyRequestget_currency>
   </soapenv:Body>
</soapenv:Envelope>""".format(country)

        headers = {'Content-Type': 'text/xml;charset=UTF-8', 'SOAPAction': '"http://localhost:8080/CountryService/get_currency"'}
        r = requests.post('http://localhost:8080/CountryService', data=soap_request, headers=headers)
        try:
            tree = etree.fromstring(r.content)
            currency = tree.xpath('//currency')
            message = currency[0].text
        except:
            message = "Failure"
        return message

These are two of the operations of the web service (get_currency & get_temp). So SOAPUI hits get_temp, which makes a SOAP request to get_currency (via make_curr_request and the requests module). Then the results should just chain back and be sent back to SOAPUI.

The actual operation of the service makes no sense (returning the currency when asked for the temperature) but i'm just trying to get the functionality working and these are the operations I have.

johnharris85
  • 17,264
  • 5
  • 48
  • 52
  • Have you tried using `@tornado.web.asynchronous`? http://www.tornadoweb.org/documentation/web.html?highlight=async#tornado.web.asynchronous Please post some code if that doesn't work, and I'll be glad to help you resolve your problem. – Steve Peak Jan 29 '13 at 21:26
  • @StevePeak thanks for your help. I have used it for the post method but not sure if I'm doing it correctly. Relevant code added now. – johnharris85 Jan 30 '13 at 09:47
  • I see a couple syntax issues in your request. What are the `exceptions` you are seeing from this service (ex. `NameError`)? – Steve Peak Jan 30 '13 at 15:36
  • @StevePeak I'm not getting any exceptions at all...where are the syntax errors? The service just hangs (I guess it's being blocked somewhere). – johnharris85 Jan 30 '13 at 15:44
  • In your function `make_curr_request` where does the object `requests` come from? – Steve Peak Jan 30 '13 at 15:48
  • requests is a popular third party module for making HTTP requests. This is only a snippet of code for brevity. I am not getting any exceptions though, and calling the operations invidually (get_currency for instance) works fine. It's just this example where one operation (get_temp) calls another (get_currency) that it is being blocked somewhere and I don't have a lot of experience with concurrency/async. – johnharris85 Jan 30 '13 at 15:54
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/23644/discussion-between-steve-peak-and-jharris) – Steve Peak Jan 30 '13 at 15:55

2 Answers2

7

I don't think that your soap module, or requests is asyncronous.

I believe adding the @asyncronous decorator is only half the battle. Right now you aren't making any async requests inside of your function (every request is blocking, which ties up the server until your method finishes)

You can switch this up by using tornados AsynHttpClient. This can be used pretty much as an exact replacement for requests. From the docoumentation example:

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        http.fetch("http://friendfeed-api.com/v2/feed/bret",
                   callback=self.on_response)

    def on_response(self, response):
        if response.error: raise tornado.web.HTTPError(500)
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched " + str(len(json["entries"])) + " entries "
                   "from the FriendFeed API")
        self.finish()

Their method is decorated with async AND they are making asyn http requests. This is where the flow gets a little strange. When you use the AsyncHttpClient it doesn't lock up the event loop (PLease I just started using tornado this week, take it easy if all of my terminology isn't correct yet). This allows the server to freely processs incoming requests. When your asynchttp request is finished the callback method will be executed, in this case on_response.

Here you can replace requests with the tornado asynchttp client realtively easily. For your soap service, though, things might be more complicated. You could make a local webserivce around your soap client and make async requests to it using the tornado asyn http client???

This will create some complex callback logic which can be fixed using the gen decorator

dm03514
  • 54,664
  • 18
  • 108
  • 145
  • Hi @dm03514, can you tell me how can we send request body with it, actually I am trying to call async soap api using suds but not getting any response from it, here is my question: http://stackoverflow.com/questions/39227719/asynchronous-soap-api-call-using-python, please look into this, may be you can help me – Vikram Singh Chandel Sep 01 '16 at 04:59
0

This issue was fixed since yesterday.

Pull request: https://github.com/rancavil/tornado-webservices/pull/23

Example: here a simple webservice that doesn't take arguments and returns the version. Notice you should:

  • Method declaration: decorate the method with @gen.coroutine
  • Returning results: use raise gen.Return(data)

Code:

from tornado import gen
from tornadows.soaphandler import SoapHandler
...

class Example(SoapHandler):
    @gen.coroutine
    @webservice(_params=None, _returns=Version)
    def Version(self):
        _version = Version()
        # async stuff here, let's suppose you ask other rest service or resource for the version details.
        # ...
        # returns the result.
        raise gen.Return(_version)

Cheers!

Jonatan Anauati
  • 759
  • 5
  • 5