8

Can a requests library session object be used across greenlets safely in a gevented program?

EDIT - ADDING MORE EXPLANATION:

When a greenlet yields because it has made a socket call to send the request to the server, can the same socket (inside the shared session object) be used safely by another greenlet to send its own request?

END EDIT

I attempted to test this with the code posted here - https://gist.github.com/donatello/0b399d0353cb29dc91b0 - however I got no errors or unexpected results. However, this does not validate thread safety.

In the test, I use a shared session object to make lots of requests and try to see if the object gets the requests mixed up - it is kind of naive, but I do not get any exceptions.

For convenience, I am re-pasting the code here:

client.py

import gevent
from gevent.monkey import patch_all
patch_all()

import requests
import json

s = requests.Session()

def make_request(s, d):
    r = s.post("http://127.0.0.1:5000", data=json.dumps({'value': d}))
    if r.content.strip() != str(d*2):
        print("Sent %s got %s" % (r.content, str(d*2)))
    if r.status_code != 200:
        print(r.status_code)
        print(r.content)

gevent.joinall([
    gevent.spawn(make_request, s, v)
    for v in range(300)
])

server.py

from gevent.wsgi import WSGIServer
from gevent.monkey import patch_all

patch_all()

from flask import Flask
from flask import request

import time
import json

app = Flask(__name__)

@app.route('/', methods=['POST', 'GET'])
def hello_world():
    d = json.loads(request.data)
    return str(d['value']*2)

if __name__ == '__main__':
    http_server = WSGIServer(('', 5000), app)
    http_server.serve_forever()

Exact library versions:

requirements.txt

Flask==0.10.1
Jinja2==2.7.2
MarkupSafe==0.23
Werkzeug==0.9.4
argparse==1.2.1
gevent==1.0.1
greenlet==0.4.2
gunicorn==18.0
itsdangerous==0.24
requests==2.3.0
wsgiref==0.1.2

Is there some other test that can check greenlet thread safety? The requests documentation is not too clear on this point.

donatello
  • 5,727
  • 6
  • 32
  • 56
  • Remember that greenlets don't run in separate threads. That code is running on the same thread you declared it (i.e. the main thread) – Luis Masuelli May 29 '14 at 14:05
  • I am going to edit the question - but what I mean is are the session objects greenlet-safe? When one greenlet has yielded while sending something over the socket, can another grab the same socket (inside the session object), and send its own stuff? – donatello May 29 '14 at 14:20
  • underlying sockets -if standard ones- have only one execution flow, AFAIK, and not simultaneos access is possible. At least in the standard C impl is so. – Luis Masuelli May 29 '14 at 14:39
  • 1
    the session object is not a wrapper on a socket or connection object, it is a wrapper on a pool of sockets/connections. This connection pool creates new connection for new greenlets if no connection is reusable. – user869210 May 19 '16 at 16:01

1 Answers1

8

The author of requests has also created a gevent-integration package: grequests. Use that instead.

It supports passing in a session with the session keyword:

import grequests

s = requests.Session()

requests = [grequests.post("http://127.0.0.1:5000", 
                           data=json.dumps({'value': d}), session=s)
            for d in range(300)]

responses = grequests.map(requests)
for r in responses:
    if r.content.strip() != str(d*2):
        print("Sent %s got %s" % (r.content, str(d*2)))
    if r.status_code != 200:
        print(r.status_code)
        print(r.content)

Note that while the session is shared, concurrent requests must use a separate connection (socket) each; HTTP 1.x can't multiplex multiple queries over the same connection.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • In my actual application, I will not be able to gather all the requests together and use the map call. The requests are being made in different greenlets with the shared session object. That's why I want to confirm the thread-safety. – donatello May 29 '14 at 14:23
  • @donatello: the map call uses it's own pool, you can set up your own pool and add requests to that to be executed. – Martijn Pieters May 29 '14 at 14:25
  • @donatello: use `grequests.send()` to send a request with your own pool, see [In what way is grequests asynchronous?](http://stackoverflow.com/a/16016635) for an example. – Martijn Pieters May 29 '14 at 14:26
  • Ok, I am going to try this out. Is there any reason why plain requests is not thread-safe (or is it)? – donatello May 29 '14 at 14:50
  • Session shared state (cookies, headers) are not thread-safe, really. But greenlets are not threads. – Martijn Pieters May 29 '14 at 14:52
  • 1
    Greenlets are only preempted when they yield; I/O is an automatic yield here. But the code handling shared state in the Session is not preempted, so safe from race conditions. – Martijn Pieters May 29 '14 at 14:54
  • In a session object, isn't the underlying socket shared? If it is, why is it that the code is thread-safe, as another greenlet could attempt to send a request via the same socket, when another greenlet is blocked on it? – donatello May 30 '14 at 08:26
  • 3
    No, the socket isn't 'shared'; connections are handled by a connection pool that is thread-safe (protected by `threading.RLock`). – Martijn Pieters May 30 '14 at 08:31
  • Ok, now I understand. Thank you. This discussion has been very useful for me. – donatello May 30 '14 at 08:36
  • Sorry to bring this up again, but how does this cause the connection to be reused? In my testing, I've noticed that the only way to ensure the session connection is re-used, is to run all the greenlets in a `gevent.pool.Pool` of size 1, which leads to little to no significant performance benefits. Is `grequests` doing the same with Pool? – smac89 Sep 10 '22 at 17:02
  • 1
    @smac89: *subsequent* requests reuse connections. Concurrent requests can't reuse connections because those are *currently in use*. That's a limitation of the HTTP protocol, not grequests. – Martijn Pieters Sep 11 '22 at 13:38