Skip to main content

Cache, TTL e sincronização da API

Resumo das regras que decidem quando a API serve o cache, quando vai ao SIGAA e quando não sincroniza. O código vive em apps/api/api/sync/ (motor), cache.py (Cache-Control) e nas service.py de cada feature, onde ficam os TTLs.

Visão geral

  1. A rota chama um service, que lê o cache do banco (Postgres) com Sync.resolve.
  2. Cache dentro do TTL: responde na hora, sem tocar no SIGAA.
  3. Cache vencido: responde na hora com o que tem e agenda um job no QStash para atualizar (stale-while-revalidate).
  4. Sem cache (ou o cliente exigiu revalidar): busca no SIGAA antes de responder, com a sessão do próprio usuário.
Nada roda depois da resposta: os jobs são publicados no QStash antes dela. Cada job loga numa sessão própria do SIGAA com a credencial cifrada, roda as tarefas e faz logout. A sessão do usuário nunca é usada num job.

TTLs

O TTL conta a partir do synced_at do dado e vence quando idade > TTL (estritamente maior). Sem TTL (None), o dado nunca vence sozinho. “Turma atual” é a que o SIGAA marca como do semestre vigente (ClassroomParticipant.current). Dados por turma (participantes, estatísticas, notícias) são compartilhados entre os usuários: se outro aluno da mesma turma sincronizou há pouco, o seu cache também está fresco. A frequência é por usuário.

Cache-Control do cliente

O cliente só consegue ser mais exigente que o servidor, nunca menos: os TTLs acima continuam valendo. Respostas com cache saem com Cache-Control: private, no-cache e Age. Rotas sem cache saem com no-store.

Quando a API sincroniza

Na requisição (antes de responder)

  • Não há cache do dado.
  • O cliente pediu (no-cache, max-age vencido). Aqui a lista de turmas também regrava as turmas de semestres passados (refresh).
  • Uma turma que não está no cache do usuário é pedida: a lista de turmas é relida na hora. Se mesmo assim a turma não aparece → 404.
  • Cardápio do RU vencido (ou intervalo pedido sem nenhum dia em cache).
Se a busca falhar e houver cache aceito pelo stale-if-error, ele é servido. Senão o erro sobe (502 para falha da origem).

Em segundo plano (job no QStash)

  • O dado foi servido vencido (idade > TTL): agenda a tarefa daquela turma, ou da conta.
  • A lista de turmas está vencida ao abrir qualquer turma: agenda o sync da conta.
  • Login (POST /auth/sigaa) e refresh (POST /auth/sigaa/refresh, chamado ao abrir o app): se o usuário não existe, ou perfil/lista de turmas/participantes/estatísticas estão vencidos, agenda o sync da conta. O job da conta atualiza perfil e lista de turmas vencidos e então agenda um job por turma com participantes ou estatísticas vencidos.
O login só pré-aquece perfil, turmas, participantes e estatísticas. Frequência e notícias só sincronizam quando alguém as consulta. Um job por turma leva todas as tarefas vencidas dela; tarefas repetidas na mesma requisição entram uma vez. O job roda as tarefas sem rechecar o TTL.

Quando a API não sincroniza

  • Cache dentro do TTL e o cliente não pediu para revalidar.
  • Dado sem TTL: conteúdo de notícia, e participantes, estatísticas, frequência e lista de notícias de turmas de semestres passados. Só são relidos com no-cache.
  • Regras do calendário acadêmico (api/academic_calendar.py): passados 3 dias do fim do semestre da turma, os participantes vencem até um último sync. Depois dele, nunca são ressincronizados, nem com no-cache nem no login. Semestres anteriores ao calendário já contam como encerrados.
  • Turmas passadas na lista: o job de fundo não regrava turmas de semestres passados que já existem (só um refresh do cliente faz isso).
  • only-if-cached: nunca vai ao SIGAA na requisição (504 sem cache).
  • Outro usuário sincronizou a mesma turma há pouco: o synced_at compartilhado ainda está fresco.
  • Job duplicado: o QStash descarta o mesmo job (mesma matrícula, turma e tarefas) publicado em até 10 min. Pedidos seguidos de um dado vencido não geram uma fila de jobs.
  • Falha ao agendar: se o QStash não responder em 3 s ou recusar, o erro é só logado e nenhum job existe; o dado segue vencido até o próximo acesso agendar de novo.
  • Senha recusada no job: o job é descartado em silêncio (sem retentativa). Na requisição vira 401 e apaga os cookies.
  • Turma saiu da lista do usuário depois de agendada: o job é descartado.
  • Turma ou notícia sumiu do SIGAA (ClassroomNotFound, NewsNotFound) ou gravação concorrente (IntegrityError): a tarefa é descartada, sem retentativa.
  • Rotas sem cache, que não sincronizam porque nada é guardado: /me/ru-statement, /me/ru-token, /posts (home do SIGAA) e /public/classrooms sempre vão à origem; /me/settings só usa o banco.

Falhas e retentativas

Os jobs de um mesmo usuário rodam no máximo 4 em paralelo (flow_control do QStash), para poupar o CAS.

Cardápio do RU (sem job)

Não passa pelo Sync: o cardápio é público e vem do site da UnB, sem login.
  • Cache por campus, TTL de 5 dias. Vencido, o cardápio inteiro do campus é baixado e gravado na hora.
  • Todos os dias do período informado no link são salvos; dias sem refeições têm os campos null.
  • Intervalo pedido sem nenhum dia em cache vai ao site mesmo com o campus fresco.
  • Site fora do ar: se houver cache do intervalo, serve-o (TTL vencido) ou só com stale-if-error (se o cliente forçou a revalidação). Sem cache do intervalo → 502.
  • Erro ao ler ou gravar o cache no banco só é logado: a resposta sai do site.

Cache no navegador

O front tem outra camada, independente: o TanStack Query persiste os dados da API por 14 dias para o modo offline. Veja offline.md.

Posts das turmas

ClassroomPosts (classroom_posts) reúne notícias, tópicos de aula, materiais e agenda de avaliações. source_id identifica o post dentro da turma, type indica news, topic, material ou assessment, e a chave estrangeira composta de topic_id impede vincular um material a um tópico de outra turma. GET /classrooms/{classroom_id}/posts aceita ?type=material (ou outra categoria). GET /classrooms/{classroom_id}/posts/{post_id} usa o id opaco da listagem. /posts é o feed recente da home do SIGAA (notícias e avaliações de todas as turmas atuais), sem cache e sem IDs. sync_posts atualiza a lista completa a cada 60 minutos nas turmas atuais; listas históricas não vencem. Tópicos, materiais e avaliações atualizam seus conteúdos junto com a lista. Notícias mantêm o carregamento de conteúdo sob demanda por sync_news_content, com revalidação por Cache-Control: no-cache. Posts removidos da origem são preservados, assim como os conteúdos de notícias já consultados. Datas de publicação ausentes continuam nulas; a agenda usa campos próprios para data e horário. Resultados individuais ficam fora dos posts. Não há migração do cache antigo: conforme a convenção do projeto, a mudança de classrooms.news_synced_at para posts_synced_at exige recriar o banco existente.