Перехват NOTICE в PostgreSQL при вызове хранимых процедур
При разработке на PostgreSQL часто возникает необходимость отслеживать статус выполнения хранимой процедуры в реальном времени, а не после её завершения. Стандартный способ - использовать команду RAISE NOTICE, но перехватить эти сообщения через драйвер pyodbc не так просто. Разберём, как решить эту задачу.
Почему NOTICE не приходят мгновенно?
PostgreSQL отправляет NOTICE-сообщения клиенту только после завершения текущего оператора или транзакции. При вызове процедуры через pyodbc драйвер накапливает все сообщения и передаёт их приложению после завершения вызова. Это делает невозможным получение промежуточных статусов в реальном времени без дополнительных ухищрений.
Методы перехвата NOTICE в реальном времени
1. Использование LISTEN/NOTIFY
Самый надёжный способ - механизм LISTEN и NOTIFY. В хранимой процедуре отправляйте уведомления через NOTIFY channel_name, 'статус'. В приложении запустите параллельный поток, который слушает канал через LISTEN channel_name и обрабатывает сообщения по мере поступления. В pyodbc это реализуется через отдельное соединение с вызовом connection.execute('LISTEN channel_name') и последующим опросом connection.poll() или использованием select.
2. Запись статусов в таблицу
Если архитектура позволяет, пишите промежуточные статусы в отдельную таблицу логов. Хранимая процедура вставляет строки с временными метками и кодом статуса. Приложение параллельно читает эту таблицу через другое соединение. Недостаток - дополнительная нагрузка на базу данных и задержка на чтение.
3. Использование dblink или pg_background
Расширение dblink позволяет процедуре отправлять NOTICE в другое соединение. Однако это сложно в настройке и не рекомендуется для production. pg_background - ещё один экспериментальный вариант, но он требует прав суперпользователя.
Практический пример с pyodbc и LISTEN/NOTIFY
Рассмотрим базовую реализацию. Создадим канал уведомлений:
CREATE OR REPLACE FUNCTION my_procedure() RETURNS void AS $$
BEGIN
PERFORM pg_notify('status_channel', 'Начало работы');
-- ... долгая операция ...
PERFORM pg_notify('status_channel', 'Этап 1 завершён');
-- ... ещё операция ...
PERFORM pg_notify('status_channel', 'Готово');
END;
$$ LANGUAGE plpgsql;В Python с pyodbc запускаем слушатель в отдельном потоке:
import pyodbc
import threading
import time
def listener():
conn = pyodbc.connect('DSN=my_dsn')
conn.execute('LISTEN status_channel')
conn.commit()
while True:
conn.execute('SELECT 1') # триггер для получения уведомлений
notifications = conn.poll()
if notifications:
for n in notifications:
print(f'Получено: {n.payload}')
time.sleep(0.1)
threading.Thread(target=listener, daemon=True).start()
# основной вызов процедуры
main_conn = pyodbc.connect('DSN=my_dsn')
main_conn.execute('CALL my_procedure()')
main_conn.commit()Этот подход позволяет получать NOTICE в реальном времени без изменения драйвера.
Ограничения и альтернативы
Если вы не можете использовать LISTEN/NOTIFY (например, из-за сетевых ограничений), рассмотрите вариант с временными таблицами или очередями сообщений на стороне приложения. В некоторых СУБД (например, PostgreSQL 14+) появилась поддержка pg_logical_emit_message, но она требует расширения pglogical. Для простых сценариев достаточно таблицы логов с периодическим опросом.
Заключение
Перехват NOTICE во время выполнения хранимой процедуры через pyodbc возможен только с помощью асинхронных механизмов вроде LISTEN/NOTIFY. Прямой перехват сообщений RAISE NOTICE в реальном времени не поддерживается драйвером. Выберите подходящий метод в зависимости от сложности задачи и требований к производительности.