← Все проекты уровня 10
Уровень 10 · Связь с другими сервисамиВариант B

Синхронизация задач

Согласование задач между двумя системами с устойчивыми внешними идентификаторами и явным разрешением конфликтов.

Техническое задание

Модель данных и назначение

Создайте Go-сервис синхронизации локальных задач с двумя внешними системами и PostgreSQL. Task содержит id, title, description, status, due_at, revision, updated_at; сервер задаёт id, revision и updated_at. TaskLink хранит id, local_task_id, provider, external_task_id, last_local_revision, last_remote_revision, last_synced_at, sync_state. Внешняя идентичность уникальна по (provider, external_task_id), локальная связь — по (provider, local_task_id). Адаптеры нормализуют поля и статусы, сохраняя внешнюю ревизию и удаление как tombstone.

API, проверки и эксплуатация

POST /api/tasks создаёт задачу и возвращает id (201): title длиной 1–160 символов, description до 10000 символов, status open/in_progress/done и nullable dueAt в RFC 3339 UTC; id, revision=1 и updatedAt назначает сервер. PATCH /api/tasks/{id} принимает одно или несколько из этих четырёх полей, валидирует их по тем же ограничениям, атомарно увеличивает revision и задаёт updatedAt сервера; dueAt допускает null для очистки, остальные поля — нет. Неизвестный id даёт 404, неверные поля — 400. POST /api/sync-runs принимает provider из system_alpha/system_beta, direction (push, pull, bidirectional), taskIds и idempotencyKey. taskIds содержит от 1 до 500 уникальных id; пустой список, повтор id или больше 500 элементов дают 400. idempotencyKey содержит 1–128 символов и обязателен. Запрос создаёт сохраняемое задание и отвечает 202 с run id. Повтор idempotencyKey с теми же параметрами возвращает тот же run, с другими — 409. GET /api/sync-runs/{id} сообщает state pending/running/completed/failed/cancelled, прогресс, counters created/updated/skipped/conflicts и безопасные error codes. GET /api/sync-runs/{id}/conflicts возвращает стабильную страницу конфликтов с локальной и внешней ревизиями и выбранным исходом. POST /api/sync-runs/{id}/cancel отменяет pending/running на границе одной задачи и отвечает 200; completed/failed/cancelled дают 409.

Направление push изменяет только удалённую систему, pull — только локальную, bidirectional — обе. Сервис помечает собственные изменения origin/run ID и не принимает их за новую работу, поэтому обратный цикл не возникает. Внешний объект всегда связывается по уникальной паре (provider, external_task_id), локальная связь — по (provider, local_task_id). Если обе стороны изменились после last_*_revision, побеждает более поздний updated_at в UTC; при равенстве побеждает локальная запись. Решение записывается в историю конфликтов, автоматического слияния полей нет. Удаление передаётся tombstone и не отменяется старым объектом из другой стороны.

Фоновые исполнители настраиваются от 1 до 8, по умолчанию 2; задание выполняется не более трёх раз с паузами 10 и 30 секунд; каждая полная попытка синхронизации длится не более 60 секунд, а запросы адаптера используют контекст этой попытки и прекращаются вместе с ним. При временной ошибке повторяется запрос, постоянная ошибка завершает run состоянием failed. После перезапуска задания pending и прерванные running восстанавливаются, один запуск атомарно захватывает только один исполнитель. Отмена прекращает обработку на границе задачи; уже подтверждённое внешнее изменение сохраняется в локальной отметке синхронизации. Запись во внешнюю систему передаёт ожидаемую ревизию внешней записи (expectedRemoteRevision); при конфликте версия загружается заново и применяется та же политика. Локальные изменения выполняются транзакционно и идемпотентно. API поддерживает limit/cursor и коды 400/404/409. SQL-запросы ограничены контекстом, ключи провайдеров находятся в конфигурации и никогда не попадают в журнал. Содержимое задач не логируется.

Структура программы

Код разделён на небольшие internal-пакеты: internal/httpapi содержит DTO и маршруты, internal/syncdomain задаёт идентичность, направления и конфликты, internal/adapters общается с двумя системами, internal/workers запускает задания. main.go связывает приложение; ctx передаётся до адаптера, функции не вызывают os.Exit.

Критерии готовности

Ожидаемый результат

Поставка

  • Запускаемый Go REST API, PostgreSQL-миграции и README описывают оба адаптера с заменяемой реализацией, направления синхронизации и политику разрешения конфликтов.
  • Детерминированные офлайн-тесты используют две управляемые тестовые системы и временную локальную базу без внешних учётных записей.

Приёмка

  • Повтор отправки после тайм-аута использует тот же внешний идентификатор и не создаёт вторую удалённую задачу.
  • Изменение с origin/run ID не порождает цикл; pull никогда не меняет удалённую запись.
  • При конфликте выигрывает больший updated_at UTC, при равенстве local; решение записано в истории конфликтов.
  • POST создаёт задачу с revision=1; PATCH неизвестного id даёт 404, неверный status — 400, а корректное изменение атомарно увеличивает revision и updated_at.
  • Повтор idempotencyKey с другими параметрами даёт 409, tombstone не воскрешается старым объектом.
  • Число исполнителей настраивается от 1 до 8, по умолчанию 2; выполняется не более трёх попыток с паузами 10 и 30 секунд, полная попытка синхронизации ограничена 60 секундами; запросы адаптера используют контекст попытки, progress — целое 0–100; задание и результат восстанавливаются после перезапуска.
  • README показывает зависимости; конфликтные правила тестируются без HTTP/БД, адаптеры — двумя эмуляторами, SQL-сценарии — отдельно.