Search code examples
pythonapiredismultiprocessingsanic

Trouble using Sanic and Redis


I am using Sanic with 2 workers. I am trying to get a billing system working, i.e. Counting how many times a user hit the API endpoint. Following is my code:

class User(object):
    def __init__(self, id, name, age, address, mobile, credits=0):
        self.id = id
        self.name = name
        self.credits = count
        self.details = {"age": age, "address": address, "mobile_number": mobile}

The above Users Class is used to make objects that I have uploaded onto Redis using another python script as follows:

user = User(..., credits = 10)
string_obj = json.dumps(user)
root.set(f"{user.user_id}", string_obj)

The main issue arises when I want to maintain a count of the number of hits an endpoint receives and track it withing the user object and upload it back onto Redis. My code is as follows:

from sanic_redis_ext import RedisExtension

app = Sanic("Testing")
app.config.update(
{
    "REDIS_HOST": "127.0.0.1",
    "REDIS_PORT": 6379,
    "REDIS_DATABASE": 0,
    "REDIS_SSL": None,
    "REDIS_ENCODING": "utf-8",
    "REDIS_MIN_SIZE_POOL": 1,
    "REDIS_MAX_SIZE_POOL": 10,
})

@app.route("/test", methods=["POST"])
@inject_user()
@protected()
async def foo(request, user):
    user.credits -= 1
    if user.credits < 0:
        user.credits = 0
        return sanic.response.text("Credits Exhausted")

    result = process(request)

    if not result:
        user.credits += 1

    await app.redis.set(f"{user.user_id}", json.dumps(user))
    return sanic.response.text(result)

And this is how I am retrieving the user:

async def retrieve_user(request, *args, **kwargs):
    if "user_id" in kwargs:
        user_id = kwargs.get("user_id")
    else:
        if "payload" in kwargs:
            payload = kwargs.get("payload")
        else:
            payload = await request.app.auth.extract_payload(request)
        if not payload:
            raise exceptions.MissingAuthorizationHeader()
        user_id = payload.get("user_id")

    user = json.loads(await app.redis.get(user_id))
    return user

When I use JMeter to test the API endpoint with 10 threads acting as the same user, the credit system does not seem to work. In this case, as the user starts with 10 credits, they may end up with 7 or 8 (not predictable) credits left whereas they should have 0 left. According to me, this is due to the workers not sharing the user object and not having the updated copy of the variable which is causing them to overwrite each others update. Can anyone help me find a way out of this so that even if the same user simultaneously hits the endpoint, he/she should be billed perfectly and the user object should be saved back into Redis.


Solution

  • The problem is that you read the credits info from Redis, deduct it, then save it back it to Redis, which is not an atomic process. It's a concurrency issue.

    I don't know about Python, so I'll just use pseudo code.

    First set 10 credits for user {user_id}.

    app.redis.set("{user_id}:credits", 10)
    

    Then this user comes in

    # deduct 1 from the user credits and get the result
    int remaining_credits=app.redis.incryBy ("{user_id}:credits",-1) 
    if(remaining_credits<=0){
       return sanic.response.text("Credits Exhausted")} else{
       return "sucess" # or some other result}
    

    Save your user info with payload somewhere else and retrieve the "{user_id}:credits"and combine them when you retrieve the user.