Scheduled data sync: инкрементальная синхронизация по watermark-курсору
Cron-задача каждый час: запросить изменения после `last_synced_at`, сделать idempotent upsert, обновить курсор. Без полного перезапроса, без дублей.
Last updated: 2026-04-20
Answer first
Direct answer
Scheduled data sync: инкрементальная синхронизация по watermark-курсору. Cron-пайплайн читает `last_synced_at` из хранилища, запрашивает изменения после этой отметки, делает `UPSERT ON CONFLICT`, сдвигает курсор только после успешной записи.
When it fits
- Источник поддерживает фильтрацию по `updated_at`
- Нужна инкрементальная синхронизация без полного перезапроса
Tradeoffs
- Обновлять `last_synced_at` до завершения записи: при ошибке записи курсор уже сдвинут — данные потеряны тихо.
- Без idempotent upsert повторный прогон из-за ретрая cron создаёт дубликаты.
Нагрузка и где ломается
Как ломается синхронизация без watermark
- VPS crontab: failed syncs go to /dev/null or root mail nobody reads
- Full re-sync every run: re-fetching all records wastes API quota and increases sync time quadratically
- Missing idempotency: network failure mid-sync creates partial state and duplicate records on retry
- No run history: "did the sync run last night?" requires SSH and log search
Полный перезапрос при каждом прогоне: медленно, дорого, нагружает источник. При росте данных прогон начинает занимать больше, чем интервал cron.
Где костыли не спасают
Типичные ошибки при data sync
Обновлять `last_synced_at` до завершения записи: при ошибке записи курсор уже сдвинут — данные потеряны тихо.
Без idempotent upsert повторный прогон из-за ретрая cron создаёт дубликаты.
Как помогает Inquir
Watermark sync на Inquir
Cron-пайплайн читает `last_synced_at` из хранилища, запрашивает изменения после этой отметки, делает `UPSERT ON CONFLICT`, сдвигает курсор только после успешной записи.
История каждого прогона в консоли: сколько записей синхронизировано, где остановился курсор при ошибке.
Что получаете
Ключевые части watermark sync
Watermark курсор
Читать `last_synced_at` из БД/хранилища; запрашивать только записи после этой отметки.
Idempotent upsert
`INSERT … ON CONFLICT (id) DO UPDATE SET …` — безопасный повтор.
Атомарный сдвиг курсора
Сдвигать `last_synced_at` только после успешной записи всего батча.
Cron-расписание
Часовой или дневной прогон — полные cron-выражения с историей и повторами.
Что делать дальше
Как организовать watermark sync
Прочитать курсор
Запросить `last_synced_at` из базы или persistent storage в начале каждого прогона.
Запросить дельту
`WHERE updated_at > last_synced_at ORDER BY updated_at LIMIT N` — инкрементальный запрос.
Upsert и сдвиг курсора
Сделать upsert батча, затем обновить `last_synced_at = MAX(updated_at)` в одной транзакции.
Пример кода
Incremental sync with watermark cursor
Reads cursor from environment/DB, fetches delta, upserts idempotently, returns new cursor. Cron trigger fires on schedule; retries on failure.
export async function handler(event) { // Read watermark — default to 24h ago on first run const cursor = await db.syncCursors.get('crm-contacts') ?? new Date(Date.now() - 86_400_000).toISOString(); let page = 0, synced = 0, newCursor = cursor; do { const { contacts, nextPage } = await crm.fetchContacts({ updatedAfter: cursor, page }); if (contacts.length === 0) break; await db.contacts.upsertBatch(contacts); // upsert by contacts[].externalId synced += contacts.length; newCursor = contacts.at(-1)?.updatedAt ?? newCursor; page = nextPage; } while (page); await db.syncCursors.set('crm-contacts', newCursor); return { synced, cursor, newCursor }; }
Когда подходит
Когда нужен watermark sync
Когда это уместно
- Источник поддерживает фильтрацию по `updated_at`
- Нужна инкрементальная синхронизация без полного перезапроса
Когда лучше не трогать
- Источник не поддерживает фильтрацию по времени — нужна полная выгрузка с hash-сравнением
FAQ
Вопросы и ответы
Что если прогон упал на середине?
Cron запустит следующий прогон с тем же `last_synced_at`: данные запросятся снова, upsert безопасен — дублей не будет.
Как хранить cursor?
Отдельная таблица `sync_state` или key-value хранилище; одна строка на источник.