Перейти к содержанию

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

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

Общие сведения

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

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

Для разработки плагинов предусмотрен Picodata SDK, предоставляющий компонент picoplugin — модульную библиотеку на языке Rust.

Структура плагина

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

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

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

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

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

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

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

См. также:

Манифест

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

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

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

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

Миграции

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

Внутри файл миграций содержит аннотации и 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 опциональна.

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

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

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

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

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

Основной сценарий использования подобной подстановки параметров — задание тиров, на которых будут созданы нужные для работы плагина таблицы. Для этого в синтаксисе создания таблицы существует возможность указать тир. Таким образом, на этапе разработки плагина можно определить, какие объекты БД нужны для работы плагина, и описать их создание в файле миграций.

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

Сервис

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

Примечание

Так как Rust не имеет стабильного ABI, то это значит что мы не можем использовать этот тип при пересечении “границы” между .so-файлом и picodata. Поэтому библиотека picoplugin предоставляет специальный тип ServiceBox который может безопасно перемещаться между .so-файлами пользователя и picodata. Для реализации такого типа используется библиотека abi_stable.

Трейт Service

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

pub type ErrorBox = Box<dyn Error + Send + Sync>;
pub type CallbackResult<T> = std::result::Result<T, ErrorBox>;
pub trait ServiceFacade {
   type CFG: DeserializeOwned;


   /// Called on the governor before the new configuration on THIS plugin
   /// is committed/applied.
   /// If an error is returned, then ALTER command is aborted.
   fn on_cfg_validate(&self, configuration: Self::CFG) -> CallbackResult<()>;


   /// Invoked on every instance after SET OPTION is committed to Raft.
   fn on_config_change(
       &mut self,
       ctx: &PicoContext,
       new_cfg: Self::CFG,
       old_cfg: Self::CFG,
   ) -> CallbackResult<()>;


   /// Called on healthcheck, if error is returned,
   /// healthcheck will be considered as failed.
   fn on_health_check(&self, context: &PicoContext) -> CallbackResult<()>;


   /// Called at instance start for an enabled plugin or on ALTER PLUGIN ENABLE
   fn on_start(&mut self, context: &PicoContext) -> CallbackResult<()>;


   /// Called on instance stop or ALTER PLUGIN DISABLE
   fn on_stop(&mut self, context: &PicoContext) -> CallbackResult<()>;


   /// Called after replicaset master is changed
   fn on_leader_change(&mut self, context: &PicoContext) -> CallbackResult<()>;
}

Регистрация сервиса

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

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

Конфигурация плагина

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

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

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

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

Настроить сервис можно двумя способами:

1. С помощью SQL-запроса. Пример:

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

2. Через RPC-вызов:

proc_service_configure(
  plugin_name: string,
  service_name:string,
  key: string,
  value: rmpv::Value
)

Конфигурация сервисов плагина хранится в глобальной служебной таблице _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

Жизненный цикл плагина

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

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

Установка

Сначала необходимо заранее скопировать на все узлы кластера .so-файлы плагина в директорию plugin_dir (задается при запуске инстанса). Например, можно сделать с помощью роли Ansible.

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

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

  • валидация манифеста на узле, с которого происходит установка
  • загрузка сериализованной информации из манифеста в системную таблицу _pico_property на время установки плагина
  • централизованная проверка наличия .so-файлов на всех живых узлах губернатором в виде вызова load_plugin_dry_run
  • при отсутствии ошибок губернатор заполняет системные таблицы _pico_plugin и _pico_service данными из манифеста с помощью CaS-операции
  • губернатор удаляет ранее загруженные данные из таблицы _pico_property
  • узлы кластера ожидают очистки таблицы _pico_property и затем проверяют таблицу _pico_plugin на наличие плагина
  • когда установка плагина подтверждена всеми узлами, выполняются миграции

Включение

Установленный в кластере плагин по умолчанию находится в выключенном состоянии: в системной таблице _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-запрос enable_plugin
  • плагин включается на узлах кластера, которые используют метаинформацию из указанных выше системных таблиц
  • в зависимости от топологии кластера (принадлежности узла к нужному тиру), на отдельных узлах валидируются и включаются сервисы плагина. Для каждого сервиса запускаются on_start callbacks, результат возвращается губернатору
  • в случае успеха губернатор обновляет системную таблицу _pico_plugin и устанавливает для плагина свойство enable = true, а также заполняет таблицу _pico_service_route (маршрутизация для RPC)
  • губернатор очищает значение ключа pending_plugin_enable
  • узлы кластера ожидают очистки pending_plugin_enable и после этого проверяют в таблице _pico_plugin свойство enable

Отключение

Отключение плагина происходит локально на отдельных узлах с последующим подтверждением через 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

Удаление

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

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