vutran1710/PyrateLimiter

Deadlock/hang is possible from multi-threaded calling

Closed this issue · 5 comments

I'm afraid I only have a statistical reproduction of this, based on an issue I'm seeing in a real world application.

I ran the following code on Python 3.9, WSL2, AMD processor, and after a few hours it stopped printing.

from pyrate_limiter import Duration, RequestRate, Limiter
import random
import datetime
import concurrent

CONCURRENCY = 32

limiter = Limiter(RequestRate(1000, Duration.SECOND))

@limiter.ratelimit('get_feed_page', delay=True)
def test():
    if random.randint(0, 999) == 0:
        print(datetime.datetime.now())

while True:
    with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as executor:
        for _ in range(CONCURRENCY):
            executor.submit(test)

The stack trace when I CTRL-C this is:

KeyboardInterrupt                         Traceback (most recent call last)
Cell In [13], line 4
      2 with concurrent.futures.ThreadPoolExecutor(max_workers=CONCURRENCY) as executor:
      3     for _ in range(CONCURRENCY):
----> 4         executor.submit(test)

File ~/.pyenv/versions/3.9.7/lib/python3.9/concurrent/futures/_base.py:636, in Executor.__exit__(self, exc_type, exc_val, exc_tb)
    635 def __exit__(self, exc_type, exc_val, exc_tb):
--> 636     self.shutdown(wait=True)
    637     return False

File ~/.pyenv/versions/3.9.7/lib/python3.9/concurrent/futures/thread.py:229, in ThreadPoolExecutor.shutdown(self, wait, cancel_futures)
    227 if wait:
    228     for t in self._threads:
--> 229         t.join()

File ~/.pyenv/versions/3.9.7/lib/python3.9/threading.py:1053, in Thread.join(self, timeout)
   1050     raise RuntimeError("cannot join current thread")
   1052 if timeout is None:
-> 1053     self._wait_for_tstate_lock()
   1054 else:
   1055     # the behavior of a negative timeout isn't documented, but
   1056     # historically .join(timeout=x) for x<0 has acted as if timeout=0
   1057     self._wait_for_tstate_lock(timeout=max(timeout, 0))

File ~/.pyenv/versions/3.9.7/lib/python3.9/threading.py:1069, in Thread._wait_for_tstate_lock(self, block, timeout)
   1067 if lock is None:  # already determined that the C code is done
   1068     assert self._is_stopped
-> 1069 elif lock.acquire(block, timeout):
   1070     lock.release()
   1071     self._stop()

When I try and then exit ipython, it hangs, which would point to one of the threads the ThreadPoolExecutor creates being still live (which the stack trace also points to).

Once I'm free, I will see what I can do

@vutran1710 if you're going to close this, you really should remove threaded support entirely. This bug means this library isn't safe to use in any real world multi-threaded scenario.

@vutran1710 if you're going to close this, you really should remove threaded support entirely. This bug means this library isn't safe to use in any real world multi-threaded scenario.

I made some drastic change and it should not happen again.

@vutran1710 if you're going to close this, you really should remove threaded support entirely. This bug means this library isn't safe to use in any real world multi-threaded scenario.

I made some drastic change and it should not happen again.

Oh that's great news. When you closed it as "not planned" I thought that meant the bug is still present. I'll test with my reproduction and confirm we're good.

@vutran1710 I cannot reproduce with my reproduction in the original issue description.

I'll start using the library in the threaded workloads where I encountered this issue, and if I see any problems I'll open another issue. But it looks like you've fixed this bug; thanks!