ionelmc / python-redis-lock

Lock context manager implemented via redis SET NX EX and BLPOP.
https://pypi.python.org/pypi/python-redis-lock
BSD 2-Clause "Simplified" License
543 stars 76 forks source link

TypeError: 'NoneType' object is not callable #96

Open froOzzy opened 2 years ago

froOzzy commented 2 years ago

When using redis_lock , the following errors began to be received:

Traceback (most recent call last):
  File "./appointment/lock.py", line 40, in wrapped
    lock.release()
  File "/home/test/.cache/pypoetry/virtualenvs/pd-GUvEH9yN-py3.7/lib/python3.7/site-packages/redis_lock/__init__.py", line 361, in release
    error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
TypeError: 'NoneType' object is not callable

The latest version of python-redis-lock 3.7.0 is used. Added logging of this error and found that all the functions that are registered at the very beginning are not registered https://github.com/ionelmc/python-redis-lock/blob/8e1872e56375d707c7ffa739511b555a5639f821/src/redis_lock/__init__.py#L170

A line from the log:

{"timestamp": "2022-08-17T10:15:24.272596", "level": "INFO", "name": "lock_by_ip_and_user", "message": "release failed", "location": "appointment/lock.py:56", "lock": "<redis_lock.Lock object at 0x7f5944878ef0>", "unlock_script": "None", "extend_script": "None", "reset_script": "None", "reset_all_script": "None"}

Please help me to sort out the problem 🙏

ionelmc commented 2 years ago

Do you pickle/unpickle the lock instances?

ionelmc commented 2 years ago

Or use anything that might do that (multiprocessing, ipc frameworks etc)?

froOzzy commented 2 years ago

I use this library in conjunction with Django == 3.2 to prevent simultaneous execution of the operation. Example:

def lock_user_request(func):  # decorator for blocking double removal
    @wraps(func)
    def wrapped(*args, **kwargs):
        ...
        lock_key = 'key'
        redis_conn = redis_connection(strict=True)
        lock = redis_lock.Lock(redis_conn, lock_key, expire=60)
        if lock.acquire(blocking=False):
            result = func(request, *args, **kwargs)
            try:
                lock.release()  # The error occurs here
                logger.info('release %s', lock_key)
            except Exception:
                ...

            return result
        else:
            raise redis_lock.NotAcquired('Can not acquire key')

@lock_user_request
@transaction.atomic
def drop(data):  # some function for deleting data
    ...
    return True

def func(request):  # view
    ...
    success_drop = drop(data)
    ...

Moreover, this error does not always appear, but periodically

ionelmc commented 2 years ago

Do you use this in a thread-based webserver? I wonder if there's some race condition going on somewhere.

ionelmc commented 2 years ago

I wonder if 0db1d1d makes your problem go away.

froOzzy commented 1 year ago

Thank you for your help, I will try to check your changes as soon as possible

froOzzy commented 1 year ago

Good afternoon, I'm sorry that I checked your changes for a long time, a lot of work by the end of the year, but unfortunately these changes did not help.

Interestingly, in the tests I was able to get around this problem using a fixture.

@fixture
def mock_reset_all_script(mocker: MockerFixture) -> None:
    mocker.patch('redis_lock.reset_all_script', None)

I run tests with pytest and pytest-xdist.

ionelmc commented 1 year ago

@froOzzy So this is a problem that appears in your test suite? Can you extract a reproducer?

froOzzy commented 1 year ago

In production and more recently (after adding the pytest-testmon library) and CI/CD

ionelmc commented 1 year ago

Well I need more info. Are you pickling the lock instances or anything strange?

froOzzy commented 1 year ago

For me now this is not a burning problem, so in my free time I will assemble a mini test project for you, on which I will reproduce this problem, so that you have everything you need to fix the problem (in production I also found how to get around the problem)

yonil commented 1 year ago

I am experiencing the same issue after upgrading from 3.5.0 to 4.0.0 Currently not able to reproduce yet as this happens only when running my tests on Travis. I will try to get more info, but was wondering if there's any update on this?

chrishawes-zc commented 8 months ago

I worked around this issue by subclassing redis_lock.Lock and overriding Lock.register_scripts() as an instance method, rather than a class method, which sets the script variables on each instance. This seems to have fixed the problem for us (so far), and it doesn't seem to add any significant overhead.

    def register_scripts(self, redis_client):
        redis_lock.reset_all_script = redis_client.register_script(redis_lock.RESET_ALL_SCRIPT)
        self.unlock_script = redis_client.register_script(redis_lock.UNLOCK_SCRIPT)
        self.extend_script = redis_client.register_script(redis_lock.EXTEND_SCRIPT)
        self.reset_script = redis_client.register_script(redis_lock.RESET_SCRIPT)
        self.reset_all_script = redis_client.register_script(redis_lock.RESET_ALL_SCRIPT)

Another idea would be to set these as variables in the global scope since there is nothing about them that would differ between instances anyways.

gshmu commented 3 months ago

I'm using python-redis-lock==4.0.0, this error occurred once.

I use this in fastapi to prevent duplicate calls to the interface.