current transaction is aborted, commands ignored until end of transaction block означает, что один из предыдущих запросов в той же транзакции уже завершился ошибкой. PostgreSQL пометил транзакцию как проваленную и отклоняет последующие команды. SQLSTATE 25P02 описывает это состояние, но не называет исходную причину.
Поэтому исправлять запрос, на котором показался 25P02, обычно бесполезно. Сначала найдите первое сообщение об ошибке в этой транзакции, затем выполните ROLLBACK. Если сбой ожидался и локальное восстановление было предусмотрено заранее, откатитесь к SAVEPOINT.
25P02 сообщает о более ранней ошибке
25P02 появляется на втором запросе, но причина находится в первом: откат завершает проваленную транзакцию, после чего исправленную операцию запускают заново.Минимальное воспроизведение 25P02
Откройте явную транзакцию и выполните запрос, который делит на ноль:
BEGIN;
SELECT 1 / 0;
ERROR: division by zero
У деления на ноль SQLSTATE 22012. Это исходная ошибка. Транзакция после неё остаётся открытой, но переходит в состояние ошибки. Любая обычная команда внутри того же блока получает уже другое сообщение:
SELECT 42;
ERROR: current transaction is aborted, commands ignored until end of transaction block
Код второго сообщения — 25P02, символьное имя в документации PostgreSQL — in_failed_sql_transaction. Сам SELECT 42 исправен: сервер даже не пытается его выполнить. Завершите проваленную транзакцию явно:
ROLLBACK;
После этого соединение снова принимает команды. Если первый запрос упал из-за опечатки в имени объекта, порядок чтения самого сообщения разобран в статье про relation does not exist; синтаксические ошибки — в отдельном разборе.
Почему 25P02 скрывает полезную ошибку
Приложение нередко пишет в лог только последнее исключение. Например, вставка нарушила UNIQUE, обработчик поймал исключение и попытался записать аудит той же транзакцией. Запись аудита получила 25P02, а общий обработчик сохранил именно её. В итоге дежурный видит следствие вместо 23505 unique_violation.
Ищите первую ошибку до 25P02 в том же соединении и транзакции. Это может быть 23505 для уникального ограничения, 22012 для деления на ноль, 42P01 для отсутствующей таблицы или другой SQLSTATE. Последующие 25P02 можно считать шумом до момента отката.
В коде исходное исключение надо журналировать там, где оно поймано, до попыток очистки и повторных SQL-команд. Полезный минимум записи: SQLSTATE, идентификатор запроса приложения, операция и стек исключения. Значения параметров добавляйте только с учётом секретов и персональных данных.
Как найти исходную ошибку в журнале PostgreSQL
Серверный лог помогает, когда приложение потеряло первое исключение. Для разбора нужно связать соседние записи одного сеанса. PostgreSQL позволяет добавить в log_line_prefix идентификатор сеанса %c, PID %p, виртуальную транзакцию %v, имя приложения %a и SQLSTATE %e:
log_line_prefix = '%m [%p] session=%c vxid=%v app=%a db=%d user=%u sqlstate=%e '
log_min_error_statement = 'ERROR'
log_min_error_statement = 'ERROR' записывает SQL-команду, вызвавшую ошибку. Значение ERROR используется по умолчанию, но на конкретном сервере настройку могли изменить. Формат csvlog или jsonlog удобнее обычного текста: в нём SQLSTATE, сообщение, команда и application_name лежат в отдельных полях.
Порядок поиска такой:
- Найдите запись с SQLSTATE
25P02и зафиксируйтеsession,vxid, PID и время. - Отфильтруйте лог по тому же сеансу и виртуальной транзакции.
- Двигайтесь назад до первого
ERRORс другим SQLSTATE. ЕгоSTATEMENT,DETAILиHINTописывают причину.
PID можно переиспользовать после завершения процесса, поэтому одного %p в длинном журнале недостаточно. Идентификатор сеанса %c отделяет новое подключение, а %v помогает не спутать две последовательные транзакции в одном сеансе. Если в префиксе этих полей не было, сопоставляйте короткое окно времени с application_name, пользователем и базой, а затем добавьте идентификаторы на будущее.
ROLLBACK завершает проваленную транзакцию
После исходной ошибки обычные запросы не исправляют состояние. Выполните ROLLBACK, устраните причину и повторите всю бизнес-операцию в новой транзакции. Все изменения, сделанные после BEGIN, будут отменены, включая успешные команды до сбоя.
COMMIT не надо использовать как команду восстановления. Он выражает намерение зафиксировать успешную транзакцию, но проваленную транзакцию PostgreSQL завершит без фиксации её изменений. В обработчике ошибки нужен явный ROLLBACK и исключение наружу; иначе вызывающий код может принять отменённую операцию за выполненную.
Автоматический повтор тоже подходит не для всех причин. Конфликт сериализации или взаимная блокировка допускают повтор всей транзакции по заданной политике. Нарушение уникальности, неверное имя колонки или деление на ноль повтором не лечатся: сначала исправьте данные или запрос.
ROLLBACK TO SAVEPOINT оставляет внешнюю транзакцию живой
Если одна операция может законно не выполниться, создайте точку сохранения до рискованной команды. ROLLBACK TO SAVEPOINT отменит изменения после этой точки и вернёт внешнюю транзакцию в рабочее состояние:
BEGIN;
CREATE TEMP TABLE demo_users (email text UNIQUE);
INSERT INTO demo_users VALUES ('ann@example.com');
SAVEPOINT optional_row;
INSERT INTO demo_users VALUES ('ann@example.com');
ERROR: duplicate key value violates unique constraint "demo_users_email_key"
DETAIL: Key (email)=(ann@example.com) already exists.
После локального отката транзакция снова принимает команды:
ROLLBACK TO SAVEPOINT optional_row;
RELEASE SAVEPOINT optional_row;
INSERT INTO demo_users VALUES ('boris@example.com');
COMMIT;
В таблице останутся строки ann@example.com и boris@example.com; неудачная повторная вставка отменена. ROLLBACK TO сохраняет саму точку, поэтому RELEASE SAVEPOINT после восстановления явно закрывает её.
Точка сохранения не чинит произвольный уже случившийся сбой задним числом. Если SAVEPOINT не был создан до ошибки, остаётся полный ROLLBACK. Не оборачивайте каждую команду в savepoint без причины: это механизм для ожидаемой локальной ошибки, а не замена нормальной проверке данных и обработке исключений.
try/except/rollback в Psycopg 3
Psycopg по умолчанию начинает транзакцию при первой операции с базой. После ошибки тот же объект Connection нельзя использовать, пока код не вызовет rollback(). Минимальный обработчик сохраняет исходное исключение и гарантированно завершает транзакцию:
import psycopg
def mark_order_paid(conn: psycopg.Connection, order_id: int) -> None:
try:
conn.execute(
"UPDATE orders SET status = %s WHERE id = %s",
("paid", order_id),
)
conn.commit()
except Exception:
conn.rollback()
raise
Широкий except Exception здесь намеренный: если Python-код упадёт между несколькими SQL-командами, незавершённую транзакцию тоже надо откатить. После rollback() исходное исключение поднимается снова, поэтому верхний уровень не сообщит об успехе. Параметры передаются отдельно от SQL-строки, а не склеиваются с ней.
Если rollback() сам не проходит из-за разорванного соединения, это соединение больше нельзя считать пригодным: его закрывают или отбрасывают, а не возвращают в ручной пул как исправное.
Почему aborted connection опасен для пула
Пул выдаёт одно физическое соединение разным запросам приложения. Если обработчик проглотил первую ошибку и вернул соединение в состоянии INERROR, следующий запрос получит чужой 25P02. По логам это выглядит как новый сбой в безобидном SELECT, хотя причина осталась в предыдущем HTTP-запросе или задаче очереди.
Для Psycopg безопаснее использовать контекст пула:
from psycopg_pool import ConnectionPool
pool = ConnectionPool(conninfo="postgresql://app@db/app")
with pool.connection() as conn:
conn.execute(
"UPDATE orders SET status = %s WHERE id = %s",
("paid", 42),
)
При нормальном выходе pool.connection() фиксирует открытую транзакцию, при исключении откатывает её, после чего возвращает соединение в пул. Если соединение сломано, Psycopg Pool отбрасывает его и создаёт замену. У ручной пары getconn()/putconn() нет контекста, который выбирает commit() или rollback() по результату блока: приложение должно завершить транзакцию явно до putconn().
Короткий порядок действий
- Зафиксируйте SQLSTATE
25P02, идентификатор запроса приложения и соединения. - Найдите первое предшествующее исключение той же транзакции.
- Выполните полный
ROLLBACK; используйтеROLLBACK TO SAVEPOINTтолько при заранее созданной точке. - Исправьте причину и повторите бизнес-операцию целиком, если повтор для неё безопасен.
- Перед возвратом соединения в пул убедитесь, что транзакция завершена; сломанное соединение отбросьте.
Запрос с 25P02 почти никогда не тот запрос, который сломал транзакцию. Исходная ошибка стоит выше в логе.