PostgreSQL: LISTEN/NOTIFY¶
LISTEN/NOTIFY в PostgreSQL передаёт сообщение из одного сеанса всем сеансам, которые слушают его
канал. У клиента есть listen() и notify(); NotificationListener — подписка, которая сама
переподключается.
Прослушивание: client.listen(channel, callback)¶
listen() есть у клиента любого драйвера (client = Connections.get(connection_alias)). Открывает
отдельное, не входящее в пул соединение, выполняет LISTEN на канале channel и вызывает
callback(connection, pid, channel, payload) — обычную синхронную функцию, не корутину — для каждого
полученного на нём NOTIFY. Возвращённый объект принадлежит вам — он никогда не берётся из пула и не
возвращается в него.
Однако два драйвера возвращают разные типы:
- Клиент
rust_pg(hare.dialects.postgresql.drivers.rust_pg) —client.listen(...)возвращаетpg.Listener. Его.is_closed()возвращаетTrueи после явногоawait listener.close(), и если соединение оборвалось само (обрыв сети, завершённый процесс сервера и т. п.) — он не просто запоминает, вызывали ли вы.close(). Если отбросить последнюю ссылку, не вызвав.close(), отдельное соединение всё равно закроется, когда сборщик мусора Python удалитListener, — это страховка от утечки соединения, а не замена явному.close()(момент сборки не гарантирован). - Клиент
asyncpg(hare.dialects.postgresql.drivers.asyncpg) —client.listen(...)возвращает обычныйasyncpg.Connectionс собственным.is_closed()изasyncpg. Отдельной обёрткиListenerу этого драйвера нет. Параметры подключения, которые относятся только к пулу (min_size,max_size,max_queries,max_inactive_connection_lifetime, ...), к этому отдельному соединению не применяются; неизвестный параметр даётConfigurationError.
В любом случае закройте возвращённый объект, когда слушать больше не нужно:
def on_notify(connection, pid, channel, payload):
print(f"got {payload!r} on {channel}")
client = Connections.get("default")
listener = await client.listen("book_returned", on_notify)
...
await listener.close()
Отправка: client.notify(channel, payload="")¶
Выполняет SELECT pg_notify($1, $2) с параметрами — канал и содержимое передаются как значения, поэтому
кавычки и экранирование не нужны. Есть и у клиента транзакции
(async with Transactions.atomic() as connection: await connection.notify(...)): там PostgreSQL ставит
уведомление в очередь и доставляет его только при фиксации транзакции — при откате оно не
доставляется никогда. Вне транзакции уведомление доставляется сразу.
client.features.supports_listen_notify равно True на обоих драйверах PostgreSQL и False в SQLite и
ClickHouse (там нет ни listen(), ни notify()).
Подписка, которая сама переподключается: NotificationListener¶
Соединение обычного client.listen(), которое оборвалось (завершённый процесс сервера, обрыв сети,
отключение по простою), так и остаётся мёртвым. hare.contrib.notify.NotificationListener оборачивает
его сторожем и переподключается:
from hare.contrib.notify import NotificationListener
async def on_book_returned(payload: str) -> None: # обычный `def` тоже подойдёт
...
listener = NotificationListener(
"default", # имя подключения
"book_returned", # канал
on_book_returned, # callback(payload: str), синхронный или асинхронный
reconnect_attempts=None, # None — пытаться бесконечно; N — сдаться после N неудачных попыток
backoff=0.5, # основа паузы backoff * 2**номер_попытки или явная последовательность пауз
max_backoff_seconds=30.0, # наибольшая одна пауза; None — без ограничения
)
await listener.start()
...
await listener.stop()
или async with NotificationListener(...) as listener: ....
start()запускает фоновую задачу и возвращается, когда закончилась первая попыткаLISTEN(подключились или не удалось и теперь повторяем в фоне). Повторный вызов ничего не меняет. Задача всегда использует общий клиент подключения, даже еслиstart()вызван внутри транзакции.stop()отменяет эту задачу, закрывает отдельное соединение и отменяет ещё работающие асинхронные функции-обработчики, дожидаясь всего этого. Повторный вызов ничего не меняет.run()— сам цикл; дождитесь его черезawait, чтобы выполнять в своей задаче (stop()всё равно работает); возвращается, когда попытки переподключения исчерпаны.is_listening— открыто ли сейчас живое соединениеLISTEN.- Функция-обработчик получает только строку содержимого. Она выполняется в потоке цикла событий с
переменными контекста (contextvars) кода, который начал слушать. Если она возвращает ожидаемый объект
(
async def), он выполняется как отслеживаемая фоновая задача. Исключения обоих видов записываются в журнал и никогда не передаются дальше — ошибка обработчика не обрывает подписку. - Сторож и паузы: соединение проверяется раз в секунду; когда оно обрывается, слушатель
переподключается. Подряд идущие неудачи (не удалось подключиться или соединение оборвалось, не пережив
одной проверки) ждут
backoff * 2**attemptсекунд (или следующий элемент последовательностиbackoff, повторяя последний), но не большеmax_backoff_seconds. Послеreconnect_attemptsнеудачных попыток слушатель пишет в журнал на уровне ERROR и останавливается. Счётчик обнуляется, когда соединение переживает проверку. Если слушатель уже слушал, переподключение, отклонённое как ошибка настройки (база удалена и создана заново), повторяется так же — на обоих драйверах. - Без поддержки (SQLite, ClickHouse):
start()иrun()даютUnSupportedErrorдо открытия соединения — у базы нет каналов уведомлений; опрашивайте таблицу. channel, которыйLISTENне принимает, даётConfigurationError— на нём не начало бы слушать ни одно переподключение: пустой, не текст или с нулевым байтом — из конструктора, длиннее имён базы (63 байта в PostgreSQL) — изstart()иrun().
NOTIFY не сохраняется
Уведомление, отправленное, пока слушатель переподключается, теряется. Используйте LISTEN/NOTIFY, чтобы
уменьшить задержку поверх надёжного источника данных (опроса таблицы), но никогда вместо него — именно
так его использует OutboxRelay.