1

Sobre o "não basta transformar qualquer exceção em horário ocupado": dá pra deixar isso objetivo olhando o SQLSTATE e o nome da restrição, nunca a mensagem:

  • 23505 (unique_violation) vindo de uma_reserva_ativa_por_vaga vira 409 (no índice parcial, o constraint_name do erro traz o nome do próprio índice)
  • 23P01 (exclusion_violation) vindo da restrição de sobreposição vira 409
  • o resto continua 500 (fora o 40P01 abaixo)

Um detalhe do EXCLUDE que pouca gente conhece: diferente do índice único, ele grava a entrada no índice e só depois varre procurando conflito. Se duas transações inserirem quase juntas, pode acontecer de cada uma enxergar a linha não commitada da outra: as duas ficam esperando e o detector de deadlock aborta uma com 40P01, não 23P01. Vale tratar com retry da transação inteira (ela já foi abortada): na nova tentativa, a reexecução espera a outra terminar e cai no 23P01 normal (ou passa, se a outra deu rollback).

Pra testar sem sleep fixo: duas conexões, faz o insert na A sem commitar e dispara o da B em outra thread, que fica bloqueada esperando a A terminar. Faz polling até pg_blocking_pids(pid_da_b) conter o pid da A, commita a A e confere que a B recebeu 23505 e que existe exatamente uma reserva ativa. Repetindo com ROLLBACK na A, a B tem que passar.

Carregando publicação patrocinada...