Thank you, your response was very informative! It's given me a more intuitive understanding of how greenlets cooperatively yield to each other.
At this point, I think it's safe to say that the issue is either with gunicorn or my configuration of it, so I will follow up with the gunicorn project. Do you have any ideas why this might be happening? I would assume it's something to do with how python's ssl module is monkey-patched under gunicorn, but their GeventWorker appears to call monkey.patch_all() without hardly any tweaks.
Results from python console:
>>> test(is_secure=False)
All threads joined after 5 seconds.
>>> test(is_secure=True)
All threads joined after 5 seconds.
Results from a gunicorn server:
(code slightly refactored to fit within the django framework)
>>> test(is_secure=False)
All threads joined after 5 seconds.
>>> test(is_secure=True)
All threads joined after 30 seconds, and the first join takes 30 seconds to complete. Timeout param not respected.
Example code:
import gevent, gevent.monkey; gevent.monkey.patch_all()
import datetime
import requests
import threading
def target(name="", is_secure=False):
'''Make a http call that will take 30 seconds to complete'''
print(name + ": in target")
scheme = "https" if is_secure else "http"
url = scheme + "://localhost:8000/sleep/30" # response takes 30 seconds
print(name + ": getting " + url)
response = requests.get(url)
print(name + ": response: " + str(response))
print(name + ": done target")
def calc_thread_timeout(start_time, seconds=5):
'''Work around the additive nature of Thread.join() timeouts, to do an aggregate timeout'''
elapsed = (datetime.datetime.now() - start_time).total_seconds()
remaining = float(seconds) - elapsed
return 0.0 if remaining < 0.0 else remaining
def test(is_secure=False):
threads = []
threads.append(threading.Thread(target=target, kwargs={"name": "t1", "is_secure": is_secure}))
threads.append(threading.Thread(target=target, kwargs={"name": "t2", "is_secure": is_secure}))
threads.append(threading.Thread(target=target, kwargs={"name": "t3", "is_secure": is_secure}))
threads.append(threading.Thread(target=target, kwargs={"name": "t4", "is_secure": is_secure}))
print("STARTING THREADS")
for t in threads:
t.start()
print("JOINING THREADS")
join_start = datetime.datetime.now()
# it should take 5 seconds wall time for all joining to complete
for t in threads:
t.join(timeout=calc_thread_timeout(start_time=join_start, seconds=5))
join_end = datetime.datetime.now()
join_total_secs = (join_end - join_start).total_seconds()
print("joined all threads after " + str(join_total_secs) + " seconds")