Prevenindo Transações PDO Órfãs e Deadlocks em Workers PHP
No PHP-FPM tradicional, se uma requisição fosse interrompida por uma exceção não tratada, timeout ou erro fatal dentro de uma transação de banco de dados ($pdo->beginTransaction()), o processo PHP morria e o próprio servidor de banco de dados executava o rollback automático ao fechar o socket TCP.
Em runtimes PHP persistentes (FrankenPHP, Laravel Octane, RoadRunner, Swoole), a conexão com o banco de dados é mantida aberta entre as requisições.
Se uma transação aberta não for explicitamente confirmada (commit()) ou revertida (rollBack()), a conexão PDO volta para o pool de conexões ainda em estado transacional ($pdo->inTransaction() === true).
O Perigo: Como Transações Órfãs Travam a Produção
Requisição 1 (Checkout):
├── $pdo->beginTransaction();
├── UPDATE estoque SET saldo = saldo - 1 WHERE id = 42; (Linha Bloqueada!)
└── throw new PagamentoRecusadoException(); (Requisição encerra sem rollback!)
└── Conexão nº 1 retorna ao pool mantendo o LOCK ativo no banco!
Requisição 2 (Outro usuário acessa a Home):
├── Reutiliza a Conexão nº 1
├── SELECT * FROM produtos;
└── $pdo->beginTransaction(); ➔ ERRO FATAL: "There is already an active transaction"!
└── Ou queries travam esperando o Lock nº 42 ➔ Esgotamento do Pool do Banco de Dados!Por Que Blocos Try/Catch Manuais Não São Suficientes
Mesmo com boas práticas, transações podem ficar abertas devido a:
- Instruções
returnantecipadas que pulam a linha docommit(). - Pacotes de terceiros que abrem transações aninhadas e não tratam exceções internas.
- Erros de rede/desconexão temporária durante a confirmação da transação.
- Erros não capturáveis ou memory limits dentro de blocos críticos.
Como o TransactionGuard do Leakless Resolve Automaticamente
O Leakless possui um TransactionGuard nativo que executa no bloco finally ao término de cada requisição.
Ciclo da Requisição
│
▼
$leakless->startRequest()
│
▼
Lógica da Aplicação (Processa HTTP / Job)
│
▼
finally {
$leakless->endRequest()
│
▼
O TransactionGuard audita todas as conexões PDO ativas:
├── $pdo->inTransaction() === true?
├── SIM ➔ Executa $pdo->rollBack() imediatamente!
└── Registra log de diagnóstico com detalhes!
}Configuração & Uso
A auditoria de transações vem ativada por padrão no Leakless:
use TheMattos\Leakless\DTOs\Config;
use TheMattos\Leakless\Leakless;
$config = new Config(
checkTransactions: true, // Ativado por padrão
);
$leakless = new Leakless($config);
// No Laravel Octane: o Leakless audita automaticamente todas as conexões do DatabaseManager.
// No PHP Vanilla: registre instâncias PDO customizadas no boot:
$leakless->registerConnection($pdo);Quando uma transação órfã é interceptada, o Leakless executa o rollback defensivo e emite um log estruturado:
[Leakless] 🚨 Dangling database transaction(s) detected and rolled back (1 transaction(s)).