Repository: eventlet/eventlet (Python)

Issue:
time.sleep(0) or greenthread.sleep(0) is docuemnted as the  canonical way of expressing a cooperative yield
In https://github.com/eventlet/eventlet/pull/932 asserts were converted in to a runtime error
while that may be correct in some cases the docs string of greenthread.sleep states
that calling sleep(0) is the canonical way of expressing a cooperative yield

https://github.com/eventlet/eventlet/blob/3450e6adb017f4cd47d1c4e06331c9fa26a986d1/eventlet/greenthread.py#L25-L35

nova and opentack in general has also codeifed this pattern in our contributor docs
https://docs.openstack.org/nova/latest/reference/threading.html#yielding-the-thread-in-long-running-tasks

as a result calling time.sleep(0) or greenthread.sleep(0) is quite common in OpenStack code.

as of PR https://github.com/eventlet/eventlet/pull/932 the previsly silent assertion is now a runtime error.

this manifests in trackbacks like this

https://paste.opendev.org/show/bGPgfURx1cZYOsgmtDyw/

as reported in https://bugs.launchpad.net/watcher/+bug/2086710

in that traceback we see oslo.db define a _thread_yeild function 

https://github.com/openstack/oslo.db/blob/21c484ffb26ed03cbb1ca9dd7fd27fb19a1d4b36/oslo_db/sqlalchemy/engines.py#L43-L52

which calls time.sleep(0)

this is registered as a callback handeler for the sqlalchemy checkin event

https://github.com/openstack/oslo.db/blob/21c484ffb26ed03cbb1ca9dd7fd27fb19a1d4b36/oslo_db/sqlalchemy/engines.py#L361

which is emmited by sqlalchemy _finalize_fairy method

https://github.com/sqlalchemy/sqlalchemy/blob/214088c42ad61794e315174c41ee92a3c408ae6c/lib/sqlalchemy/pool/base.py#L1034-L1035

I belive the correct behavior in evently would be to only raise a runtime error if and only if seconds is not 0 and the current thread is the main thread.



Discussion:
I will take this on in a bit.
by the way the full set of logs also have additional exceptions

https://paste.opendev.org/show/bPfXhgQUM7Nu2YZZilAq/

so i think this other assertion/exption can also be raised with sqlachmeys current usage parterns

https://github.com/eventlet/eventlet/pull/932/files#diff-959fa03cb091386d7ad8eb6740341037ea2c345b30681ddd29f2b9106f7621d5R139

i am unsure if that is correct or not in that case.
i might just be misreading that trackback by the way, im not sure if the excption raised form the trampoline funciton in that case is the same excption that form the time.sleep(0) execption beign reraied.
Thank you @SeanMooney 
Thanks, that resolves the first of the two exceptions that i am seeing


the other one is 

```
 [-] Exception during reset or similar: RuntimeError: do not call blocking functions from the mainloop                  
 Traceback (most recent call last):                                                                                     
   File "/opt/stack/data/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 986, in _finalize_fairy       
     fairy._reset(                                                                                                      
   File "/opt/stack/data/venv/lib/python3.12/site-packages/sqlalchemy/pool/base.py", line 1432, in _reset               
     pool._dialect.do_rollback(self)                                                                                    
   File "/opt/stack/data/venv/lib/python3.12/site-packages/sqlalchemy/engine/default.py", line 699, in do_rollback      
     dbapi_connection.rollback()                                                                                        
   File "/opt/stack/data/venv/lib/python3.12/site-packages/pymysql/connections.py", line 493, in rollback               
     self._read_ok_packet()                                                                                             
   File "/opt/stack/data/venv/lib/python3.12/site-packages/pymysql/connections.py", line 453, in _read_ok_packet        
     pkt = self._read_packet()                                                                                          
           ^^^^^^^^^^^^^^^^^^^                                                                                          
   File "/opt/stack/data/venv/lib/python3.12/site-packages/pymysql/connections.py", line 744, in _read_packet           
     packet_header = self._read_bytes(4)                                                                                
                     ^^^^^^^^^^^^^^^^^^^                                                                                
   File "/opt/stack/data/venv/lib/python3.12/site-packages/pymysql/connections.py", line 782, in _read_bytes            
     data = self._rfile.read(num_bytes)                                                                                 
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                 
   File "/usr/lib/python3.12/socket.py", line 707, in readinto                                                          
     return self._sock.recv_into(b)                                                                                     
            ^^^^^^^^^^^^^^^^^^^^^^^                                                                                     
   File "/home/ubuntu/repos/eventlet/eventlet/greenio/base.py", line 358, in recv_into                                  
     return self._recv_loop(self.fd.recv_into, 0, buffer, nbytes, flags)                                                
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                
   File "/home/ubuntu/repos/eventlet/eventlet/greenio/base.py", line 346, in _recv_loop                                 
     self._read_trampoline()                                                                                            
   File "/home/ubuntu/repos/eventlet/eventlet/greenio/base.py", line 314, in _read_trampoline                           
     self._trampoline(                                                                                                  
   File "/home/ubuntu/repos/eventlet/eventlet/greenio/base.py", line 206, in _trampoline                                
     return trampoline(fd, read=read, write=write, timeout=timeout,                                                     
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                     
   File "/home/ubuntu/repos/eventlet/eventlet/hubs/__init__.py", line 139, in trampoline                                
     raise RuntimeError('do not call blocking functions from the mainloop')                                             
 RuntimeError: do not call blocking functions from the mainloop                                                         
  
```


