← Назад к вопросам

Расскажи про Big Fail на работе

1.0 Junior🔥 41 комментариев
#Soft Skills и карьера

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Рассказ о крупном проекте с критическим сбоем в продакшене

Коллега, поделюсь реальной историей из практики, которая стала мощнейшим уроком на десятилетия вперед. Речь о полном отказе системы авторизации в крупном банковском приложении для iOS, произошедшем в пятницу вечером, в пик активности пользователей.

Контекст и архитектурные решения

Мы разрабатывали обновление ключевого модуля аутентификации и управления сессиями. Система использовала:

  • Кастомную реализацию OAuth 2.0 с refresh-токенами
  • Локальное хранилище в Keychain для чувствительных данных
  • Собственный кэш сессий в памяти
  • Механизм автоматического продления токенов при истечении

Основная архитектурная ошибка была допущена на этапе проектирования — мы решили отказаться от использования проверенных фреймворков (вроде AppAuth) в пользу "более оптимизированного" собственного решения. Код отвечал за критически важную функциональность:

class SessionManager {
    private var currentToken: AuthToken?
    private let keychainWrapper: KeychainWrapper
    private let networkService: AuthNetworkService
    
    func refreshToken(completion: @escaping (Result<Bool, Error>) -> Void) {
        guard let refreshToken = keychainWrapper.getRefreshToken() else {
            completion(.failure(SessionError.noRefreshToken))
            return
        }
        
        networkService.refresh(refreshToken) { [weak self] result in
            switch result {
            case .success(let newToken):
                // ОШИБКА: Не атомарная операция!
                self?.keychainWrapper.save(newToken)
                self?.currentToken = newToken
                completion(.success(true))
                
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
}

Цепочка событий, приведшая к катастрофе

  1. Пятница, 18:30 — Выкатываем обновление в AppStore. Все тесты пройдены, включая нагрузочное тестирование.

  2. 20:45 — Первые пользователи начинают жаловаться на "выкидывание" из приложения. Логи показывают странное поведение: токены будто "теряются".

  3. 21:15 — Количество инцидентов растет в геометрической прогрессии. Мониторинг показывает 100% рост ошибок авторизации.

  4. 22:00 — Срабатывает алерт "критического уровня". Собирается экстренная команда.

Root Cause Analysis (RCA)

После 4 часов анализа мы обнаружили фатальную комбинацию проблем:

Основные технические причины:

  1. Гонка условий (race condition) в обновлении токенов

    • При одновременных запросах из разных частей приложения создавалось несколько refresh-запросов
    • Более старый токен мог перезаписать новый
  2. Некорректная обработка ошибок сети

    // АНТИПАТТЕРН: Тихий сброс ошибки
    func handleNetworkError(_ error: Error) {
        if error.code == -1009 { // Нет интернета
            // Просто игнорируем - "пользователь сам разберется"
            return
        }
        // ... остальная обработка
    }
    
  3. Отсутствие механизма отката (rollback)

    • При сбое сохранения в Keychain система не возвращалась в предыдущее стабильное состояние
    • Пользователи оказывались в "полуаутентифицированном" состоянии
  4. Недостаточное логирование

    • Критические состояния не логировались
    • Отсутствовали трассировочные ID для отслеживания цепочек запросов

Момент прозрения и исправление

В 3:30 ночи, просматривая логи параллельных потоков, я заметил паттерн: проблема возникала именно при быстром переключении между вкладками приложения, что активировало несколько фоновых процессов обновления токена.

Экстренный фикс потребовал:

// Решение: Введение атомарных операций с диспетчеризацией
actor TokenRefreshActor {
    private var isRefreshing = false
    private var pendingCompletions: [(Result<AuthToken, Error>) -> Void] = []
    
    func safeRefresh(currentToken: AuthToken?) async throws -> AuthToken {
        // Если обновление уже идет - ждем его результата
        if isRefreshing {
            return try await withCheckedThrowingContinuation { continuation in
                pendingCompletions.append(continuation.resume(with:))
            }
        }
        
        isRefreshing = true
        defer {
            isRefreshing = false
            completePendingRequests(with: .failure(SessionError.refreshInProgress))
        }
        
        do {
            let newToken = try await networkService.refreshToken()
            completePendingRequests(with: .success(newToken))
            return newToken
        } catch {
            completePendingRequests(with: .failure(error))
            throw error
        }
    }
}

Бизнес-последствия и извлеченные уроки

Финансовые потери:

  • 14 часов простоя критической функциональности
  • ~40% активных пользователей не могли совершать операции
  • Прямые убытки от невозможности проведения транзакций
  • Резкий рост нагрузки на кол-центр

Технические выводы:

  1. Не изобретай велосипеды для критической инфраструктуры

    • Используй проверенные решения для авторизации, сетевого слоя, хранилищ
  2. Конкурентность требует особого подхода

    • Все операции с общим состоянием должны быть thread-safe
    • Акторы (Swift Concurrency) — твой друг
  3. Полноценное тестирование edge cases

    • Обязательно тестируй сценарии:
     * Потеря сети во время операции
     * Параллельные запросы
     * Быстрое повторное нажатие кнопок
     * Перезапуск приложения в середине критической операции

  1. Мониторинг и observability

    • Внедри трейсинг запросов
    • Настрой алерты на аномальное поведение
    • Логируй достаточно, но с умом
  2. План отката и feature flags

    • Всегда имей возможность отключить функциональность без выпуска новой версии
    • Используй phased rollout даже для критических исправлений

Личные выводы

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

Ключевое изменение в подходе: теперь перед любым значимым изменением я задаю себе вопрос: "Что произойдет, если этот код выполнится дважды одновременно? А если сеть пропадет на середине? А если пользователь закроет приложение прямо сейчас?"

Такой опыт — болезненный, но бесценный. Он формирует инженерную дисциплину, учит уважать сложность и ценить простоту решений. После этого инцидента мы внедрили mandatory code review для критических модулей, практику post-mortem встреч и культуру "безопасного падения" (fail-safe design).

Расскажи про Big Fail на работе | PrepBro