Кол-во сеансов не обновляется последовательно с параллельными запросами

Я замечаю, что когда параллельные запросы изменяют Flask session, записываются только некоторые ключи. Это происходит как с сеансом cookie Flask по умолчанию, так и с Flask-Session, использующим бэкэнд Redis. Проект не новый, но это стало заметно только тогда, когда в один и тот же сеанс одновременно поступало много запросов.

import time
from flask import Flask, session
from flask_session import Session

app = Flask(__name__)
app.secret_key = "example"
app.config["SESSION_TYPE"] = "redis"
Session(app)

@app.route("/set/<value>")
def set_value(value):
    """Simulate long running task."""
    time.sleep(1)
    session[value] = "done"
    return "ok\n"

@app.route("/keys")
def keys():
    return str(session.keys()) + "\n"

Следующий скрипт оболочки демонстрирует проблему. Обратите внимание, что все запросы выполнены, но в итоговом списке присутствует только один ключ, и он отличается в разных тестовых прогонах.

#!/bin/bash
# set session
curl -c 'cookie' http://localhost:5007/keys
# run parallel
curl -b 'cookie' http://localhost:5007/set/key1 && echo "done1" &
curl -b 'cookie' http://localhost:5007/set/key2 && echo "done2" & 
curl -b 'cookie' http://localhost:5007/set/key3 && echo "done3" &
wait
# get result
curl -b 'cookie' http://localhost:5007/keys
$ sh test.sh 
dict_keys(['_permanent'])
ok
ok
ok
done3
done1
done2
dict_keys(['_permanent', 'key2'])

$ sh test.sh 
dict_keys(['_permanent'])
ok
done3
ok
ok
done2
done1
dict_keys(['_permanent', 'key1'])

Почему не все ключи присутствуют после завершения запросов?

Ответ 1

Сеансы на основе файлов cookie не являются потокобезопасными. Любой заданный запрос видит только куки файл сеанса, отправленный с ним, и возвращает только cookie с этими изменениями запроса. Это не относится к Flask, как работает HTTP-запрос.

Вы параллельно отправляете три запроса. Все они читают начальный файл cookie, который содержит только _permanent key, отправляет свои запросы и получает ответ, который устанавливает cookie с их конкретным ключом. Каждый куки-ответ должен иметь только _permanent key и key_keyN. Какой бы запрос не завершил последнюю запись в файл, переписывая предыдущие данные, поэтому вы остаетесь только с его файлом cookie.

На практике это не проблема. Сеанс не предназначен для хранения данных, которые быстро изменяются между запросами, для чего предназначена база данных. Вещи, которые меняют сеанс, например, вход в систему, происходят не параллельно одному и тому же сеансу (и в любом случае идемпотент).

Если вы действительно обеспокоены этим, используйте сеанс на стороне сервера для хранения данных в базе данных. Базы данных хорошо синхронизируют записи.


Вы уже используете Flask-Session и Redis, но встраивание в реализацию Flask-Session показывает, почему у вас есть эта проблема. Flask-Session не хранит каждый ключ сеанса отдельно, он записывает одно сериализованное значение со всеми ключами. Таким образом, он испытывает такую же проблему, как сеансы на основе файлов cookie: только то, что присутствовало во время этого запроса, помещается обратно в Redis, переписывая то, что произошло параллельно.

В этом случае будет лучше написать собственный подкласс SessionInterface для хранения каждого ключа отдельно. Вы бы переопределили save_session чтобы установить все ключи в session и удалить все, которых нет.