Триггеры¶
В этом руководстве показано, как настроить конфигурацию мастер-мастер, а также установить персистентный триггер для разрешения конфликта репликации.
Подробная информация о триггерах доступна в документации Tarantool.
Содержание:
Пререквизиты¶
Для выполнения примера требуются:
- установленный Docker-образ Tarantool DB; 
- приложение Docker Compose; 
- исходные файлы примера - triggers.- Примечание - Есть два способа получить исходные файлы примера: - Архив с полной документацией Tarantool DB, полученный по почте или скачанный в личном кабинете tarantool.io. Пример архива: - tarantooldb-documentation-3.0.0.tar.gz. Пример- triggersрасположен в таком архиве в директории- ./doc/examples/triggers/.
- Отдельный архив triggers.tar.gz, скачанный c сайта Tarantool. 
 
Запуск стенда и подключение к узлам¶
Для успешного запуска должны быть свободны следующие порты:
- 2379 
- 3301 
- 3302 
- 8081 
Перейдите в папку с примером triggers:
cd ./doc/examples/triggers
Запустите стенд:
make start
Команда развернет следующий стенд:
- кластер Tarantool DB, который состоит из одного набора реплик с двумя мастер-узлами; 
- кластер etcd из 3 узлов; 
- 1 узел Tarantool Cluster Manager (TCM). 
После запуска должны работать все контейнеры, кроме init_host.
Также после запуска кластера становится доступен веб-интерфейс TCM. Для входа в TCM откройте в браузере адрес http://localhost:8081. Логин и пароль для входа:
- Username: - admin
- Password: - secret
Конфигурация мастер-мастер¶
Обратите внимание, что в TCM на вкладке Stateboard в наборе реплик два мастера:

Выберите экземпляр кластера storage-1-msk. В открывшемся окне выберите вкладку Terminal.
Во вкладке Terminal добавьте временную функцию now() для удобства ввода данных:
function now()
    local datetime = require('datetime')
    return datetime.now()
end
Откройте веб-интерфейс TCM во второй вкладке браузера. В TCM перейдите на вкладку Stateboard и выберите
второй экземпляр кластера storage-1-spb. В открывшемся окне перейдите на
вкладку Terminal
и добавьте туда временную функцию now(), описанную выше.
Теперь в браузере открыты две вкладки TCM (далее TCM 1 и TCM 2) для работы с узлами кластера
storage-1-msk и storage-1-spb соответственно.
В TCM 1 во вкладке Terminal выполните следующую команду:
box.space.my_space:replace({1, now(), 'Too'})
Теперь в TCM 2 во вкладке Terminal выполните такую команду:
box.space.my_space:replace({2, now(), 'Foo'})
Чтобы просмотреть содержимое спейсов, выполните эту команду во вкладках Terminal в TCM 1 и TCM 2:
box.space.my_space:fselect()
Несмотря на то, что записи были вставлены на разных экземплярах, обе записи успешно добавлены в спейс:
---
- |-
  +-----+-----------------------------+-----+
  | id  |             dt              |data |
  +-----+-----------------------------+-----+
  |  1  |"2025-02-13T08:00:35.839119Z"|"Too"|
  |  2  |"2025-02-13T08:00:41.792330Z"|"Foo"|
  +-----+-----------------------------+-----+
...
Конфликт репликации мастер-мастер¶
Если набор реплик работает в режиме мастер-мастер, одновременная запись по одному и тому же первичному ключу с разных экземпляров кластера приводит к конфликту репликации: в наборе реплик у записи с этим первичным ключом возникают две версии текущего значения. Длительность окна, в течение которого запись в два узла считается одновременной, определяется скоростью репликации между этими узлами и зависит от сетевых задержек. В общем случае речь идёт о нескольких миллисекундах.

Рекомендуется использовать одно из решений ниже:
- использовать коммутативные операции; 
- реализовать механизм разрешения конфликта репликации. 
Кроме того, в системах с репликацией мастер-мастер по описанным выше причинам нельзя использовать
автоинкремент в индексах. Используйте вместо этого значения типа UID.
Подробнее о конфликтах репликации читайте в документации Tarantool.
Поскольку воспроизвести одновременную запись в оба узла сложно, в примере для демонстрации конфликта репликации мастер-мастер будет специальным образом нарушен процесс репликации.
Выполните во вкладках Terminal в TCM 1 и TCM 2 следующие команды:
replication_bak = box.cfg.replication
box.cfg({replication = {}})
Добавьте в TCM 1 во вкладке Terminal такую запись:
box.space.my_space:replace({3, now(), 'AAAA'})
В TCM 2 во вкладке Terminal вставьте следующую запись:
box.space.my_space:replace({3, now(), 'BBBB'})
Чтобы восстановить процесс репликации, выполните команду на обоих узлах (во вкладках Terminal в TCM 1 и TCM 2):
box.cfg({replication = replication_bak})
Чтобы просмотреть содержимое спейсов, выполните эту команду во вкладках Terminal в TCM 1 и TCM 2:
box.space.my_space:fselect()
На первом экземпляре вывод будет выглядеть так:
---
- |-
  +-----+-----------------------------+------+
  | id  |             dt              | data |
  +-----+-----------------------------+------+
  |  1  |"2025-02-13T08:00:35.839119Z"|"Too" |
  |  2  |"2025-02-13T08:00:41.792330Z"|"Foo" |
  |  3  |"2025-02-13T08:12:34.279904Z"|"BBBB"|
  +-----+-----------------------------+------+