this ligitemtirly does look like its a blockign read on the main loop but im not sure if its
an sqlalchemy bug, or something in howe we are using it.

what i dont really understand is how is that runnign on the mainlop rahther then a greentread where it will be monkey patched.


the orginal execption i was seeign are nologner present with the tip of master so i can confirm #1015 resolves this bug.

Can you open a new GitHub issue for the final issue that remains?



Patch:
diff --git a/eventlet/greenthread.py b/eventlet/greenthread.py
index 8041def7f..d1be0058d 100644
--- a/eventlet/greenthread.py
+++ b/eventlet/greenthread.py
@@ -32,6 +32,13 @@ def sleep(seconds=0):
     hub = hubs.get_hub()
     current = getcurrent()
     if hub.greenlet is current:
+        if seconds <= 0:
+            # In this case, sleep(0) got called in the event loop threadlet.
+            # This isn't blocking, so it's not harmful. And it will not be
+            # possible to switch in this situation. So not much we can do other
+            # than just keep running. This does get triggered in real code,
+            # unfortunately.
+            return
         raise RuntimeError('do not call blocking functions from the mainloop')
     timer = hub.schedule_call_global(seconds, current.switch)
     try:


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 8ac71494e..2176bd3f3 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -29,8 +29,6 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - { py: 3.7, toxenv: py37-epolls, ignore-error: false, os: ubuntu-latest }
-          - { py: 3.7, toxenv: py37-asyncio, ignore-error: false, os: ubuntu-latest }
           - { py: 3.8, toxenv: py38-epolls, ignore-error: false, os: ubuntu-latest }
           - { py: 3.8, toxenv: py38-openssl, ignore-error: false, os: ubuntu-latest }
           - { py: 3.8, toxenv: py38-poll, ignore-error: false, os: ubuntu-latest }
diff --git a/tests/api_test.py b/tests/api_test.py
index d28526824..ac3b041c0 100644
--- a/tests/api_test.py
+++ b/tests/api_test.py
@@ -184,3 +184,7 @@ def test_timeouterror_deprecated():
     code = '''import eventlet; eventlet.Timeout(1).cancel(); print('pass')'''
     args = ['-Werror:eventlet.Timeout:DeprecationWarning', '-c', code]
     tests.run_python(path=None, args=args, expect_pass=True)
+
+
+def test_zero_second_sleep():
+    tests.run_isolated("zero_second_sleep.py")
diff --git a/tests/isolated/zero_second_sleep.py b/tests/isolated/zero_second_sleep.py
new file mode 100644
index 000000000..dc8d72a83
--- /dev/null
+++ b/tests/isolated/zero_second_sleep.py
@@ -0,0 +1,31 @@
+import eventlet
+
+eventlet.sleep(0)
+eventlet.monkey_patch()
+
+from eventlet.hubs import get_hub
+import time
+
+FAILURES = []
+
+
+def zero_second_sleep():
+    try:
+        eventlet.sleep(0)
+        time.sleep(0)
+    except RuntimeError:
+        FAILURES.append(1)
+        raise
+
+
+# Simulate sleep(0) being called from a trampoline function. Or try to, anyway,
+# not really sure if this matches the original reported bug but it does at
+# least trigger the RuntimeError about blocking functions lacking the fix for
+# sleep(0).
+get_hub().schedule_call_local(0, zero_second_sleep)
+zero_second_sleep()
+
+if FAILURES:
+    raise RuntimeError("There were failures")
+
+print("pass")