Desafios de Escala: Transformando uma API de "Sessões Espalhadas" para Transações Atômicas no FastAPI
No desenvolvimento de APIs de alta complexidade, o gerenciamento de estados e conexões com banco de dados costuma ser o primeiro ponto de gargalo. Em nosso projeto recente, enfrentamos um cenário crítico: uma arquitetura de dados acoplada, instável e que sofria de inconsistências transacionais graves. Este artigo detalha como diagnosticamos e resolvemos esses problemas, estabelecendo um padrão de Unit of Work e injeção de dependência compartilhada.
1. O Diagnóstico: O "Inferno" das Sessões
Inicialmente, a nossa API apresentava sintomas de uma arquitetura que havia crescido sem um padrão transacional definido. Os principais pontos de falha eram:
Acoplamento Exacerbado: Os repositories detinham a responsabilidade de abrir, fechar e realizar commits, criando uma dependência cíclica e dificultando a testabilidade.
Proliferação de Sessões: Devido a uma implementação incorreta das dependências no FastAPI, o sistema criava instâncias de sessão distintas para o mesmo ciclo de requisição. Se a API precisava de dois repositórios, ela abria duas conexões diferentes para o mesmo banco.
Ausência de Atomicidade: Como as sessões não eram compartilhadas, um commit em um repositório não garantia a persistência do outro. Isso gerava cenários onde metade de uma operação era gravada (ex: histórico) enquanto a outra falhava (ex: status da instrução), além de constantes Deadlocks (travamentos) causados pela briga entre diferentes conexões tentando acessar o mesmo registro.
2. A Estratégia de Solução
Para resolver esse problema, redesenhamos o fluxo de dados em dois pilares fundamentais:
A. Centralização via Injeção de Dependência Compartilhada
Esta implementação encapsula a complexidade de decidir entre instâncias de banco (Leitura/Escrita) e garante que o ciclo de vida da sessão seja respeitado, independentemente do sucesso ou falha da operação:
def _get_session(db_name: str, is_write: bool):
# Roteamento inteligente de conexão
if db_name == "db_name_A":
db_instance = get_db_name_A_write() if is_write else get_db_name_A_read()
else: # db_name == "db_name_B"
db_instance = get_db_name_B_write() if is_write else get_db_name_B_read()
session = db_instance.get_session()
try:
yield session
except Exception:
# Rollback automático em caso de erro na transação
if is_write:
session.rollback()
raise
finally:
# Garantia de liberação do recurso (Connection Pooling)
session.close()
Ao centralizar o provimento da sessão, garantimos que todos os repositórios (seja o de Instrução ou o de Histórico) operassem exatamente sobre a mesma instância do Session do SQLAlchemy. Essa mudança foi fundamental porque operações de escrita, como UPDATE, aplicam bloqueios (locks) nas tabelas do SQL Server; esses bloqueios permanecem ativos e são liberados apenas quando a sessão é finalizada com um commit. Ao alinhar as sessões em um único contexto transacional, passamos a gerenciar a concorrência e a atomicidade de forma precisa, eliminando os conflitos de lock que, anteriormente, causavam o travamento das nossas operações de banco de dados.
Contudo, é fundamental um alerta sobre a gestão do ciclo de vida dessas transações:
Embora a sessão única resolva a atomicidade, ela exige disciplina no design do código. Como o commit só ocorre ao final de todo o fluxo, mantemos uma transação aberta durante toda a execução do método. Para mitigar riscos de concorrência, adotamos três diretrizes rigorosas:
Operações pesadas fora da transação: Validamos regras de negócio, formatações de mensagem e chamadas de APIs externas antes de iniciar a lógica que exige escrita no banco.
O princípio do 'Open Late, Close Early': A transação só é efetivamente iniciada quando o repositório realiza a primeira operação de escrita. Mantemos a lógica dentro do try o mais enxuta possível para reduzir ao mínimo o tempo de retenção dos locks.
Evitar a escalada de locks: Operações de escrita em massa ou que tocam muitas linhas devem ser evitadas dentro da transação principal, pois o banco de dados pode elevar o nível de bloqueio (de nível de linha para nível de tabela ou página), impactando outros processos que tentam acessar o banco simultaneamente.
B. Implementação de Atomicidade (Unit of Work)
Removemos a responsabilidade de commit dos repositórios. O repositório tornou-se "burro" — ele apenas executa operações. A decisão de persistir (o commit) foi movida para a camada de Service.
Isso permitiu o controle total da transação:
Validamos todas as regras de negócio.
Executamos as operações (procedimentos, updates).
Efetuamos o flush() para enviar as alterações ao banco.
Finalizamos com o commit() em ambos os bancos de forma orquestrada. Se qualquer um falhar, o rollback centralizado, gerenciado pelo gerador da sessão, reverte todo o bloco.
3. Resultados: Eficiência e Confiabilidade
Após a refatoração, o sistema apresentou melhorias quantitativas e qualitativas significativas:
Fim dos Deadlocks: Com a sessão única compartilhada, o banco de dados enxerga apenas uma transação por requisição. A disputa por recursos desapareceu.
Integridade dos Dados: O conceito de "tudo ou nada" foi restaurado. Não existem mais registros órfãos ou inconsistentes entre o banco de dados legado e o banco da API.
Código Limpo: A remoção de lógicas redundantes de controle de sessão nos repositórios reduziu a complexidade ciclomática da aplicação, facilitando a manutenção e a escrita de novos testes.
Performance: A redução na sobrecarga de abertura/fechamento de conexões desnecessárias otimizou o uso dos pools de conexão.
Conclusão
O desenvolvimento de APIs robustas exige um olhar atento ao ciclo de vida da transação. O que parecia ser apenas um problema de "código repetido" era, na verdade, uma falha arquitetural de gerenciamento de estado. Mover a gestão transacional para um nível superior, utilizando injeção de dependência de forma inteligente no FastAPI, não apenas resolveu nossos problemas técnicos imediatos, mas preparou a base para uma API que agora pode escalar com segurança e previsibilidade.
Dica para outros desenvolvedores: Se você notar que o ID das suas sessões de banco de dados muda entre chamadas de repositório na mesma requisição, pare imediatamente. Você está prestes a ter um deadlock em produção.