Миграцию приходится повторять после сетевого сбоя, таймаута или исправления преобразования. Если повторный запуск создает новые копии записей, процесс не умеет отличать уже обработанный объект от нового и не является идемпотентным.
Основа исправления — стабильный ключ источника, уникальное ограничение в приемнике и операция upsert. Checkpoint помогает продолжать работу, но не заменяет уникальность: задача может завершиться после записи и до отметки прогресса.
Что проверить в первую очередь
Сначала зафиксируйте наблюдаемое поведение и не меняйте сразу несколько настроек. Важны точное время сбоя, адрес или сценарий, ожидаемый результат и последнее известное рабочее состояние. Так можно отличить причину от случайного совпадения и сохранить возможность быстрого отката.
- Определите, есть ли у каждой исходной сущности неизменяемый уникальный ID.
- Найдите, по какому набору полей сейчас решается, новая запись или существующая.
- Проверьте уникальные индексы приемника и поведение при конфликте.
- Воспроизведите сбой после записи, но до сохранения checkpoint.
Почему возникает проблема
У подобных сбоев редко бывает одна универсальная причина. На итог одновременно влияют конфигурация приложения, окружение, данные, кеш, права и внешние сервисы. Проверка должна идти от внешнего симптома к конкретному уровню, на котором впервые появляется неверное состояние.
- Каждый запуск генерирует новый UUID вместо сохранения связи source_id → target_id.
- Поиск дубля выполняется по изменяемым полям: имени, email или времени импорта.
- Проверка SELECT затем INSERT не защищена от параллельных воркеров.
- Checkpoint обновляется отдельно и теряется после частичного сбоя.
- Одна сущность приходит из нескольких источников без namespace или приоритета.
Пошаговая диагностика
Диагностику проводите на копии или в контролируемое время. Перед изменениями сохраните конфигурацию, данные и журналы. Каждый шаг должен отвечать на один вопрос и оставлять измеримый результат: код ответа, запись в логе, состояние процесса, значение поля или воспроизводимый тест.
- Выберите один дубликат и сравните source ID, payload, время вставки и запуск задачи.
- Проверьте план и ограничения таблицы, а не только прикладную проверку exists.
- Запустите один небольшой batch дважды и сравните количество insert/update/no-op.
- Искусственно оборвите задачу между пакетами и повторите ее на тестовой базе.
- Проверьте параллельный запуск двух воркеров на пересекающемся диапазоне.
Идемпотентность на уровне данных
Надежность обеспечивается не флагом задачи, а инвариантом, который база может проверить атомарно.
- Храните source_system и source_id с уникальным составным индексом.
- Используйте upsert с явным перечнем обновляемых полей и правилами разрешения конфликтов.
- Для событий применяйте уникальный event_id и отдельный журнал обработки.
- Checkpoint сохраняйте после коммита batch, но допускайте безопасное повторение последнего пакета.
- Для удалений определите tombstone/статус, чтобы повтор не воскресил устаревшую запись.
Как исправить проблему
Исправление лучше разбить на небольшие обратимые изменения. После каждого шага повторяйте исходный сценарий и проверяйте соседние функции. Если правка касается данных, сначала выполните ее на ограниченной выборке и сохраните журнал затронутых записей.
- Добавьте стабильный внешний ключ и заполните его для существующих записей после сверки.
- Создайте unique constraint, предварительно подготовив отчет о найденных конфликтах.
- Замените SELECT+INSERT атомарным upsert или транзакцией с блокировкой.
- Разбейте миграцию на ограниченные batch с журналом результата и повторов.
- После выполнения запустите reconciliation: количество, суммы, связи и выборочные хеши.
Безопасный порядок внедрения
- Сохраните резервную копию затрагиваемых файлов, базы и конфигурации, а также заранее опишите способ отката.
- Воспроизведите сбой на тестовой записи, учетной записи или отдельном окружении без реальных платежей и рассылок.
- Вносите по одному логическому изменению, фиксируя его в системе контроля версий или журнале работ.
- Не отключайте права, проверку входных данных и защитные механизмы только ради исчезновения сообщения об ошибке.
- После выкладки контролируйте логи, метрики и ключевой пользовательский сценарий, а не только открытие одной страницы.
Как проверить результат
Успешный разовый тест еще не доказывает исправление. Нужны повторный запуск, крайние случаи и проверка после очистки кеша, перезапуска процесса или новой сессии. Для критичных сценариев полезно сохранить автоматический тест или хотя бы короткий регрессионный чек-лист.
- Два одинаковых запуска не увеличивают количество целевых сущностей.
- Повтор после искусственного обрыва корректно обновляет или пропускает последний batch.
- Параллельные воркеры не создают дубли благодаря ограничению базы.
- Отчет разделяет inserted, updated, skipped и conflicted и сходится с источником.
Типичные ошибки при исправлении
- Удалять дубли до добавления правила, которое не даст им появиться снова.
- Считать email универсальным стабильным ID для всех сущностей.
- Полагаться только на checkpoint без unique constraint.
- Применять upsert, который незаметно перетирает более новые данные старым источником.
Как предотвратить повторение
Профилактика строится вокруг наблюдаемости и воспроизводимости: понятных конфигураций, контролируемых релизов, журналов без секретов и тестов на реальные сценарии. Важно не просто убрать текущий симптом, а сделать следующий похожий сбой заметным раньше пользователя.
- Проектируйте миграции как повторяемые операции с самого начала.
- Добавляйте тест двойного запуска и аварийного продолжения.
- Храните версию преобразования и идентификатор batch у измененных записей.
- Проверяйте инварианты и reconciliation автоматически после каждого запуска.
Что подготовить для разбора
- Краткое описание ожидаемого и фактического поведения без паролей, токенов и персональных данных.
- Точное время и последовательность действий, после которых появляется проблема.
- Версии приложения, окружения и зависимостей, а также перечень последних изменений.
- Фрагменты журналов с контекстом до и после ошибки; секреты в них необходимо скрыть.
- Описание уже выполненных проверок и способ безопасно повторить проблему.
Частые вопросы
Можно ли просто пропускать уже существующий email?
Это опасно, если email меняется или принадлежит разным типам сущностей. Лучше использовать неизменяемый ID источника и namespace.
Нужен ли checkpoint при наличии upsert?
Да, он повышает эффективность и позволяет продолжать с нужного места, но корректность при повторе обеспечивает идемпотентная запись и ограничения.
Когда нужна помощь специалиста
Если миграция создает дубли или ее страшно перезапускать, я могу разобрать ключи и точки отказа, сделать загрузку идемпотентной, подготовить безопасную очистку существующих дублей и отчет сверки.