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 emapps/api/api/sync/ (motor), cache.py (Cache-Control) e nas
service.py de cada feature, onde ficam os TTLs.
Visão geral
- A rota chama um service, que lê o cache do banco (Postgres) com
Sync.resolve. - Cache dentro do TTL: responde na hora, sem tocar no SIGAA.
- Cache vencido: responde na hora com o que tem e agenda um job no QStash para atualizar (stale-while-revalidate).
- Sem cache (ou o cliente exigiu revalidar): busca no SIGAA antes de responder, com a sessão do próprio usuário.
TTLs
O TTL conta a partir dosynced_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-agevencido). 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).
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.
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 comno-cachenem 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
refreshdo cliente faz isso). only-if-cached: nunca vai ao SIGAA na requisição (504sem cache).- Outro usuário sincronizou a mesma turma há pouco: o
synced_atcompartilhado 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
401e 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/classroomssempre vão à origem;/me/settingssó 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 peloSync: 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.