Если многошаговый процесс завершается с ошибкой, повторный запуск целиком может привести к повторению уже произошедших эффектов: созданию двух заказов, отправке двух уведомлений или повторному импорту одной и той же записи. Частичное восстановление сбойных процессов в PHP заключается в том, чтобы определить, какие шаги подтверждены, какие можно безопасно повторить, а какие требуют компенсации или проверки человеком.
Решение зависит от семантики каждой операции, а не только от того, где возникло исключение. Например, потерянный ответ API не доказывает, что внешняя система отклонила запрос. Процесс мог завершиться успешно, а соединение — прерваться до того, как PHP получил результат. Проектирование с учетом такого случая позволяет не путать прерванное выполнение с невыполненным.
Выбор между повтором, возобновлением и компенсацией

Полный повтор заново выполняет все шаги. Он подходит, если весь процесс идемпотентен — повторное выполнение приводит к тому же конечному состоянию, — или если внешние эффекты еще не возникли. Если ни одно из этих условий нельзя гарантировать, повторный запуск без проверки рискован.
Возобновление означает продолжение с первого неподтвержденного шага. Для этого необходимо записывать состояние шагов и их результаты, а также иметь возможность восстановить или проверить эффект вызова, результат которого неоднозначен. Это не означает, что можно пропустить все шаги, которые выглядят завершенными: нужны сохраненные доказательства.
Компенсация — это выполнение действия, которое нейтрализует предыдущий эффект, например отмена бронирования. Она не всегда в точности восстанавливает исходное состояние: уже отправленное уведомление нельзя отозвать, а списанный платеж может потребовать возврата средств, для которого предусмотрены собственные сроки и учет. Поэтому компенсация — это явная бизнес-операция, а не автоматический rollback базы данных.
Моделирование шагов с сохранением состояний и результатов
Представьте процесс как последовательность или машину состояний, шаги которой имеют стабильные имена, идентифицируемые входные данные и сохраняемые результаты. Начальная модель может включать состояния pending, running, succeeded, retryable, failed и manual_review. Определите допустимые переходы и не позволяйте процессу переходить в конечное состояние, пока не сохранены необходимые доказательства.
Запись процесса может включать стабильный идентификатор, тип процесса, его общее состояние, версию описания процесса, даты начала и обновления, номер попытки и причину последнего перехода. Для каждого шага нужно сохранять его состояние, идентификатор операции, временные метки и ссылку на релевантный результат. Сохраняйте только те сведения, которые необходимы для возобновления процесса или объяснения результата; не копируйте без разбора полные ответы API или секреты.
В PHP координатор может отделять переход состояния от выполнения шага. Если одну и ту же задачу могут взять несколько воркеров, обновление должно быть атомарным: используйте транзакцию или подходящий механизм блокировки и записывайте, кто захватил процесс и до какого времени. Блокировка с истечением срока действия должна позволять восстанавливать брошенные задачи, не считая завершенным шаг, выполнение которого остановилось на полпути.
Определение контрольных точек без предположения о гарантии «ровно один раз»
Сохраняйте контрольную точку после каждого результата, который система может надежно подтвердить. Для локальных операций это может быть транзакция, которая одновременно сохраняет изменение бизнес-данных и состояние шага. Для внешнего вызова общей транзакции между базой данных и провайдером нет: процесс может остановиться после выполнения действия провайдером, но до того, как PHP сохранит ответ.
В таких случаях используйте ключ идемпотентности, если провайдер его поддерживает; формируйте его на основе стабильного идентификатора процесса и шага. Если такой поддержки нет, перед повторной отправкой запроса проверьте удаленное состояние по идентификатору операции. Если нет ни идемпотентности, ни надежной проверки, считайте результат неоднозначным и передавайте случай на проверку. Тайм-аута недостаточно, чтобы заключить, что операция не произошла.
Для асинхронных задач шаблон transactional outbox позволяет в рамках одной транзакции сохранить локальное изменение и ожидающее отправки сообщение. Затем воркер доставляет сообщение; потребитель тоже должен корректно обрабатывать дубликаты, например, сохраняя идентификаторы, по которым обработка уже выполнялась. Эти механизмы снижают риск несогласованности, но не превращают всю распределенную интеграцию в атомарную операцию автоматически.
Установка ограничений на повторное выполнение и компенсацию
Определите для каждого шага, какие ошибки являются временными, какие — окончательными, а какие оставляют результат неизвестным. Временные сбои можно обрабатывать повторами с увеличивающейся задержкой и случайным разбросом; задайте максимальное число попыток и общий срок. Ошибки валидации или прав доступа обычно не исправляются повторными попытками: процесс лучше остановить, устранить причину и определить, допустим ли новый запуск.
Для каждого внешнего эффекта задокументируйте, можно ли его повторить, проверить, компенсировать или является ли он необратимым. Оставляйте действительный частичный результат, если операция допустима, а повторение нанесет больший ущерб, чем сохранение этого состояния; выполняйте компенсацию только при наличии безопасного и разрешенного бизнес-действия. Останавливайте процесс и передавайте его на эскалацию, если по имеющимся данным нельзя определить, что произошло, если компенсация тоже завершается с ошибкой или если действие имеет финансовые, юридические последствия либо затрагивает клиентов и требует согласования.
Политика компенсации должна определять порядок, условия, ответственного и ожидаемый результат. Записывайте компенсацию как новый шаг, связанный с исходным эффектом, а не удаляйте ее историю. Так операционная команда сможет различить действие, которое никогда не выполнялось, выполненное действие и действие, которое было компенсировано.
Предоставление операционной команде инструментов и контекста для действий
Консоль или операционная процедура должны показывать общее состояние и состояние каждого шага, последнюю ошибку с указанием ее категории, число попыток, внешние ссылки и доступные действия. Не предлагайте универсальную кнопку «повторить всё». Предусмотрите ограниченный набор вариантов: повторить идемпотентный шаг, проверить удаленное состояние, выполнить компенсацию или передать случай на эскалацию.
Защитите эти действия ролевой авторизацией; для чувствительных эффектов требуйте дополнительного подтверждения и записывайте, кто, когда, что и почему выбрал. Если повторный запуск меняет входные данные, требуйте создать новое выполнение или явно зафиксировать пересмотр, а не незаметно изменять входные данные исторического процесса.
Чтобы диагностировать проблемы, не раскрывая чувствительные сведения, сохраняйте идентификаторы корреляции, коды ошибок, версию процесса и ссылки, необходимые для обращения к исходным системам. Маскируйте токены, персональные данные и полные полезные нагрузки (payload). Также определите срок хранения журналов и круг лиц, имеющих к ним доступ. Полезная трассировка объясняет произошедшее, не превращаясь в ненужную копию бизнес-данных.
Тестирование сбоев и поэтапное внедрение восстановления
Проверяйте прерывания в конкретных точках: до выполнения шага, после действия провайдера, но до сохранения ответа, во время компенсации и в момент, когда два воркера пытаются захватить один процесс. Для каждого случая проверяйте согласованность состояния, отсутствие дублирования эффектов и наличие аудита ручных действий.
Добавьте тесты для неоднозначных ответов, повторно использованных ключей идемпотентности, некорректных данных, лимитов повторных попыток и изменений версии процесса. Интеграционные тесты с имитацией зависимостей позволяют воспроизводить контролируемые сбои; если поведение реального провайдера отличается, дополнительно проверяйте контракт и механизмы запросов состояния в подходящей среде.
Для существующего процесса сначала классифицируйте шаги по обратимости и идемпотентности. Затем начните сохранять состояние для ограниченного этапа, реализуйте восстановление после наиболее рискованных сбоев и изучите ожидающие обработки случаи, прежде чем расширять охват. Не удаляйте и не сбрасывайте исторические записи ради упрощения внедрения: сохраняйте трассируемость и определите, как интерпретировать процессы, созданные с предыдущими версиями.
Контрольный список для внедрения восстановления

- Имеет ли каждый шаг идентифицируемые входные данные, сохраненное состояние и проверяемый результат?
- Известно ли, какие вызовы идемпотентны и что делать, если их результат неоднозначен?
- Заданы ли лимиты попыток, сроки и классификация ошибок?
- Определены ли компенсации как бизнес-действия с аудитом и ответственным?
- Может ли операционная команда проверять состояние и выполнять действия с надлежащими правами, не получая доступа к ненужным данным?
- Проверены ли сбои между шагами, конкурентное выполнение, повторные запуски и неудачные компенсации?
- Есть ли процедура эскалации на случай, когда автоматическое возобновление небезопасно?
Практический критерий — сохранять достаточно доказательств для выбора следующего шага и останавливать автоматизацию, когда этих доказательств недостаточно. Безопасное восстановление не пытается скрыть факт сбоя: оно явно показывает, что завершено, что еще ожидает выполнения и кто может решить проблему.



