Persistência direta de progresso com fallback por rate limit
Superseded em 2026-08-06 por
ef6bc92: o progresso de aula passou a persistir de forma síncrona no ciclo de request/response, sem task Celery, sem rate limit e sem buffer no Redis. A regra vigente é R-001; o rate limit e o buffer seguem em uso apenas para áudio, meditação e live.
TLDR: Persistir o progresso da aula direto no banco em vez de sempre bufferizar no Redis, usando o buffer + garbage collector apenas como válvula de segurança quando um usuário envia o mesmo progresso mais de 3 vezes em 30s (clientes em versões antigas).
Contexto
Hoje todo POST /engagements/progress/lesson grava apenas no Redis (ProgressBufferService.store_latest_position) e um garbage collector (flush_pending_progress, Celery beat a cada 60s) drena os pendentes e persiste no Postgres. Esse desenho foi criado quando o app enviava progresso de 5 em 5 segundos, gerando sobrecarga no banco.
O app atual (Expo/RN) só envia progresso em 3 gatilhos — sair da tela (beforeRemove), app ir para background (AppState) e vídeo terminar (playToEnd). O envio periódico de 5s ainda existe no código do app, mas está morto (os callbacks não são ligados ao player). Ou seja: o volume de requisições hoje é baixíssimo e o buffer+GC deixou de ser necessário.
O único risco remanescente são usuários em versões antigas do app que ainda martelam o endpoint. Para esses casos, mantemos o buffer no Redis como proteção.
Objetivos
- Persistir progresso direto no banco no fluxo normal (sem esperar o GC de 60s)
- Bufferizar no Redis apenas quando um usuário enviar o progresso da mesma aula mais de 3 vezes numa janela de 30s (por
user_id+lesson_id) - Manter
flush_pending_progresse o Celery beat inalterados — passam a drenar somente o overflow dos clientes antigos
Fora de escopo
Sem mudanças de comportamento em flush_pending_progress nem em config/celery_defaults.py.
Mudanças
apps/engagements/services/progress.py(renomeado deprogress_buffer.py; classeProgressBufferService→ProgressService, já que o service passou a ser dono da regra, não só do buffer)- Constantes
RATE_LIMIT_MAX_HITS = 3eRATE_LIMIT_WINDOW_SECONDS = 30 - Método que registra um hit e informa se o limite foi excedido:
INCR progress:rate:{content_type}:{content_id}:{user_id}comEXPIREde 30s aplicado no primeiro hit (retornaTruequando o contador > 3) - Método
register_lesson_progress(user_id, lesson_id, position)com a regra de decisão (movida do serializer — serializer só valida, não tem regra):- Dentro do limite (≤ 3 em 30s): despachar persistência direta via
save_progress_lesson.delay(data)(task já existente), mantendo a request rápida e persistindo de imediato pela filaprogress - Acima do limite (> 3 em 30s): manter o comportamento anterior (
store_latest_position) para o GC drenar depois
- Dentro do limite (≤ 3 em 30s): despachar persistência direta via
- Constantes
apps/engagements/serializers.py—ProgressLessonSerializerfica só com validação (lesson_id,position); o métodoapplyfoi removidoapps/engagements/views.py—ProgressLessonView.postchamaProgressService.register_lesson_progressapós validar com o serializer- Demais usos do service (audios, lives, flush, tasks e testes) apenas atualizam import/nome para
ProgressService - Testes unitários em
tests/engagements/test_progress_service.py: novo método de rate limit (≤3 não excede, 4º hit excede, janela expira);register_lesson_progressno caminho direto (task despachada, nada bufferizado) e no caminho de overflow (bufferizado, task não despachada)
Como verificar
- Unit tests cobrindo os dois branches (direto vs overflow) e o rate limit
- Manual: enviar ≤3
POST /engagements/progress/lessonem 30s e conferir que oLessonProgressé atualizado quase imediatamente (via task), sem entrada no setprogress:pending_flush:lesson. Enviar >3 no mesmo período e conferir que, a partir do 4º, o progresso vai para o Redis e é persistido no próximo ciclo doflush_pending_progress
Documentação
- rules/engagements/lesson_progress_direct_synchronous_persist.md — R-001 (versão vigente, já com a persistência síncrona)