# Механизм плагинов

В данном разделе описан механизм
[плагинов](../overview/glossary.md#plugin), с помощью которых можно
расширять функциональность Picodata.

## Общие сведения {: #overview }

Плагин представляет собой единицу законченной функциональности и
работает глобально во всем кластере. Плагин обладает следующими
свойствами:

- работает с привязкой к явно заданным [тирам][tier]. Плагины работают
  именно с тирами, а не с отдельными репликасетами
- даже если предполагается использование плагина на некоторых тирах
  кластера, он должен быть развернут на всех узлах кластера. Плагин не
  будет включен, если он отсутствует или поврежден хотя бы на одном
  активном инстансе кластера
- плагины расширяют функциональность именно СУБД, но никак не связаны
  друг с другом

[tier]: ../overview/glossary.md#tier

Для [разработки плагинов] предусмотрена библиотека [`picodata-plugin`].

Для работы плагина версия Picodata должна совпадать с версией библиотеки
`picodata-plugin`, которая использована для его сборки. Если версии
различаются, при [установке] плагина будет выведена ошибка, содержащая
текущую версию Picodata и ожидаемую версию `picodata-plugin`.

Проверку совместимости версий можно отключить, задав следующую переменную
окружения:

```bash
PICODATA_UNSAFE_DISABLE_PLUGIN_COMPATIBILITY_CHECK=1
```

Отключение проверки совместимости может негативно повлиять на работу
плагина и Picodata, поэтому должно быть хорошо обдумано.

[разработки плагинов]: ../dev/plugin_create.md#develop_plugin
[`picodata-plugin`]: https://crates.io/crates/picodata-plugin
[установке]: #plugin_install

## Структура плагина {: #structure }

Структурно, плагин состоит из трех компонентов:

- **Манифест** — файл, в котором описана версия плагина, сервисы, их
  начальная конфигурация и порядок запуска. Манифест является
  обязательной и неотъемлемой частью плагина и объединяет в себе все его
  компоненты
- **Сервис** — обязательная и основная часть плагина, реализующая его
  логику на языке Rust. Сервис обладает жизненным циклом и состоянием, а
  также его можно конфигурировать (например, задать время жизни
  сообщениям, если ваш сервис вычищает данные из таблицы по TTL). В
  плагине может быть несколько сервисов
- **Миграции** — набор файлов с расширением *.db, содержащих DML- и
  DDL-команды, которые логически связаны с плагином. Плагин определяет
  набор миграций в [манифесте](#manifest). Миграции выполняются при
  установке и удалении плагина

На файловом уровне плагин представляет собой набор из файла манифеста,
файлов миграций и некоторого числа скомпилированных разделяемых
библиотек в формате `*.so`.

Для работы плагин требует наличия дополнительных сущностей:

- версии плагина
- конфигурации плагина
- информации о [тирах][tier], на которых будут работать сервисы плагина

Версия указывается при установке/обновлении/удалении плагина, а
конфигурация представляет собой набор значений в глобальной таблице
`_pico_plugin_config` для корректной работы сервисов плагина.

Плагины могут создавать свои схемы данных в системных таблицах с помощью
механизма [миграций](#migrations).

См. также:

- [Описание системных таблиц плагинов](system_tables.md#plugins)

### Манифест {: #manifest }

Манифест плагина содержит следующие обязательные поля:

- `name` — уникальное имя плагина (в нижнем регистре без пробелов)
- `description` — произвольный текст, раскрывающий функцию и назначение
  плагина
- `version` — версия плагина в формате Semver
- `service_order` — список, отражающий порядок запуска сервисов
- `service` — список сервисов. У каждого сервиса должны быть
- заполнены поля `name` (имя), `description` (описание)
- заданы параметры конфигурации по умолчанию в разделе `default_config`.

Манифест плагина представляет собой многоуровневую конфигурацию. Пример
показан ниже:

```yml
plugin:
   description: my new super plugin
   name: my_super_plugin
   version: 0.1.0
   service_order:
    - kafka_connector
   service:
    - name: kafka_connector
      description: my service 1
      default_config:
        property_1:
          property_11: true
          property_12: false
        property_2: 123
    migration:
     - create_authors.db
     - create_books.db
     - create_costs.db
```

### Миграции {: #migrations }

[Миграции][migration] задаются в манифесте плагина с помощью
дополнительной секции `migration`.

Внутри файл миграций содержит аннотации и SQL-команды. Пример:

```sql
-- pico.UP
CREATE TABLE authors (
    id int NOT NULL,
    name text,
    PRIMARY KEY(id)
);
INSERT INTO authors VALUES (1, "Aleksandr Pushkin");
INSERT INTO authors VALUES (2, "Vladimir Mayakovsky");

-- pico.DOWN
DROP TABLE authors;
```

Аннотации разделяют части миграций, отвечающие за установку и удаление
плагина.

Аннотация `pico.UP` является обязательной и должна быть указана в первой
строке файла. Аннотация `pico.DOWN` опциональна.

<!-- такого поля в системной таблице пока нет
При установке плагина для каждого файла из секции UP выполняются все
SQL-команды и в системной таблице [`_pico_plugin`] увеличивается поле
`migration_progress`.
 -->

#### Использование параметров конфигурации плагина {: #use_plugin_config }

В миграциях можно ссылаться на значения из установленной
конфигурации плагина установленные в пространстве имен migration_context.
Например, установим значение конфигурации `red_tier`:

```sql
ALTER PLUGIN myplugin 0.1.0 SET migration_context.red_tier='my_tier';
```

После этого в теле миграции появляется возможность ссылаться на
установленный параметр:

```sql
CREATE TABLE T (A INT) ON TIER @_plugin_config.red_tier;
```

Основной сценарий использования подобной подстановки параметров —
задание [тиров][tier], на которых будут созданы нужные для работы плагина
таблицы. Для этого в синтаксисе [создания
таблицы](../reference/sql/create_table.md) существует возможность указать тир.
Таким образом, на этапе разработки плагина можно определить, какие объекты БД
нужны для работы плагина, и описать их создание в файле миграций.

На этапе разработки плагина необходимо определить, сколько тиров потребуется.
Поскольку конкретные имена тиров в кластере еще неизвестны, то имеет смысл
указать имена в конфигурации плагина. Посредством подстановки имя тира в
кластере становится доступным в миграции.


### Сервис {: #service }

Каждый `.so`-файл может содержать один или несколько сервисов. Сервис
представляет собой реализацию trait’а *Service*, а также функцию,
помеченную атрибутом `#[service_registrar]`, которая регистрирует сервис
в Picodata. Для того чтобы создать сервис, необходимо воспользоваться
библиотекой `picoplugin`, которая предоставляет trait Service и макрос
`#[service_registrar]`.

#### Трейт Service {: #service_trait }

Для создания сервиса плагин должен реализовать трейт `Service`.
Большинство методов в нем являются необязательными. Для реализации
трейта пользователю необходимо, как минимум, реализовать метод `name` и
указать тип для конфигурации (если конфигурация не нужна — следует
указать пустой кортеж `()`).

Код трейта доступен по
[ссылке](https://docs.rs/picodata-plugin/latest/picodata_plugin/plugin/interface/trait.Service.html).

#### Регистрация сервиса {: #service_registration }

Вторым шагом необходимо зарегистрировать сервис. Для этого создается
функция, помеченная специальным макросом. Эту функцию вызовет Picodata
для создания экземпляра сервиса. В функции необходимо указать имя
сервиса и версию плагина, для которого сервис создан. Пример такой
функции:

```
#[service_registrar]
fn my_registrar(registry: &mut ServiceRegistry) {
   registry.add("kafka_connector", "0.1.0", Service1::new)
}
```

## Конфигурация плагина {: #plugin_config }

Конфигурация плагина состоит из двух частей:

- топология сервисов плагина (на каких [тирах][tier] они работают)
- конфигурация самих сервисов

Сервисы запускаются только на явно заданных [тирах][tier] (нельзя
запустить сервис только на конкретном репликасете или узле из тира).

Топология сервисов хранится в глобальной системной таблице
[`_pico_service`].

Настроить сервис можно c помощью SQL-запроса. Пример:

```sql
ALTER PLUGIN <plugin_name> <version>
ADD SERVICE <service_name>
TO TIER ‘red’
OPTION(timeout=...);
```

Конфигурация сервисов плагина хранится в глобальной служебной таблице
`_pico_plugin_config` как набор `key`/`value`, где `key` — всегда
строка. Никаких ограничений на тип `value` не накладывается. Это
позволяет использовать в виде отдельных ключей те значения, которые чаще
всего будут настраиваться, что позволит избежать полной перезаписи
конфигурации на каждое изменение.

Пример таблицы с конфигурацией сервиса:

| plugin\_name | service\_name |    key     | value                                                                                                                                                 |
| :----------- | :------------ | :--------: | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
| kafka        | consumer      |  mapping   | [ { "topic": "my\_topic", "autocommit": "false", "table": "my\_table" }, { "topic": "my\_topic\_2", "autocommit": "true", "table": "my\_table\_2" } ] |
| kafka        | consumer      | kafka\_uri | 127.0.0.1:9092                                                                                                                                        |

## Жизненный цикл плагина {: #plugin_lifecycle }

Жизненный цикл плагина включает в себя следующие этапы:

- установка плагина в кластер
- включение плагина
- отключение плагина
- удаление плагина
- смена конфигурации плагина

Ниже приведены подробности реализации этих этапов. Примеры SQL-команд
для работы с плагинами содержатся в руководстве [Управление
плагинами](../dev/plugin_mgmt.md).

### Установка {: #plugin_install }

Сначала необходимо заранее скопировать на все узлы кластера `.so`-файлы
плагина в директорию `share_dir`
([задается](../reference/cli.md#run_share_dir) при запуске инстанса).
Например, можно сделать с помощью роли [Ansible][ansible].

После этого можно установить [манифест](#manifest) плагина — это можно
сделать с любого узла кластера.

Установка манифеста проходит в несколько этапов:

- валидация манифеста на узле, с которого происходит установка
- загрузка сериализованной информации из манифеста в системную таблицу
  [`_pico_property`] на время установки плагина
- централизованная проверка наличия `.so`-файлов на всех живых узлах
  [губернатором](../overview/glossary.md#governor) в виде вызова
  [`.proc_load_plugin_dry_run`]
- при отсутствии ошибок губернатор заполняет системные таблицы
  [`_pico_plugin`] и [`_pico_service`] данными из манифеста с помощью
  [CaS]-операции
- губернатор удаляет ранее загруженные данные из таблицы
  [`_pico_property`]
- узлы кластера ожидают очистки таблицы [`_pico_property`] и затем
  проверяют таблицу [`_pico_plugin`] на наличие плагина
- когда установка плагина подтверждена всеми узлами, выполняются
  [миграции](#migrations)

[`_pico_property`]: system_tables.md#_pico_property
[`_pico_plugin`]: system_tables.md#_pico_plugin
[`_pico_service`]: system_tables.md#_pico_service
[`_pico_service_route`]: system_tables.md#_pico_service_route
[`.proc_load_plugin_dry_run`]: rpc_api.md#proc_load_plugin_dry_run
[CaS]: ../overview/glossary.md#cas
[migration]: ../overview/glossary.md#migration
[ansible]: ../admin/deploy_ansible.md


### Включение {: #plugin_enable }

Установленный в кластере плагин по умолчанию находится в выключенном
состоянии: в системной таблице `_pico_plugin` задано значение `enable =
false`. Включение проходит следующим образом:

- губернатор проверяет, что значение `pending_plugin_enable` в системной
  таблице [`_pico_property`] свободно
- Raft-лидер инициирует операцию `OpPluginEnable{ plugin_name,
  on_start_timeout }` на каждом узле кластера
- на каждом узле в рамках операции OpPluginEnable происходит выборка
  сервисов из таблицы [`_pico_service`] и локально создается запись в
  таблице [`_pico_property`] с ключом `pending_plugin_enable` и значением
  `{ plugin_name, service_list, on_start_timeout }`
- губернатор реагирует на появление ключа `pending_plugin_enable` и
  отправляет всем живым узлам кластера RPC-запрос [`.proc_enable_plugin`]
- плагин включается на узлах кластера, которые используют метаинформацию
  из указанных выше системных таблиц
- в зависимости от топологии кластера (принадлежности узла к нужному
  [тиру][tier]), на отдельных узлах валидируются и включаются сервисы
  плагина. Для каждого сервиса запускаются коллбэки [`on_start`],
  результат возвращается губернатору
- в случае успеха губернатор обновляет системную таблицу [`_pico_plugin`]
  и устанавливает для плагина свойство `enable = true`, а также
  заполняет таблицу [`_pico_service_route`] (маршрутизация для RPC)
- губернатор очищает значение ключа `pending_plugin_enable`
- узлы кластера ожидают очистки `pending_plugin_enable` и после этого
  проверяют в таблице `_pico_plugin` свойство `enable`

[`.proc_enable_plugin`]: rpc_api.md#proc_enable_plugin
[`on_start`]: https://docs.rs/picodata-plugin/latest/picodata_plugin/plugin/interface/trait.Service.html#method.on_start

### Отключение {: #plugin_disable }

Отключение плагина происходит локально на отдельных узлах с последующим
подтверждением через Raft:

- узел проверят, что поле `pending_plugin_disable` в системной таблице
  `_pico_property` не заполнено
- Raft-лидер инициирует операцию `OpPluginDisable{ plugin_name }` на
  каждом узле кластера
- узел собирает метаинформацию из таблицы `_pico_plugin` и устанавливает
  для плагина значение `enable=false`
- узел в асинхронном режиме вызывает коллбэк [`on_stop`] для запущенных
  сервисов
- губернатор удаляет информацию о плагине и его сервисах из таблиц
  `_pico_service_route` и `_pico_property`, очищая, в том числе,
  значение ключа `pending_plugin_disable`
- узлы кластера ожидают очистки `pending_plugin_disable`

[`on_stop`]: https://docs.rs/picodata-plugin/latest/picodata_plugin/plugin/interface/trait.Service.html#method.on_stop

### Удаление {: #plugin_uninstall }

Удаление плагина может быть инициировано на любом узле кластера. Каждый
отдельный узел должен сначала убедиться в том, что плагин отключен (в
таблице [`_pico_plugin`] для плагина установлено значение `enable =
false`)

Далее Raft инициирует операцию `PluginRemove { plugin_name }`. По
получению этой операции каждый узел очищает таблицы [`_pico_plugin`] и
[`_pico_service`].

<!--
### Смена конфигурации {: #plugin_reconfigure }
 -->
