-
Notifications
You must be signed in to change notification settings - Fork 1
plumqqz/mbus4
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Управление очередями
Очереди нормальные, полноценные, умеют
1. pub/sub
2. queue
3. request-response
Еще умеют message selectorы, expiration и задержки доставки
Payload'ом очереди является значение hstore (так что тип hstore должен быть установлен в базе)
Очередь создается функцией
mbus.create_queue(qname, ncons, is_roles_security_model default false)
где
qname - имя очереди. Допустимы a-z (НЕ A-Z!), _, 0-9
ncons - число одновременно доступных частей. Разумные значения - от 2 до 128-256
больше ставить можно, но тогда будут слишком большие задержки на перебор всех частей
Если указанная очередь уже существует, то будут пересозданы функции получения и отправки сообщений.
Если параметр is_roles_security_model установлен, то право на отправку сообщений в очередь
получат только те пользователи, у которых есть роль mbus_<dbname>_post_<qname>, а на получение
обладатели роли mbus_<dbname>_consume_<queue_name>_by_<consumer_name>.
Теперь в очередь можно помещать сообщения:
select mbus.post_<qname>(data hstore,
headers hstore DEFAULT NULL::hstore,
properties hstore DEFAULT NULL::hstore,
delayed_until timestamp without time zone DEFAULT NULL::timestamp without time zone,
expires timestamp without time zone DEFAULT NULL::timestamp without time zone,
iid text default null)
где
data - собственно payload
headers - заголовки сообщения, в общем, не ожидается, что прикладная программа(ПП) будет их
отправлять
properties - заголовки сообщения, предназначенные для ПП
delayed_until - сообщение будет доставлено ПОСЛЕ указанной даты. Зачем это надо?
например, пытаемся отправить письмо, почтовая система недоступна.
Тогда пишем куда-нибудь в properties число попыток, в delayed_until - (now()+'1h'::interval)::timestamp
Через час это сообщение будет снова выбрано и снова предпринята попытка
что-то сделать с сообщением
expires - дата, до которой живет сообщение. По умолчанию - всегда. По достижению указанной даты сообщение удаляется
Полезно, чтобы не забивать очереди всякой фигней типа "получить урл", а сеть полегла,
сообщение было проигнорировано и так и осталось болтаться в очереди.
От таких сообщений очередь чистится функцией mbus.clear_queue_<qname>()
iid - id добавляемого сообщения. Должно быть получено с помощью функции mbus.get_iid
Если не указано, то значение будет получено самостоятельно.
!!! ВНИМАНИЕ !!!
При отправке сообщения в очередь с iid, полученным для другой очереди, сообщение
будет успешно отправлено, но в дальнейшем не будет доступно для функции mbus.take(iid).
!!! ВНИМАНИЕ !!!
Возвращаемое значение: iid добавленного сообщения.
и еще:
mbus.post(qname text, data ...)
Функция ничего не возвращает
Получаем сообщения:
mbus.consume(qname) - получает сообщения из очереди qname. Возвращает result set из одного
сообщения, колонки как в mbus.qt_model. Кроме описанных выше в post_<qname>,
существуют колонки:
id - просто id сообщения. Единственное, что про него можно сказать - оно уникально.
используется исключительно для генерирования id сообщения
iid - глобальное уникальное id сообщения. Предполагается, что оно глобально среди всех
сообщений; предполагается, что среди всех баз, обменивающихся сообщениями, каждая
имеет уникальное имя.
added - дата добавления сообщения в очередь
Если сообщение было получено одним процессом вызовом функции mbus.consume(qname), то другой процесс
его НЕ ПОЛУЧИТ. Это классическая очередь.
Реализация publish/subscribe
В настояшей реализации доступны только постоянные подписчики (durable subscribers). Подписчик создается
функцией
mbus.create_consumer(qname, cname, selector)
где
qname - имя очереди
cname - имя подписчика
selector - выражение, ограничивающее множество получаемых сообщений
Имя подписчика должно быть уникальным среди всех подписчиков (т.е. не только подписчиков этой очереди)
В selector допустимы только статические значения, известные на момент создания подписчика
Алиас выбираемой записи - t, тип - mbus.qt_model, т.е. селектор может иметь вид
$$(t.properties->'STATE')='DONE'$$,
но не
$$(t.properties>'user_posted')=current_user$$,
Следует заметить, что в настоящей реализации селекторы весьма эффективны и предпочтительней
пользоваться ими, чем фильтровать сообщения уже после получения.
Замечание: при создании очереди создается подписчик default
Получение сообщений подписчиком:
mbus.consume(qname, cname) - возвращает набор типа mbus.qt_model из одной записи из очереди qname для подписчика cname
mbus.consume_<qname>_by_<cname>() - см. выше
mbus.consumen_<qname>_by_<cname>(amt integer) - получить не одно сообщение, а набор не более чем из amt штук.
Сообщение msg, помещенное в очередь q, которую выбирают два процесса, получающие сообщения для подписчика
'cons', будет выбрано только одним из двух процессов. Если эти процессы получают сообщения из очереди q для
подписчиков 'cons1' и 'cons2' соответственно, то каждый из них получит свою копию сообщения.
После получения поле headers сообщения содержит следующие сообщения:
seenby - text[], массив баз, которые получили это сообщение по пути к получаетелю
source_db - имя базы, в которой было создано данное сообщение
destination - имя очереди, из которой было получено это сообщение
enqueue_time - время помещения в очередь исходного сообщения (может отличаться от added,
которое указывает, в какое время сообщение было помещено в ту очередь, из которой происходит получение)
Если сообщение не может быть получено, возвращается пустой набор. Почему не может быть получено сообщение?
Вариантов два:
1. очередь просто пуста
2. все выбираемые ветви очереди уже заняты подписчиками, получающими сообщения. Заняты они могут быть
как тем же подписчиком, так и другими.
Всмпомогательные функции:
mbus.peek_<qname>(msgid text default null) - проверяет, если ли в очереди qname сообщение с iid=msgid
Если msgid is null, то проверяет наличие хоть какого-то сообщения. Следует учесть, что значение "истина",
возвращенное функцией peek, НЕ ГАРАНТИРУЕТ, что какие-либо функции из семейства consume вернут какое-либо
значение.
mbus.take_from_<qname>_by_<cname>(msgid text) - получить из очереди qname сообщение с iid=msgid
ВНИМАНИЕ: это блокирующая функция, в случае, если запись с iid=msgid уже заблокирована какой-либо транзакцией,
эта функция будет ожидать доступности записи.
Временные очереди.
Временная очередь создается функцией
mbus.create_temporary_queue()
Сообщения отправляются обычным mbus.post(qname, data...)
Сообщения получаются обычным mbus.consume(qname)
Временные очереди должны периодически очищаться от мусора вызовом функции
mbus.clear_tempq()
Выборка (consume) из временных очередей может быть блокирующей!
Удаление очередей.
Временные очереди удалять не надо: они будут удалены автоматически после окончания сессии.
Обычные очереди удаляются функцией mbus.drop_queue(qname)
Следует также обратить внимание на то, что активно используемые очереди должны _весьма_
агрессивно очищаться (VACUUM)
Триггеры
Для каждой очереди можно создать триггер - т.е. при поступлении сообщения в очередь
оно может быть скопировано в другую очередь, если селектор для триггера истинный.
Для чего это надо? Например, есть очень большая очередь, на которую потребовалось
подписаться. Создание еще одного подписчика - достаточно затратная вещь, для каждого
подписчика создается отдельный индекс; при большой очереди надо при создании подписчика
указывать параметр noindex - тогда индекс не будет создаваться, но текст запроса для
создания требуемого индекса будет возвращен как raise notice.
Триггер создается фукцией mbus.create_trigger(src_queue_name text, dst_queue_name text, selector text);
Временные подписчики
Временные подписчики создаются функцией mbus.create_temporary_consumer(qname text, selector text)
Функция возвращает имя временного подписчика
Подписчик существует до тех пор, пока активная текущая сессия.
Для удаления уставших подписчиков необходимо периодически вызывать функцию mbus.clear_tempq()
Функция mbus.create_view
Предполагается, что все функции выполняются от имени пользователя pg с соответствующими правами.
Это не всегда устраивает; данная фунцкция создает view с именем viewname (если не указано - то с именем public.queuename_q)
и триггер на вставку в него; на это view уже можно раздавать права для обычных пользователей.
Упорядочивание сообщений
Для сообщения (назовем его 1) может быть указан id других сообщений(назовем их 2), ранее получения которых сообщение 1 не может быть получено.
Он находится в заголовках и называется consume_after. Сообщения 1 и 2 не обязаны быть в одной очереди. Зачем это надо?
Например, мы отправляем сообщение с командой "создать пользователя вася" и затем "для пользователя вася установить лимит в 10 тугриков".
Так как порядок получения не определен, не исключена ситуация, когда сообщение с лимитом будет получено хронологически раньше,
чем сообщение о создании пользователя. Таким образом, не очень понятно, что делать с сообщением об установлении лимита:
либо отправить его обратно в очередь с увеличением счетчика получений и задержкой доставки, либо отбросить; в любом случае
требуется дополнительный код и т.п. В случае же с упорядочиванием можно потребовать, чтобы сообщение с лимитом было получено
только и исключительно после сообщения о создании; таким образом проблема устраняется.
Так как сообщений о получении может быть указано несколько и они могут находиться в любой очереди, то вполне возможен такой
вариант:
поместить сообщение "создать пользователя" в очередь команды для сервера №1 и сохранить id сообщения как id1
поместить сообщение "создать пользователя" в очередь команды для сервера №2 и сохранить id сообщения как id2
...
поместить сообщение "создать пользователя" в очередь команды для сервера №N и сохранить id сообщения как idN
поместить сообщение "установить лимит" с ограничением "получить после id1" в очередь команды для сервера №1
поместить сообщение "установить лимит" с ограничением "получить после id2" в очередь команды для сервера №2
...
поместить сообщение "установить лимит" с ограничением "получить после idN" в очередь команды для сервера №N
поместить сообщение "установить местоположение профайла пользователя" с ограничением "получить после id1,id2,...idN" в очередь "локальные команды"
и сохранить id сообщения как id_place_set
поместить сообщение "удалить пользователя" с ограничением "получить после id_place_set" в очередь "локальные команды"
Таким образом пользователь будет скопирован на сервера, на каждом из них будет установлен лимит, установлены ссылки на профайлы
и удален пользователь на локальном сервере.
!!!!! При невозможности обработать сообщение оно должно быть помещено обратно в ту же очередь или в dmq со старым iid !!!!!!
Внимание!
Большое количество сообщений, ожидающих доставки другого сообщения, может привести к снижению производительности.
Саги
Сага - это долговременная последовательность транзакций, которая должна в итоге либо выполниться целиком, либо быть откачена целиком
путем применения для каждой ранее успешно выполненной транзакции компенсационной транзакции, отменяющей ее действие.
Например, создание тура для юзера - это сага (пример несколько искусственный, но тем не менее). Надо
1. Зарезервировать номер (например, через запрос какого-то стороннего вебсервиса)
2. Зарезервировать билеты на самолет (туда и обратно) исходя из параметров резервирования номера (какие-то http-запросы на сторонние сервера)
3. Исходя из параметров билетов - зарезервировать трансфер от/до аэропорта (еще какие-то http-запросы на другие сторонние сервера)
4. Юзер утверждает предложенный вариант через веб-интерфейс.
Пункты 2 и 3 могут выполняться параллельно (как сами по себе - искать подходящие рейсы туда можно одновременно на нескольких сервисах;
рейсы обратно можно искать также паралелльно; разумеется, эти два набора параллельных транзакций могут выполняться также параллельно).
В случае сбоя любого из четырех пунктов (не удалось зарезервировать номер, не удалось зарезервировать самолеты или трансферы)
все ранее проведенные резервирования должны быть отменены.
Как это должно быть выполнено :
Создаются ДВЕ последовательности действий, две упорядоченных группы сообщений : первая ("прямая") - резервирование
и вторая, с компенсационными сообщениями("обратная") - откат резервирования, причем вторая упорядочена относительно
сообщения в специальной очереди ("затычки"), из которой выборка производится только путем вызова функции take.
Это служебное сообщение, от которого зависят компенсационные сообщения; кроме того, в нем находятся iid всех
сообщений прямой последовательности; в случае инициирования отката это сообщение забирается функцией take и
функцией же take забираются все сообщения прямой последовательности, полученные из этого служебного сообщения.
Все сообщения прямой последовательности содержат ссылку на головное сообщение ("затычку") последовательности отката.
Последнее сообщение прямой последовательности - также служебное, в нем находятся id сообщений последовательности отката и
при окончании прямой последовательности они также забираются функцией take. Последнее сообщение обратной
последовательности, как и прямой, служебное - для фиксации успешности проведения отката.
Прямая последовательность | Откат
----------------------------------------------------------------------+-----------------------------------------------------------------------
id выбрать после id команда Сообщение отката | id выбрать после id команда
----------------------------------------------------------------------+------------------------------------------------------------------------
1 -/- Зарезервировать номер 1b | 1b забрать сообщения с #1,2,3,4,5,6
могут выполняться / 2 1 Заказ билета туда 1b | 2b 1b отмена резервирования отеля \
параллельно \ 3 1 Заказ билета обратно 1b | 3b 1b отмена резервирования билета туда |
могут выполняться / 4 2 Трансфер до отеля 1b | 4b 1b отмена резервирования билета обратно + могут выполняться параллельно
параллельно \ 5 3 Трансфер от отеля 1b | 5b 1b отмена резервирования трансфера до отеля |
6 4,5 забрать сообщения с 1b | 6b 1b отмена резервирования транфера от отеля /
#1b,2b,3b,4b,5b,6b,7b | 7b 2b,3b,4b,5b,6b откат завершен
Всякий консумер прямой очереди (тот, который делает реальную работу - резервирует номер, рейсы, отправляет письмо "утвердите тур" и т.п.) в случае
обнаружения необходимости отката выбирает сообщение-"затычку" путем вызова функции take(ид-сообщение-отката), откуда получает полный список
сообщений прямой последотвальности, которые также забирает командой take, после чего фиксирует транзакцию и завершается.
Если выбрать сообщение отката не удалось - не беда, стало быть, какой-то другой обработчик уже сделал это.
Обработчик, удаляя главное сообщение отката ("затычку"), инициирует этим запуск обработчиков сообщений отката (которые аннулируют резервирование номера,
авиабилетов и т.п.), а удалив сообщения прямой последовательности, останавливает прямую обработку сообщений (т.е. останавливает заказ авиабилетов,
трансфера, всякие страховки и проч.)
Для передачи данных между обработчиками нам потребуется иметь таблицу (или даже несколько таблиц) "проведение заказа тура" (тем более что все равно
надо иметь возможность просмотреть формируемые туры, залипшие туры, отчетность по сформированным и т.п.)
Каждый тип сообщения - как прямого, так и сообщения отката - фактически требует своего отдельного обработчика, что логично -
обработчик, который резервирует номера, другой обработчик, который разбирается с авиабилетами, третий - с трансфером.
Псевдокод :
-- forward transactions
bung_iid := get_iid('bungs'); -- нам потребуется iid сообщения-затычки
final_iid := get_iid('final'); -- и финального сообщения в прямой последовательности
insert into tour.... returning tour_id into tour_id; --в tour у нас будут жить промежуточные данные - номера рейсов, мест, номеров в отеле и и.п.
--а также id сообщений, которые обрабатывают данный заказ на тур
room_resvr := post('rooms_reservations', ....);
book_tiket_to := post('book_tikets', consume_after := array[room_resvr], tour_id := tour_id, bung :=bung_iid ...);
book_tiket_from := post('book_tikets', consume_after := array[room_resvr],tour_id := tour_id, bung :=bung_iid ...);
transf_to := post('transfer_to', consume_after := array[book_tiket_to], tour_id := tour_id, bung :=bung_iid ...);
transf_from := post('transfer_from', consume_after := array[book_tiket_from], tour_id := tour_id, bung :=bung_iid ...);
-- compensation transactions
post('bungs', id_to_take := array[ room_revrv, book_tiket_to, book_tiket_from, transf_to, transf_from, final_iid], iid:=bung_iid);
cancel_room_resvr:= post('cancel_room_reservations', consume_after:=bung_iid, tour_id := tour_id...);
cancel_tiket_booking_to:=post('cancel_tiket_booking_to', consume_after:=bung_iid, tour_id := tour_id...);
cancel_tiket_booking_from:=post('cancel_tiket_booking_from', consume_after:=bung_iid, tour_id := tour_id...);
cancel_transfer_to:=post('cancel_transfer_to', consume_after:=bung_iid, tour_id := tour_id...);
cancel_transfer_from:=post('cancel_transfer_from', consume_after:=bung_iid, tour_id := tour_id...);
post('cancel_completed', consume_after:=array[cancel_room_resvr,cancel_tiket_booking_to,canel_tiket_booking_from,canel_transfer_to,canel_transfer_from]);
update tour set room_resvr_iid=room_resvr,
....
cancel_room_resvr_iid:=cancel_room_resvr,
....
where tour_id=tour_id; --чтобы можно было видеть в процессе выполнения статус обработки/отката резервирования тура
--закрываем прямой ход
post('end_of_tour_reservation', iid:=final_iid, id_to_take:=array[cancel_room_reservation,cancel_tiket_booking_to,canel_tiket_booking_from,canel_transfer_to,canel_transfer_from, bung_iid]);
--наконец
commit;
Кроме того, нам потребуются 12 обработчиков сообщений - пять резверирующих номера, рейсы, трансфер и пять откатывающих резервирование, плюс два
для окончания прямого хода или отката. Каждый из пяти резервантов должен в случае обнаружения необходимости отката выбрать сообщение с
данным bung_id и уже из него выбрать iid сообщений из id_to_take и забрать их тоже. Два оставшихся обработчика - успешного выполнения и отката -
должны соответствующим образом поменять запись в таблице tour - дескать, заказ завершен и отправлять сообщение с уведомлением
юзера о том, что надо утвердить собранное (и менеджеру, что этот тур собран - ну или не собран, хехе)
Что должны делать обработчики прямой и обратной последовательностей? Все очень прямолинейно:
1. Начать транзакцию
2. Получить сообщение
3. Попытаться выполнить требуемую операцию
3.1 При удачном выполнении - изменить запись о состоянии резервировании тура (всякие подробности в таблице tour)
3.2 При неудачном - либо отправить сообщение обратно с отложенной доставкой и увеличением счетчика, либо, если счетчик уже зашкаливает, перейти к следующему пункту
3.3 При невозможности - сервис посылает или слишком много попыток - начать откат, как описано выше -
взять iid затычки, выбрать по нему затычку, выбрать все сообщения, которые в ней указаны, поменять tour на состояние "откатывается потому что...."
4. Зафиксировать транзакцию
Безопасность и права доступа
Разграничение прав для очередей может быть двух типов:
1. Обычное, как и в предыдущих версиях
2. С использованием штатных средств авторизации (GRANT/REVOKE)
При создании очереди можно указать, как она создается: либо как и ранее, либо с указанием дополнительного параметра is_roles_security_model,
установленного в true. При наличии этого параметра, во-первых, создаются функции отправки сообщения с опцией security definer, и, во-вторых,
при отправке проверяется наличие роли mbus_<dbname>_post_<queue_name> /где <dbname> - имя базы, <queue_name> - имя очереди/ у пользователя,
отправляющего сообщение.
Для получения сообщения подписчик должен иметь роль вида mbus_<dbname>_consume_<queue_name>_by_<consumer_name> /<consumer_name> - имя подписчика,
остальное см. выше/
При создании очереди (любой - как со старым способом ограничения доступа, так и с новым) автоматически создается соответсвующие роли - для отправки
и для получения (при создании очереди автоматически создается получатель default - вот и роль для получения).
При создании получателя (любого) автоматически создается соответствующая роль; эта роль удаляется при удалении получателя.
При удалении очереди автоматически удаляются роли для отправки и получения сообщений (для всех получателей).
About
simple message bus for pg
Resources
Stars
Watchers
Forks
Releases
No releases published
Packages 0
No packages published