🔄

Sincronização de Episódios

Sincronização incremental de episódios

Objetivos operacionais

Fluxo

O cron api-sync-episodes-daily mantém o identificador por compatibilidade, mas
corre a cada minuto. O PodcastSyncDispatcherService consulta no máximo 500
podcasts cujo next_episode_sync_at venceu, ordena pelo vencimento e adquire um
lease atómico de 10 minutos. Só o processo que obteve o lease cria o job
sync-episodes-feed; uma falha no enqueue liberta o lease e torna o feed
imediatamente elegível.

Cada job contém podcastId, leaseToken e o intervalo seguinte. Favoritos usam
prioridade 1 e 15 minutos; os restantes usam prioridade 5 e 60 minutos. O Worker
processa até 10 jobs em paralelo e o limiter BullMQ é global, 120 feeds/minuto.
jobId inclui podcast e lease, impedindo duplicação da mesma tentativa.

Abrir, pesquisar ou favoritar um podcast stale tenta adquirir o mesmo lease e
enfileira um refresh em background. A resposta pública continua a usar os dados
existentes. GET /podcasts/:id/episodes mantém o body compatível e acrescenta:

Fetch e persistência

Os URLs guardados dos feeds e os das páginas seguintes só são buscados em endereços públicos; um URL bloqueado fica na categoria blocked_url, sem nova tentativa na mesma execução; o limite por feed é 30 MiB.

O primeiro pedido RSS envia If-None-Match e If-Modified-Since. Um 304
atualiza tentativa, sucesso, estado e próximo sync, sem parsing nem escritas de
episódios. Em 200, GUIDs string válidos geram dedupe_key=guid:<guid>; sem GUID,
a chave é enclosure:<sha256-url>. O Worker carrega as chaves candidatas numa
única query limitada e usa bulkWrite apenas para episódios novos ou metadados
alterados. A unicidade é {podcast_id,dedupe_key} com filtro parcial.

Timeouts, 408, 429 e 5xx têm três tentativas, backoff exponencial e jitter;
Retry-After tem precedência. 404/410 só marcam feed_dead e estado
unavailable após três execuções. Tentativas falhadas nunca atualizam
last_episode_sync_success_at.

Histórico e backfill

backfill-podcast-history é uma fila separada, concorrência 1, prioridade 20 e
limite de 10 páginas/minuto. O cursor RFC5005 fica em
history_backfill_next_url depois de cada página, portanto restart retoma do
checkpoint. Na última página, o Worker consulta até 1000 itens do Podcast Index
quando existe podcast_index_id. Se o total conhecido continuar superior ao
armazenado, o estado final é source_limited; caso contrário é complete.

A origem pode retirar episódios ou não publicar arquivo/paginação. Nesse caso a
Audissey não inventa cobertura: source_limited torna a limitação explícita.

Campos do podcast

last_episode_sync_attempt_at, last_episode_sync_success_at,
next_episode_sync_at, episode_sync_lease_token,
episode_sync_lease_until, episode_sync_etag,
episode_sync_last_modified, episode_sync_status, episode_sync_error,
consecutive_episode_sync_failures, latest_episode_publish_date,
last_episode_sync_duration_ms,
history_backfill_status, history_backfill_next_url,
history_backfill_started_at, history_backfill_completed_at e
history_earliest_publish_date.

Administração e alertas

Rollout

  1. Preencher as variáveis dos três Variable Groups descritas em
    docs/setup/ENVIRONMENTS.md.
  2. Fazer deploy da migração 20260804_011 e executá-la no Admin. Confirmar que o
    TTL desapareceu e o índice GUID é parcial.
  3. Fazer deploy de API/Worker/Admin, mantendo o dispatcher cron desativado.
  4. Usar POST /admin/podcasts/:id/sync para Prova Oral e Bitalk. Confirmar
    respetivamente episódios de 31 de julho e 28 de julho, e zero duplicados na
    segunda execução.
  5. Ativar o cron em staging durante 24 horas. Exigir cobertura >=99,9%, nenhum
    favorito acima de 15 minutos e queue age dentro da capacidade.
  6. Só então ativar dev/prod. O antigo job global deixa de ser agendado; triggers
    manuais sem podcastId permanecem temporariamente limitados a 500 feeds.

Referência: Podcasting 2.0 — recomendações de arquivo e paginação.