Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python - Using nonces with multithreading

I am using python 2 with requests. This question is more of a curiosity of how I can improve this performance.

The issue now is that I must send a cryptographic signature in the header of the request to a HTTPS server. This signature includes a "nonce" which must be a timestamp, and ALWAYS must increase (on the server side).

Obviously this can wreak havoc on running multiple HTTP sessions on multiple threads. Requests ended up sent out not in order because they get interrupted between generating the headers and sending the HTTPS POST request.

The solution is to lock the thread from before creating the signature till the end of recieving HTTPS data. Ideally, I would like to release the LOCK after the HTTP request was SENT, and not have to wait for the data to be recieved. Is there any way I can release the lock, using requests, after just the HTTP headers are SENT? See code sample:

self.lock is a Threading.Lock. This instance of this class (self) is shared amongst multiple Threads.

def get_nonce(self):
    return int(1000*time.time())

def do_post_request(self, endpoint, parameters):
    with self.lock:
        url = self.base + endpoint
        urlpath = endpoint
        parameters['nonce'] = self.get_nonce()
        postdata = urllib.urlencode(parameters)
        message = urlpath + hashlib.sha256(str(parameters['nonce']) + postdata).digest()
        signature = hmac.new(base64.b64decode(self.secret_key), message, hashlib.sha512)
        headers = {
            'API-Key': self.api_key,
            'API-Sign': base64.b64encode(signature.digest())
        }
        data = urllib.urlencode(parameters)
        response = requests.post(url, data=data, headers=headers, verify=True).json()

    return response
like image 247
beiller Avatar asked Feb 03 '16 17:02

beiller


1 Answers

It sounds like the requests library doesn't have any support for sending asynchronously.

With the default Transport Adapter in place, Requests does not provide any kind of non-blocking IO. The Response.content property will block until the entire response has been downloaded. If you require more granularity, the streaming features of the library (see Streaming Requests) allow you to retrieve smaller quantities of the response at a time. However, these calls will still block.

If you are concerned about the use of blocking IO, there are lots of projects out there that combine Requests with one of Python’s asynchronicity frameworks. Two excellent examples are grequests and requests-futures.

I saw in a comment that you hesitate to add more dependencies, so the only suggestions I have are:

  • Add retry logic when your nonce is rejected. This seems like the most pythonic solution, and should work fine as long as the nonce isn't rejected very often.
  • Throttle the nonce generator. Hold the timestamp used for the previous nonce, and sleep if it hasn't been long enough when the next nonce is requested.
  • Batch the messages. If the protocol allows it, you may find that throughput actually goes up when you add a delay to wait for other messages and send them as a batch.
  • Change the server so the nonce values don't have to increase. If you control the server, making the messages independent of each other will give you a much more flexible protocol.
  • Use a session pool. I'm guessing that the nonce values only have to increase within a single session. If you create a thread pool and have each thread open its own session, you could still have reasonable throughput without the timing problems you currently have.

Obviously, you'd have to measure the performance results of making these changes.

Even if you do decide to add a dependency that lets you release the lock after sending the headers, you may still find that you occasionally have timing issues. The message packets with the headers could be delayed on their way to the server.

like image 181
Don Kirkby Avatar answered Nov 16 '22 19:11

Don Kirkby