# Roles / tiers Документ переехал: - [RFC - Plugins] - [RFC - Tiers] [RFC - Plugins]: https://docs.google.com/document/d/1TSMmnxn_PqaUWjJhmckbkKEFkCzUrha_rhafKjQzVWI/edit [RFC - Tiers]: https://docs.google.com/document/d/1B363AMhx912c_YtNQGkVCXl2-dd_zl0_R2Pwx-a2A5Y/edit ::: spoiler Исторический контент ## Группы хранения Группы хранения (они же всем нам знакомые vshard groups) - это фича, которая позволяет иметь несколько кластеров vshard в рамках одного кластера Picodata. Один узел (в смысле хранения данных) не может принадлежать к разным группам хранения. Физически она выглядит так - в каждой группе хранения свое распределение бакетов по узлам хранения, а на роутерах есть несколько сущностей `vshard-router`, которые могут обращаться к своим группам хранения. Группа хранения имеет собственную схему данных. Данные принадлежащие группе хранения находятся только в ней. При этом пространство имен для схемы данных едино в рамках кластера. Это значит, что нельзя создать таблицу `MY_TABLE` одновременно в группе хранения `A` и в группе хранения `B`. ## Тиры Тир - это логическое объединение инстансов, образующее отдельную группу хранения ("группа хранения" == "vshard group"). Репликационный фактор является общим на весь тир. Принадлежность инстанса к тиру задается при деплое инстанса и НЕ может быть изменена при эксплуатации. Пример: ```bash= picodata run --tier red ``` ```console $ picodata run --help picodata-run Run the picodata instance USAGE: picodata run [OPTIONS] OPTIONS: --tier <TIER> TODO: description [env: PICODATA_TIER=] [default: storage] ``` Каждый тир соответствует отдельной группе хранения. Тиры и группы хранения соотносятся 1 к 1. К каждому тиру привязана единственная и уникальная группа хранения. ### Требования к тирам * Набор тиров определяется на этапе разработке * Расположение тиров определяется на этапе эксплуатации * В тиры можно добавлять инстансы * Из тиров можно удалять инстансы и репликасеты * В тире можно управлять фактором репликации ### Свойства тира * Репликационный фактор. * Набор включенных на нем ролей * Возможно ли делать узлы из этого тира `raft-voter` * Группа параметров для `box.cfg` **Важно**: тир не хранит желаемое количество репликасетов. Это позволяет нам делать автоматическое масштабирование (и вверх, и вниз) развертыванием инстансов внешним оркестраторов. Это значит, что условный kubernetes может запустить еще 10 узлов Picodata с указанным тиром и Picodata автоматически его расширит. ### Первоначальная конфигурация тиров При первом запуске кластера инстансам указывается параметр `--init-cfg`, который описывает изначально существующие в кластере тиры и другие параметры кластера. Этот параметр используется только узлом `bootstrap-leader`, который переносит эту конфигурацию в глобальные спейсы. ```bash= picodata run --init-cfg cfg.yml --tier red ``` ```yml= # init.cfg # ======== tier: - name: red replication_factor: 3 raft_voter: false memtx_memory: 5 * 1024 * 1024 * 1024 # other_box_cfg_opts: ... role: - storage - name: blue replication_factor: 3 raft_voter: false memtx_memory: 5 * 1024 * 1024 * 1024 role: - storage - name: purple replication_factor: 1 raft_voter: false memtx_memory: 128 * 1024 * 1024 role: - api-gateway - name: yellow replication_factor: 2 raft_voter: false role: - search-index - name: green replication_factor: 2 raft_voter: false role: - kafka-consumer - name: raft-voters replication_factor: 1 raft-voter: true role: [] ``` ### Пример деплоя кластера с набором тиров 1. У меня есть ansible роль, которая разложит Picodata на необходимых мне хостах 2. Эта же ansible роль запустит узлы Picodata, указав тиры узлов при запуске каждого узла. Без указания тира, каждый узел входит в тир по умолчанию - `storage` 4. Первоначальная конфигурация тиров указывается при создании группы (по аналогии с `--init-replication-factor`). Конфигурация тиров включает в себя имя и `replication-factor` 3. В join-запросе при добавлении инстанса в кластер указывается тир. 4. При формировании репликасетов `governor`, кроме `failure-domain` учитывает их тиры. Инстансы из разных тиров в репликасет не объединяются. ## Breaking changes В интерфейс `create_space` будет добавлен параметр `tier`. Пример: ```lua pico.create_space({ tier = 'my_tier', name = 'my_space', format = ..., }) ``` ## Picodata Roles Роль - это набор кода, который исполняется в соответствии с некоторым жизненным циклом роли и позволяет выполнять пользовательскую логику внутри кластера Picodata ## Требования * Возможость включать / выключать роли на уровне тиров * Роли имеют доступ в сторадж напрямую (box.space.*) * Роль экспортирует набор RPC хендлеров. * Одну роль можно включить на множестве тиров * На одном тире можно включить множество ролей * Функционал ролей определяется на этапе разработки * Расположение ролей определяется на этапе эксплуатации * Роли должны иметь функционал зависимых ролей (своеобразных сайдкаров. Как пример - см. `metrics` в картридже) * > Pavel Yudin: Слушай, нам в механизме ролей в пикодате обязательно нужна нативная поддержка синхры. То есть мы должны дожидаться выбора лидера до применения ролей, что бы не было как сейчас. ## Пример использования Я хочу собрать кластер Picodata, который будет выполнять следующие функции: Пользовательская логика: * Считывать данные из Kafka и выполнять их обогащение * Хранить эти данные шардированно, причем хранение должно быть разделено на горячее и холодное (memtx / vinyl) * Иметь над этими данными распределенный вторичный индекс * Предоставлять REST API поверх этих данных по HTTP Технологическая часть: * Предоставлять prometheus метрики (должно быть встроено в каждый узел Picodata с возможность расширения метрик из кода ролей, а не отдельной ролью) * Предоставлять метрики для `zabbix`. Это будет реализовано через `dependency role`, которая будет сайдкаром прицеплена ко всем остальным. Я хочу разнести весь это функционал по различным узлам, чтобы * ошибка в программном коде в одной из часте приложения не приводила к выходу из строя всего приложения * перегрузка одних частей приложения (например, необходимость считать миллион сообщений из кафки) не приводила к DoS всего приложения и увеличению latency из других частей Таким образом у меня появляются 4 роли: 1. `kafka-consumer` 2. `storage` 3. `search-index` 4. `api-gateway` Проецируемые на 5 тиров (каждый тир имеет независимую схему данных) 1. red: -- hot storage - storage 2. blue: -- cold storage - storage 3. yellow -- search-index - search-index 4. green: - kafka-consumer 5. purple: - api-gateway При зачастую обращения к `search-index` будут идти по одному ключу, что приведет к высокой паразитной нагрузке и помешает производительности слоя storage [пример из проекта](https://git.picodata.io/ttq/ttq/-/issues/212). Таким образом, я хочу иметь несколько групп хранения в одной кластере Picodata (`yellow`, `blue` и `red` в нашем примере). При этом потребности в объединении ролей и групп хранения не возникает, так как на каждом узле Picodata запускается не только `vshard-storage`, но и набор из нескольких `vshard-router`, позволяющих получить доступ к любой из групп хранения. При это на этапе разработки приложения я ничего не знаю о наборе физическом расположении групп хранения, но в RPC я должен уметь указывать необходимую мне группу хранения для обращения к данным / хранимым процедурам из этого тира. Пример RPC вызова по тиру ```lua= picodata> pico.rpc_call( 'tier', bucket, 'my_stored_procedure', {1}, opts ) --- true ... ``` Пример RPC вызова по имени роли ```lua= picodata> pico.rpc_call_role( 'role_name', 'my_stored_procedure', {1}, opts ) ``` Это значит, что при деплое кластера для работоспособности RPC вызовов по тиру я должен создать соответствующие тиры. ### Как происходит деплой приложения 1. У меня есть `ansible` роль, которая разложит Picodata и `so` файлы моего приложения по узлам Picodata на необходимых мне хостах 2. Эта же `ansible` роль запустит узлы Picodata, указав тиры узлов и `--init-cfg`, где будут описаны существующие в кластере тиры и назначенные им роли 3. Picodata соберется в кластер, выберет лидера Raft и `governor`. При этом `governor` будет запущен в соответствии с конфигурацией тиров, что позволит его выносить на отдельный узел Picodata и не позволит логике отдельной роли помешать его работе (например, заблокировав `event loop`). Это похоже на логику k8s и `master` нод в нём. 4. `governor` назначит соответствующие роли инстансам, в соответствии с содержимым глобальных спейсов **Важно** отдельного `init.lua` как с Cartridge в качестве входной точки здесь нет. Его роль Picodata выполняет самостоятельно. ### Запуск роли Governor'ом https://www.websequencediagrams.com/ ``` title Initialize role actor Governor actor Worker Note over Worker: dlopen "so" files Governor->Worker: enable "storage" role Note over Worker: call "storage.init" function from "so" opened earlier Worker->Governor: ok note over Governor: CaS("update", instance, status...) Governor->Worker: enable "missing" role Note over Worker: fail calling "missing.init" function Worker->Governor: not ok note over Governor: CaS("update", instance, status...) ``` TODO: описать параметр, который будет указывать Picodata, где искать `so` файлы с ролями ```bash= $ picodata run --my-app-folder . ``` governor> call(my_role_init) ### Что такое роль - Роль - это ... - Роль на инстансе имеет статус включна / выключена (enabled / disabled) - Роль имеет набор колбеков. - Роль имеет набор RPC хендлеров - Апгрейд приложений делается в 2 этапа - сначала обновляется рантайм (инстансы рестартуют с новым кодом), потом обновляется схема, в конце можно заперсисть признак что все готово и новой версией можно пользоваться. ### Successful reload - sequencediagram.org ``` title Reload cluster participantgroup #red Governor tier participant former governor participant governor end participantgroup #lightgreen Replicaset participant s-1-1 participant s-1-2 end note left of s-1-1: leader note right of s-1-2: replica [->former governor:Reload cluster former governor->governor: reload former governor->governor: become governor former governor->former governor: reload governor->s-1-2: reload governor->s-1-2: ready check s-1-2->governor: ready ok governor->s-1-1: demote governor->s-1-2: promote governor->s-1-1: reload governor->s-1-1: ready check s-1-1->governor: ready ok governor->s-1-1: promote governor->s-1-2: denote governor->s-1-1: apply DDL ``` ### Errorneous reload ### Как происходит выключение роли 1. Gracefull * Увидев, что текущая топология требует выключения роли на узле, `governor` зовет на инстансах синхронный метод `stop` соответствующей роли. Этот метод должен также вызывать методы `stop` у всех зависимых ролей 2. Force * `Governor` обнаруживает, что инстанс с выбранной ролью не отвечает на `healthcheck` * `Governor` исключает инстанс из кластера (в моем идеальном мире, кажется, что RPC фреймворк на всех узлах должен начать выкидывать exception при попытке обращения к этому инстансу) * Должен ли вызываться `stop`? ### Role callbacks 1. `once_ddl` - упорядоченный набор DDL операций, который необходимо выполнить на кластере. - `governor` парсит это набор и строго упорядоченно вставляет эти операции в Raft - каждый новый узел получает и применяет эти операции локально Ex: ```Lua { once_ddl = { [1] = "CREATE TABLE T", [2] = "ALTER TABLE T", } }; ``` 2. `async fn init()` - исполняется на узле, где запущена роль. Этот колбек дергает говернор по сети в процессе обработки `target_grade: Online`. 3. `cas_apply` - исполняется на узле, где запущена роль 4. `stop` - исполняется на узле, где запущена роль 5. `version` - отдает semver роли ### API предоставляемые коду в роли 1. HTTP sever 2. RPC server / client 3. Background jobs container (должны ли мы повесить на отдельные коллбеки?) Конфигурация ролей применяется через Governor, который и инициализирует роли ## Создание роли как пользователь Для работы роли пользователю необходимо предоставить shared lib (so/dylib) файл который экспортирует необходимые роли. Каждая роль представляет собой rust структуру реализующую `trait picorole::Role`. `picorole::Role` по сути набор callback'ов, реализацию которых мы ждем от пользователя. Кроме того, данная структура может хранить состояние (например, коннект к kafka, in-memory cache и т.д.). Shared lib может экспортировать как одну так и несколько ролей. Кроме .so файла на вход к `picodata` поступает файл с конфигурацией (через CaS), описывающий какие роли необходимо запустить и зависимости ролей (зависимости возможно лучше описать статически в .so файле а не в конфигурации). Пример проекта который реализует 2 роли в одном бинарном файле: ```rust= // экспорт всех макросов и прочего use picorole::prelude::*; struct Role1 { _kafka_connection: Option<bool>, other_state: Vec<String>, } impl Role for Role1 { fn name(&self) -> &str { "role_1" } // init callback fn init(&mut self, _context: &PicoContext) { println!("hello (role 1)"); println!("some state: {:?}", self.other_state); } // stop callback fn stop(&mut self, _context: &PicoContext) { println!("bye (role 1)"); } } // функция-конструктор для роли 1 #[role_constructor] fn role_1_ctor() -> Role1 { Role1 { _kafka_connection: None, other_state: vec!["hello".to_string(), "world".to_string()], } } struct Role2 {} impl Role for Role2 { fn name(&self) -> &str { "role_2" } // init callback fn init(&mut self, _context: &PicoContext) { println!("hello (role 2)"); } // stop callback fn stop(&mut self, _context: &PicoContext) { println!("bye (role 2)"); } } // функция-конструктор для роли 2 #[role_constructor] fn role_2_ctor() -> Role2 { Role2 {} } ``` ### Дефолтная реализация callback'ов. Для некоторых callback'ов может быть стандартная реализация (обычно пустой callback) это решается через дефолтные методы trait'а. ## Детали реализации ### Picorole Библиотека `picorole` предоставляет набор макросов и типов для создания role. Кроме того `picorole` экспортирует функции необходимые picodata для импорта ролей. #### Rust to Rust ABI По сути `.so` файл представляет набор плагинов для Picodata, каждый из которых - это роль. В настоящий момент Rust не имеет стабильного ABI, это значит что нет гарантии что Picodata "поймет" типы которые будут выгружены из .so файла. Варианты получить такую гарантию: 1) Lock версии rust которым компилируются плагины на тот же которым скомпилирована Picodata. Этот вариант нам не подходит т.к. сильно ограничивает пользователя и вносит дополнительную сложность при соблюдении совместимости между разными версиями picodata и .so файлами. 2) Использовать C ABI. C ABI является стабильным, поэтому мы можем его использовать. К сожалению C ABI заставляет использовать только простейшие типы, что не подходит для нас, т.к. на границе между picodata и .so мы хотим использовать `trait objects` (т.е. реализации trait'a Role). 3) Использовать сторонние крэйты предоставляющие стабильный ABI. Реализация ролей использует этот вариант, в частности крэйт https://github.com/rodrimati1992/abi_stable_crates/. Данный крэйт позволяет реализовать переносимые trait objects и предоставляет stable реализацию различных rust типов (Vec -> RVec, Result -> RResult, &str -> RStr и тд). Одна из важных задач реализации ролей - скрыть эти подробности для пользователя (разработчика роли) и позволить ему использовать привычные rust типы, делая необходимые преобразования прозрачно. Итоговым выбран вариант №3 - `abi_stable_crates` #### Trait Role Trait role которые необходимо реализовать пользователю имеет вид: ```rust= pub trait RoleFacade { fn name(&self) -> &str; fn init(&mut self, context: &PicoContext) { } fn stop(&mut self, context: &PicoContext) { } } ``` Как можно заметить данный тип не является stable (его нельзя использовать при переходе границы Picodata -> .so file). Поэтому данный trait экспортируется для пользователя как `Role` и при запросе со стороны Picodata преобразуется в stable trait object (детали преобразования будут описаны ниже). #### Role constuctor Для того чтобы picodata знала о том что .so файл экспортирует конкретную роль пользователю необходимо создать функции-конструктор и пометить ее специальным макросом `#[role_constructor]`. Пример такой функции: ```rust= #[role_constructor] fn role_2_ctor() -> Role2 { Role2 {} } ``` При вызове этого конструктора на стороне picodata происходит следующее: 1) Создается функция которая: 1.1 Создает структуру Role2 имплементирующую trait Role. Данная структура преобзуется в TO: `Box<dyn Role>`. 1.2 Создает структуру типа RoleProxy оборачивающая trait object типа `Box<dyn Role>`. Структура RoleProxy имплементирует trait `RoleStable`, данный trait является стабильным (trait object можно использовать при переходе границы picodata -> .so file). Тип trait object для `RoleStable` называется RoleBox. 2) С помощью библиотеки linkme эта функция на этапе линковки будет помещена в массив `CONSTRUCTORS` который имеет вид `CONSTRUCTORS: [extern "C" fn() -> RoleBox]`. В будущем picodata может обратится к этому массиву, выгрузить все конструкторы ролей и создать необходимую роль. #### Экспортируемые функции picorole Для того чтобы picodata могла получить доступ к массиву `CONSTRUCTORS` в .so файле `picorole` экспортирует функцию `constructors`. Пример: ### На стороне picodata Picodata выполняет функцию загрузки роли, на входе есть shared lib файл (so/dylib) (в будущем кроме shared lib файла будет и файл с конфигурацией). Пример импорта ролей из .so файла: ```rust= unsafe fn get_roles_fn<'a>( l: &'a Library, symbol: &str, ) -> Result< Symbol<'a, extern "C" fn() -> RSlice<'static, extern "C" fn() -> RoleBox>>, libloading::Error, > { l.get(symbol.as_bytes()) } unsafe { let path_to_lib = "./target/debug/libproducer.so"; let lib = Library::new(path_to_lib).unwrap(); let func = get_roles_fn(&lib, "constructors").unwrap(); let role_ctors = func(); role_ctors.iter().for_each(|role_ctor| { let mut role = role_ctor(); println!("role: {}", role.name()); println!("init function execute result:"); role.init(&PicoContext::empty()); println!("stop function execute result:"); role.stop(&PicoContext::empty()); println!("--------------------------------------"); }); }; ``` Вывод (пример файла [выше](#Создание-роли-как-пользователь)): ``` role: role_1 init function execute result: hello (role 1) some state: ["hello", "world"] stop function execute result: bye (role 1) -------------------------------------- role: role_2 init function execute result: hello (role 2) stop function execute result: bye (role 2) -------------------------------------- ``` ## Вопросы: 1. Как роль storage будет отличать себя по `cold` и `hot`? Тиры должны быть описаны при разработке приложения, потому что tier - аналог vshard группы. Как роль `storage` будет отличать себя в кластерном конфиге? Определять свой тир и смотреть в соответствующие секции? А почему не определить их просто как две разные роли, которые будут переиспользовать свой код? 2. А можем мы вместе с конфигурацией тиров и конфигурацию ролей бахать при инициализации кластера? Наверное да, она просто запишется в global. А зачем? 3. Какие API будут доступны пользовательскому коду в ролях? 4. Прописать механизм импорта `so` файла ролей 5. Прописать механизм сборки ролей 6. Описать локальный запуск Picodata с ролями (вот бы еще просто механизм локального запуска Picodata увидеть) 7. Как будут выглядеть интеграционные тесты на роли? Нужно описать cluster-helpers на Rust / Lua для ролей 8. Описать обвязку OpenTelemetry для RPC фреймворка 9. Описать как мы будем шиппить готовые роли Picodata (стандартизированные kafka importer, например) 10. Как шиппить и как делать встраивание UI ролей 11. Где в UI и CLI посмотреть версии существующих ролей? Как версионировать роли? Видимо, при написании кода роль должна экспортировать свою версию 12. Неплохо бы иметь некий контейнер с глобальными "сервисами" время жизни которых привязано к времени жизни роли. В коде выглядит как то так: ```rust fn my_fn() { ... let worker = service_locator.role('my_role').get('worker_x'); worker.update_cfg(new_cfg); ... } ``` ### Заметки со встречи 2023-07-12 (d.koltsov, y.dynnikov) ```diff --- 1.txt 2023-07-12 16:28:17.640478796 +0300 +++ 2.txt 2023-07-12 16:29:20.536662759 +0300 @@ -1,112 +1,141 @@ + 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_space 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- -- {'is_nullable': false, 'name': 'id', 'type': 'unsigned'} -- {'is_nullable': false, 'name': 'name', 'type': 'string'} -- {'is_nullable': false, 'name': 'distribution', 'type': 'array'} -- {'is_nullable': false, 'name': 'format', 'type': 'array'} -- {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'} -- {'is_nullable': false, 'name': 'operable', 'type': 'boolean'} +- {'name': 'id', 'type': 'unsigned', 'is_nullable': false} +- {'name': 'name', 'type': 'string', 'is_nullable': false} +- {'name': 'engine', 'type': 'string', 'is_nullable': false} +- {'name': 'distribution', 'type': 'array', 'is_nullable': false} +- {'name': 'format', 'type': 'array', 'is_nullable': false} +- {'name': 'schema_version', 'type': 'unsigned', 'is_nullable': false} +- {'name': 'operable', 'type': 'boolean', 'is_nullable': false} +- {'name': 'tier', 'type': 'string', 'is_nullable': false} +- {'name': 'doc', 'type': 'string', 'is_nullable': true} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_index 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'space_id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'name', 'type': 'string'} - {'is_nullable': false, 'name': 'local', 'type': 'boolean'} - {'is_nullable': false, 'name': 'parts', 'type': 'array'} - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'operable', 'type': 'boolean'} - {'is_nullable': false, 'name': 'unique', 'type': 'boolean'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_peer_address 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'raft_id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'address', 'type': 'string'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_instance 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'instance_id', 'type': 'string'} -- {'is_nullable': false, 'doc': 'foobar', 'name': 'instance_uuid', 'type': 'string'} +- {'is_nullable': false, 'name': 'instance_uuid', 'type': 'string', 'doc': 'foobar'} - {'is_nullable': false, 'name': 'raft_id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'replicaset_id', 'type': 'string'} - {'is_nullable': false, 'name': 'replicaset_uuid', 'type': 'string'} - {'is_nullable': false, 'name': 'current_grade', 'type': 'array'} - {'is_nullable': false, 'name': 'target_grade', 'type': 'array'} - {'is_nullable': false, 'name': 'failure_domain', 'type': 'map'} ... +_pico_tier +- {'name': 'tier_id', 'type': 'string', 'is_nullable': false, + doc: 'Unique alias, example: red / green / hot-storage / search-index'} +- {'name': 'raft_voter', 'type': 'boolean', 'is_nullable': false} + + +alt. 1 +- {'name': 'replication_factor', 'type': 'unsigned', 'is_nullable': false} +- {'name': 'raft_voter', 'type': 'boolean', 'is_nullable': false} +- {'name': 'roles', 'type': 'array', 'is_nullable': false} +- {'name': 'doc', 'type': 'string', 'is_nullable': true} +- {'name': 'box_cfg', 'type': 'map', 'is_nullable': false} +- {'name': 'vshard_cfg', 'type': 'map', 'is_nullable': false} +- {'name': 'vshard_bootstrapped', 'type': 'boolean', is_nullable: false} + +alt. 2 -- ни в коем случае +- {'name': 'key', 'type': 'string', 'is_nullable': false} +- {'name': 'value', 'type': 'any', 'is_nullable': false} +- {'name': 'doc', 'type': 'string', 'is_nullable': true} + +alt. 3 +- {'name': 'properties', 'type': 'array', 'is_nullable': false} + - [{key: "replication_factor", value: 2, doc: ""}] + 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_property 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'key', 'type': 'string'} - {'is_nullable': false, 'name': 'value', 'type': 'any'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_replicaset 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'replicaset_id', 'type': 'string'} - {'is_nullable': false, 'name': 'replicaset_uuid', 'type': 'string'} - {'is_nullable': false, 'name': 'master_id', 'type': 'string'} - {'is_nullable': false, 'name': 'weight', 'type': 'array'} +- {'is_nullable': false, 'name': 'tier', 'type': 'string'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _raft_log 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'entry_type', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'index', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'term', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'data', 'type': 'any'} - {'is_nullable': false, 'name': 'context', 'type': 'any'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _raft_state 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'key', 'type': 'string'} - {'is_nullable': false, 'name': 'value', 'type': 'any'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_user 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'name', 'type': 'string'} - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'auth', 'type': 'array'} ... 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> _pico_privilege 2023-07-12 16:25:33.270 [516886] main/103/interactive/dump I> --- - {'is_nullable': false, 'name': 'user_id', 'type': 'unsigned'} - {'is_nullable': false, 'name': 'object_type', 'type': 'string'} - {'is_nullable': false, 'name': 'object_name', 'type': 'string'} - {'is_nullable': false, 'name': 'privilege', 'type': 'string'} - {'is_nullable': false, 'name': 'schema_version', 'type': 'unsigned'} ... """\ +-----+----+-----+--------+ |index|term| lc |contents| +-----+----+-----+--------+ | 1 | 1 |1.0.1|Insert({_pico_peer_address}, [1,"127.0.0.1:{p}"])| +| 1 | 1 |1.0.1|Insert({_pico_tier}, ["red", {"replication_factor":1, "enabled_roles": [], ...}])| // +| 9 | 2 |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","red"(tier),"i1"(leader_id),[0.0,"Auto","Initial"]])| | 2 | 1 |1.0.2|Insert({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Offline",0],{b}])| -| 3 | 1 |1.0.3|Insert({_pico_property}, ["replication_factor",1])| +| 3 | 1 |1.0.3|Insert({_pico_property}, ["replication_factor",1])| // Del | 4 | 1 |1.0.4|Insert({_pico_property}, ["global_schema_version",0])| | 5 | 1 |1.0.5|Insert({_pico_property}, ["next_schema_version",1])| | 6 | 1 | |AddNode(1)| | 7 | 2 | |-| | 8 | 2 |1.1.1|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Offline",0],["Online",1],{b}])| -| 9 | 2 |1.1.2|Insert({_pico_replicaset}, ["r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07","i1",[0.0,"Auto","Initial"]])| | 10 | 2 |1.1.3|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Replicated",1],["Online",1],{b}])| | 11 | 2 |1.1.4|Update({_pico_replicaset}, ["r1"], [["=","weight[1]",1.0], ["=","weight[3]","UpToDate"]])| | 12 | 2 |1.1.5|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["ShardingInitialized",1],["Online",1],{b}])| -| 13 | 2 |1.1.6|Replace({_pico_property}, ["vshard_bootstrapped",true])| +| 13 | 2 |1.1.6|Replace({_pico_property}, ["vshard_bootstrapped",true])| // TODO | 14 | 2 |1.1.7|Replace({_pico_instance}, ["i1","68d4a766-4144-3248-aeb4-e212356716e4",1,"r1","e0df68c5-e7f9-395f-86b3-30ad9e1b7b07",["Online",1],["Online",1],{b}])| +-----+----+-----+--------+ """ ``` :::