Inquir Compute logoInquir Compute
Сценарий · Inquir Compute

Scheduled data sync: инкрементальная синхронизация по watermark-курсору

Cron-задача каждый час: запросить изменения после `last_synced_at`, сделать idempotent upsert, обновить курсор. Без полного перезапроса, без дублей.

Last updated: 2026-04-20

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 создаёт дубликаты.

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

1

Прочитать курсор

Запросить `last_synced_at` из базы или persistent storage в начале каждого прогона.

2

Запросить дельту

`WHERE updated_at > last_synced_at ORDER BY updated_at LIMIT N` — инкрементальный запрос.

3

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.

jobs/sync-crm-contacts.mjs
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-сравнением

Вопросы и ответы

Что если прогон упал на середине?

Cron запустит следующий прогон с тем же `last_synced_at`: данные запросятся снова, upsert безопасен — дублей не будет.

Как хранить cursor?

Отдельная таблица `sync_state` или key-value хранилище; одна строка на источник.

Inquir Compute logoInquir Compute

Самый простой способ запускать AI-агентов и backend-джобы без инфраструктуры.

Связаться info@inquir.org

© 2025 Inquir Compute. Все права защищены.