Skip to content

Locking in multiple concurrent processes can cause hitting redis repeatedly #333

@nickderobertis

Description

@nickderobertis

I have been observing that occasionally some of my cached + locked functions get stuck calling set_lock repeatedly. This leads to millions of redis calls in a ~30 min span in our application.

Image

As I started to dig into this, I found other unexpected behavior with locking and opened #332. Based on that investigation, we need ttl=None on the regular cache and positive ttl on the lock to actually achieve proper locking right now.

When I write a script with that, multiprocessing, and a function with a random execution time, I can recreate the issue consistently.

import asyncio
import multiprocessing
import random
from cashews import Command, cache
from cashews.backends.interface import Backend
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
)
r.flushall()

_counter = 0


async def _logging_middleware(call, cmd: Command, backend: Backend, *args, **kwargs):
    global _counter
    if cmd == Command.SET_LOCK:
        _counter += 1
    return await call(*args, **kwargs)


_middlewares = (_logging_middleware,)


cache.setup(
    "redis://localhost:6379/0",
    client_side=True,
    retry_on_timeout=True,
    middlewares=_middlewares,
)


@cache(ttl=None)
@cache.locked(ttl=5)
async def test():
    print("running function")
    await asyncio.sleep(random.random() * 0.1)
    return 1


async def _main():
    await test()
    print("set lock called", _counter, "times")


def main():
    asyncio.run(_main())


multiprocessing.set_start_method("fork")
for _ in range(2):
    multiprocessing.Process(target=main).start()

Outputs:

running function
running function
set lock called 1 times
set lock called 185 times

I think the issue is related to this while loop. If it calls self.set_lock and it returns False, then self.set_lock gets called again almost immediately. The implementation in redis passes nx=True and from my testing I see that returns None if the key is already set. So if a function gets into this loop while the key is already set, it will repeatedly hit the cache until the key is removed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions