Store data in Redis with Flask Async
For a future project idea I wanted to test how complicated Flask + Redis in Python async is. Redis-py got official async support with version 4.2.x and Flask has async support since version 2.0. Some additional libraries are needed to run this: asgiref and uvicorn.
First I needed a redis database to test this. I could have started a local docker container with redis, but I want to host this later on fly.io so I chose the free plan of the Redis Cloud (30MB are more than enough).
First we need a redis client that can process everything async:
import os import json import redis.asyncio as redis class Store: def __init__(self): self.r = redis.from_url(os.environ.get("REDIS_URL")) async def save(self, name, state): return await self.r.set(f"game-{name}", json.dumps(state)) async def load(self, name): game = await self.r.get(f"game-{name}") if game: return json.loads(game) return game
On save the game state, which is a dict, is stored as a json string in redis. If the key is found on load this is reversed, otherwise the "None" is returned.
Now the annotated main.py:
import uuid from asgiref.wsgi import WsgiToAsgi from flask import Flask, make_response, request # store.py has the previous snippet from .store import Store wsgi_app = Flask(__name__) store = Store() @wsgi_app.route("/new") async def new(): name = str(uuid.uuid4()).split("-")[-1] user = request.cookies.get("user_id", str(uuid.uuid4())) state = {"name": name, "players": [user]} await store.save(name, state) resp = make_response(f"new game created: {name}<br/> you are: {user} (cookie set)") resp.set_cookie("user_id", user) return resp @wsgi_app.route("/join/<name>") async def join(name): state = await store.load(name) if state: user = request.cookies.get("user_id", str(uuid.uuid4())) if user not in state["players"]: state["players"].append(user) await store.save(name, state) resp = make_response(f"{name} found, players: {state['players']}") resp.set_cookie("user_id", user) return resp return "game not found" @wsgi_app.route("/") async def index(): return "nothing to see" app = WsgiToAsgi(wsgi_app)
A lot is happening here:
The wsgi_app
is the "normal" Flask app. With WsgiToAsgi
this can now run via uvicorn
.
For example like this: uvicorn main:app --reload
.
The REDIS_URL
should be set before running (for a local docker setup this would be redis://localhost:6379
).
Without the asgi setup there is no async mainloop for the Redis store and the Flask app to share.
This breaks pretty fast, so the whole app has to run in one asgi context.
The /new
endpoint created a new "game" and adds the current user as player.
The user-id is stored in a cookie and read when already set.
To return a response with a cookie in it a response object is created via make_response
.
The current game name is the last part of a uuid4.
In the "real" version I use a name generator that creates nicer names, i.e. "dark-red-deer-42".
The /join/<name>
endpoint adds the current user as player (if not already in the players list).
This is only to demonstrate how endpoints like this would work.
What is missing: no error checking for the redis client!
All the store.save()
should be checked for returned values.
One final note, I run this on fly.io with a Procfile like this:
web: uvicorn app.main:app --host=0.0.0.0 --port=8080