Hasta ahora, la aplicación ya funcionaba en Heroku, pero con una implementación de Session Store en memoria.
Por tanto, las sesiones no sobrevivían a reinicios y no se podían utilizar múltiples instancias del microservicio.
La implementación de Session Store en redis soluciona este problema.
- Utiliza la biblioteca aioredis como cliente de redis con interfaz asíncrona.
- Utiliza
picklepara serializar y deserializar las estructuras de datos. - Determina una fecha de expiración configurable. Las sesiones se utilizan y lo normal es que no se vuelvan a acceder a ellas, por lo que acaban expirando para liberar espacio.
Sin embargo, también ha introducido un problema:
-
Existe una condición de carrera:
- Dos usuarios votan.
- Se obtiene la sesión dos veces.
- Se modifican las dos sesiones en memoria con los votos
- Se guardan las dos sesiones. Un voto se pierde.
-
Para solucionarlo he usado la orden
setnxde redis:
async def get_one_and_lock(self, session_id: str) -> Session:
acquired = 0
while acquired == 0:
acquired = await self.__redis.setnx(get_lock_name(session_id), 'true')
if acquired == 0:
# Devuelve el control al eventloop
await asyncio.sleep(0)
""" Si un proceso obtiene el lock y crashea, no pasa nada.
El lock tiene expiración """
await self.__redis.pexpire(get_lock_name(session_id), 100)
return await self.get_one(session_id)- Pone un "lock" a la sesión.
- Este lock se elimina cuando se guarda la sesión modificada o si pasan 100ms (eliminando la posibilidad de bloqueo).
- Durante ese lock, si otra petición quiere modificar la misma sesión "espera" con
asyncio.sleepsin bloquear otras peticiones. - Esta función solo se usa si se va a modificar la sesión, si no, se utiliza
get_oneque no pone un lock a la sesión.
Heroku permite añadir redis como "add-on". Al hacerlo, automáticamente se añade la variable de entorno REDIS_URL.
Sin embargo, podría utilizar cualquier otro servicio redis fuera del PaaS sin problema.