vutran1710 / PyrateLimiter

⚔️Python Rate-Limiter using Leaky-Bucket Algorithm Family
https://pyratelimiter.readthedocs.io
MIT License
336 stars 36 forks source link

Deadlock/hang is possible from multi-threaded calling #108

Closed SamStephens closed 6 months ago

SamStephens commented 1 year ago

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).

vutran1710 commented 1 year ago

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

SamStephens commented 6 months ago

@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 commented 6 months ago

@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.

SamStephens commented 6 months ago

@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.

SamStephens commented 6 months ago

@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!