Ответ
Сложный баг: Race Condition (состояние гонки) при параллельном обновлении баланса пользователя через API.
Проблема: При быстрых последовательных запросах на пополнение счета (например, два одновременных запроса на +50 и +30 к балансу 100) итоговый баланс мог стать 130 или 150, вместо корректных 180. Данные терялись.
Исследование и воспроизведение:
- Анализ: Изучение логики кода показало уязвимость:
баланс = прочитать_из_БД(); баланс += сумма; сохранить_в_БД(баланс). - Воспроизведение: Создание скрипта для имитации параллельных запросов.
import concurrent.futures import requests
Функция для пополнения баланса
def add_funds(user_id, amount): response = requests.post( f"https://api.example.com/users/{user_id}/balance", json={"action": "add", "amount": amount} ) return response.json()
Запуск 10 параллельных запросов
user_id = "user123" with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: futures = [executor.submit(add_funds, userid, 10) for in range(10)] results = [f.result() for f in futures]
Проверка итогового баланса - может быть меньше 100 (стартовый 0 + 10*10)
final_balance = requests.get(f"https://api.example.com/users/{user_id}/balance").json() print(f"Итоговый баланс: {final_balance}") # Может быть 60, 70 и т.д.
3. **Логирование:** Добавление детального логирования (ID запроса, баланс до/после) помогло увидеть "наложение" операций.
**Причина и решение:**
* **Причина:** Отсутствие механизма блокировки или атомарности операций на уровне БД.
* **Решение:**
1. **Оптимальное:** Изменить логику на атомарную операцию БД: `UPDATE balance SET value = value + :amount WHERE user_id = :id`.
2. **Альтернативное:** Внедрить **оптимистичную блокировку** (проверка версии записи) или **пессимистичную блокировку** (SELECT FOR UPDATE) на уровне приложения.
**Итог:** Баг был исправлен переходом на атомарный SQL-запрос. Это подчеркивает важность тестирования многопоточных сценариев и понимания работы с данными в конкурентной среде.