...
На втором экземпляре вывод будет следующий:
---
- |-
  +-----+-----------------------------+------+
  | id  |             dt              | data |
  +-----+-----------------------------+------+
  |  1  |"2025-02-13T08:00:35.839119Z"|"Too" |
  |  2  |"2025-02-13T08:00:41.792330Z"|"Foo" |
  |  3  |"2025-02-13T08:12:27.939872Z"|"AAAA"|
  +-----+-----------------------------+------+
...
Видно, что данные не согласованы, и каждый экземпляр кластера имеет свою версию данных.
Реализация механизма разрешения конфликта репликации¶
Строгого критерия, однозначно определяющего порядок, в котором были сделаны записи в кластер, не существует. Это означает, что критерий нужно выбирать персонально для каждого приложения. Виды критериев:
- временные метки. Временные метки имеют свои недостатки, в том числе разность времени между серверами и високосные секунды; 
- критерии, характерные для данных конкретного спейса. 
В этом примере в качестве критерия используется временная метка. Логику разрешения конфликта будет выполнять триггер перед записью в спейс.
Откройте веб-интерфейс TCM в третьей вкладке браузера. Далее:
- В левом меню выберите вкладку Migrations. 
- Нажмите - +, чтобы добавить новую миграцию.
- Добавьте новую миграцию с названием - 002_test.lua. 
- Скопируйте в поле ввода код миграции - 002_test.lua:- local function apply() -- Описана логика работы триггера local body = [[ function (old_tuple, new_tuple, space_name, operation_name) if old_tuple and new_tuple then -- Индекс 2 соответствует полю dt if old_tuple[2] < new_tuple[2] then return new_tuple end else return new_tuple end return old_tuple end ]] -- Объявлен персистентный триггер box.schema.func.create('example.replicated_trigger', { body = body, trigger = 'box.space.my_space.before_replace' }) return true end -- Стандартный блок возврата для модулей миграции return { apply = { scenario = apply, } } 
- Нажмите кнопки Save и Apply, чтобы сохранить и применить добавленную миграцию. В этой миграции был добавлен триггер, который будет продолжать работать после перезапуска экземпляров. 
Обратите внимание, что для нескольких спейсов можно указывать один триггер:
box.schema.func.create('example.replicated_trigger', {
       body = body,
       trigger = {
         'box.space.my_space1.before_replace', -- < --
         'box.space.my_space2.before_replace', -- < --
       }
})
Проверка работы триггера¶
Чтобы снова нарушить репликацию, выполните следующие команды на обоих узлах (во вкладках Terminal в TCM 1 и TCM 2):
replication_bak = box.cfg.replication
box.cfg({replication = {}})
На первом узле, во вкладке Terminal в TCM 1, добавьте такую запись:
box.space.my_space:replace({4, now(), 'CCCC'})
На втором узле, во вкладке Terminal в TCM 2, добавьте следующую запись:
box.space.my_space:replace({4, now(), 'DDDD'})
Чтобы включить репликацию, выполните команду на обоих узлах:
box.cfg({replication = replication_bak})
Чтобы просмотреть содержимое спейсов, выполните эту команду на обоих узлах:
box.space.my_space:fselect()
Четвёртая строка будет одинаковой везде, потому что сработал триггер, оставивший из двух версий более свежую:
---
- |-
  +-----+-----------------------------+------+
  | id  |             dt              | data |
  +-----+-----------------------------+------+
  |  1  |"2025-02-13T08:00:35.839119Z"|"Too" |
  |  2  |"2025-02-13T08:00:41.792330Z"|"Foo" |
  |  3  |"2025-02-13T08:12:34.279904Z"|"BBBB"|
  |  4  |"2025-02-13T08:56:01.204955Z"|"DDDD"|
  +-----+-----------------------------+------+
...