Библиотека Wiki PubSub
======================
[Библиотека Wiki PubSub][src] позволяет подписываться на создание правок в Народной карте для их асинхронной обработки.


Проблема
--------
Правки объектов (коммиты в смысле [темпоральной модели данных][temporal]) на сервисе могут порождаться из нескольких источников (редактор, групповые операции, слияние данных, разовые импорты). В свою очередь в асинхронной обработке правок заинтересованы несколько потребителей (создание уведомлений и задач модерации, подсчёт статистики пользователей, тяжелые операции типа подсчёта bbox-ов и полигональных геометрий контурных объектов).

В связи с этим будет полезен общий механизм асинхронной обработки правок по принципу ([publish-subscribe][pubsub-wikipedia]). Преимуществом такого подхода является то, что те, кто порождают правки, не зависят от потребителей, а потребители в свою очередь не зависят друг от друга.

Результаты работы обработчиков не всегда находятся в той же базе, что и правки, но при этом хочется сохранить транзакционные гарантии, представляемые единой базой. В идеале, каждое событие должно быть обработано один и только один раз, несмотря на ошибки в коде обработчиков, отказы серверов, на которых они запущены и недоступность баз, где они сохраняют результаты.


Техническая реализация
----------------------
На операции с таблицей `revision.commit` повешен триггер, который порождает события – записи в таблице `revision.commit_queue`. Логика генерации событий для различных веток:
* все коммиты в trunk-ветке порождают запись с `branch_id = 0`;
* подтверждение коммита порождает запись с `branch_id`, равным идентификатору approved-ветки;
* добавление нового коммита в stable-ветку порождает запись c `branch_id` этой ветки.
При создании stable-ветки из подтверждённой никаких событий не генерируется – пока неясно, нужны ли коммиты, которые уже побывали в trunk кому-нибудь, но в случае необходимости их легко получить с помощью обычного интерфейса revision, и новых таких коммитов в stable-ветке не появляется.

Библиотека предоставляет интерфейс в стиле poll (получить новые события с момента последнего обращения). Возможности ждать возникновения нового события пока нет. Вопросы scheduling-а (например, как часто запрашивать новые события) находятся в компетенции клиентов.

Обработчик определяется своим `сonsumer_id` (строка) и `branch_id` - id ветки, события в которой ему интересны. Он открывает транзакцию `consumerTxn` на базе, в которой которой расположены результаты его работы. Пока эта транзакция не закрыта, обработчики с таким же `(consumer_id, branch_id)` заблокированы и не могут получать события. Тем не менее, целесообразно запустить несколько одинаковых обработчиков для отказоустойчивости. Для получения новых правок требуется открыть транзакцию на базе, где расположена схема `revision`. После получения нового batch–а, его обработки и записи результатов, транзакцию `consumerTxn` можно закрыть и повторить цикл.


### Гарантии обработки событий
Гарантируется, что обработчики рано или поздно увидят все правки, независимо от того, в каком порядке закончились транзакции, которые их породили и независимо от того, на каких серверах БД открывались транзакции для получения правок (разумеется, все эти сервера должны принадлежать одной группе репликации).

Для того, чтобы этого достичь, используется [информация о снапшоте][pg-snapshot], которую предоставляет PostgreSQL. Вместе с правкой в таблицу `revision.commit_queue` записывается `txid` транзакции, которая её породила. В транзакции, открытой на чтение, можно получить `txmin` – гарантируется, что все транзакции с `txid < txmin` уже завершились (функция `txid_current_snapshot()` возвращает ещё и идентификаторы транзакций, которые открыты в данный момент, но к сожалению эта информация не реплицируется).

Обработчики сохраняют `txid`, до которого они обработали события, в таблице `pubsub.watermark`. Если обработаны события с `txid > txmin`, они помещаются в таблицу `pubsub.out_of_order`, так как теоретически ещё могут появиться события с меньшим `txid`. От получения out-of-order событий можно отказаться, тогда гарантируется, что все правки будут обработаны в том порядке, в котором они были записаны в базу.

Новый watermark обработчика добавляется в базу когда в первый раз вызывается функция `lock_watermark()` (принимает на вход имя обработчика и `branch_id`). При этом идентификаторы транзакции и коммита инициализируются нулем. Таким образом обработчик получает все коммиты которые когда либо попали в базу начиная с самого первого.

Если все результаты работы обработчика хранятся в той же базе, в которой находится схема `pubsub`, гарантируется, что даже в случае сбоев все события будут обработаны ровно один раз. Если это не так (например, обработчик вызывает по HTTP внешние сервисы), события могут быть обработаны несколько раз, поэтому стоит задуматься об идемпотентности таких операций.

{% note warning %}

При наливке базы механизмами `pg_dump` и `pg_restore` значения в колонке `txid` таблицы `pubsub.watermark` станут неактуальными и механизм pubsub перестанет работать. Рекомендуется выполнить следующие действия на целевой базе данных после наливки:

```(sql)
BEGIN;
UPDATE pubsub.watermapk SET txid = 0;
-- The latter is a time consuming operation
UPDATE revision.commit_queue SET txid = 0;
COMMIT:
```

{% endnote %}


[pg-snapshot]:      https://www.postgresql.org/docs/current/functions-info.html#FUNCTIONS-TXID-SNAPSHOT
[pubsub-wikipedia]: https://en.wikipedia.org/wiki/Publish–subscribe_pattern
[src]:              https://a.yandex-team.ru/arc_vcs/maps/wikimap/mapspro/libs/pubsub
[temporal]:         https://wiki.yandex-team.ru/maps/dev/core/wikimap/mapspro/nkprotemporal
