Developer’s guide
For a quick start, skip the details below and jump right away to the Cartridge getting started guide.
For a deep dive into what you can develop with Tarantool Cartridge, go on with the Cartridge developer’s guide.
Короче говоря, чтобы разработать и запустить приложение, вам необходимо выполнить следующие шаги:
- Install Tarantool Cartridge and other components of the development environment.
- Create a project.
- Разработать приложение. Если это приложение с поддержкой кластеров, реализуйте его логику в виде отдельной (пользовательской) кластерной роли, чтобы инициализировать базу данных в кластерной среде.
- Развернуть приложение на сервере или серверах. Это включает в себя настройку и запуск экземпляров.
- Если это приложение с поддержкой кластеров, развернуть кластер.
В следующих разделах подробно описывается каждый из этих шагов.
To set up your development environment, create a project using the Tarantool Cartridge project template. In any directory, say:
$ cartridge create --name <app_name> /path/to/
This will automatically set up a Git repository in a new /path/to/<app_name>/
directory, tag it with version 0.1.0
,
and put the necessary files into it.
В этом Git-репозитории можно разработать приложение (просто редактируя файлы из шаблона), подключить необходимые модули, а затем с легкостью упаковать все для развертывания на своих серверах.
The project template creates the <app_name>/
directory with the following
contents:
- файл
<имя_приложения>-scm-1.rockspec
, где можно указать зависимости приложения. - скрипт
deps.sh
, который решает проблемы с зависимостями из файла.rockspec
. - файл
init.lua
, который является точкой входа в ваше приложение. - файл
.git
, необходимый для Git-репозитория. - файл
.gitignore
, чтобы не учитывать ненужные файлы. - файл
env.lua
, который устанавливает общие пути для модулей, чтобы приложение можно было запустить из любой директории. - файл
custom-role.lua
, который представляет собой объект-заполнитель для пользовательской кластерной роли.
The entry point file (init.lua
), among other things, loads the cartridge
module and calls its initialization function:
...
local cartridge = require('cartridge')
...
cartridge.cfg({
-- cartridge options example
workdir = '/var/lib/tarantool/app',
advertise_uri = 'localhost:3301',
cluster_cookie = 'super-cluster-cookie',
...
}, {
-- box options example
memtx_memory = 1000000000,
... })
...
Вызов cartridge.cfg()
позволяет управлять экземпляром через административную консоль, но не вызывает box.cfg()
для настройки экземпляров.
Предупреждение
Запрещается вызывать функцию box.cfg()
.
Сам кластер сделает это за вас, когда придет время:
- загрузить текущий экземпляр, когда вы:
- выполните
cartridge.bootstrap()
в административной консоли, или - нажмете Create (Создать) в веб-интерфейсе;
- выполните
- присоединить экземпляр к существующему кластеру, когда вы:
- выполните
cartridge.join_server({uri = ''uri_другого_экземпляра'})
в консоли, или - нажмете Join (Присоединить – к уже существующему набору реплик) или Create (Создать – для нового набора реплик) в веб-интерфейсе.
- выполните
Обратите внимание, что вы можете указать cookie для кластера (параметр cluster_cookie
), если необходимо запустить несколько кластеров в одной сети. Cookie может представлять собой любое строковое значение.
Now you can develop an application that will run on a single or multiple independent Tarantool instances (e.g. acting as a proxy to third-party databases) – or will run in a cluster.
If you plan to develop a cluster-aware application, first familiarize yourself with the notion of cluster roles.
Cluster roles are Lua modules that implement some specific functions and/or logic. In other words, a Tarantool Cartridge cluster segregates instance functionality in a role-based way.
Since all instances running cluster applications use the same source code and are aware of all the defined roles (and plugged modules), you can dynamically enable and disable multiple different roles without restarts, even during cluster operation.
Note that every instance in a replica set performs the same roles and you cannot enable/disable roles individually on some instances. In other words, configuration of enabled roles is set up per replica set. See a step-by-step configuration example in this guide.
В модуль cartridge
входят две встроенные роли, которые реализуют автоматический шардинг:
vshard-router
обрабатывает ресурсоемкие вычисления вvshard
: направляет запросы к узлам хранения данных.vshard-storage
работает с большим количеством транзакций вvshard
: хранит подмножество набора данных и управляет им.Примечание
For more information on sharding, see the vshard module documentation.
With the built-in and custom roles, you can develop applications with separated compute and transaction handling – and enable relevant workload-specific roles on different instances running on physical servers with workload-dedicated hardware.
You can implement custom roles for any purposes, for example:
- определять хранимые процедуры;
- implement extra features on top of
vshard
; - полностью обойтись без
vshard
; - внедрить одну или несколько дополнительных служб, таких как средство уведомления по электронной почте, репликатор и т.д.
To implement a custom cluster role, do the following:
Take the
app/roles/custom.lua
file in your project as a sample. Rename this file as you wish, e.g.app/roles/custom-role.lua
, and implement the role’s logic. For example:-- Implement a custom role in app/roles/custom-role.lua #!/usr/bin/env tarantool local role_name = 'custom-role' local function init() ... end local function stop() ... end return { role_name = role_name, init = init, stop = stop, }
Here the
role_name
value may differ from the module name passed to thecartridge.cfg()
function. If therole_name
variable is not specified, the module name is the default value.Примечание
Имена ролей должны быть уникальными, поскольку невозможно зарегистрировать несколько ролей с одним именем.
Зарегистрируйте новую роль в кластере, изменив вызов
cartridge.cfg()
в файле входа в приложениеinit.lua
:-- Register a custom role in init.lua ... local cartridge = require('cartridge') ... cartridge.cfg({ workdir = ..., advertise_uri = ..., roles = {'custom-role'}, }) ...
где
custom-role
(пользовательская роль) – это название загружаемого Lua-модуля.
The role module does not have required functions, but the cluster may execute the following ones during the role’s life cycle:
init()
– это функция инициализации роли.Inside the function’s body you can call any box functions: create spaces, indexes, grant permissions, etc. Here is what the initialization function may look like:
local function init(opts) -- The cluster passes an 'opts' Lua table containing an 'is_master' flag. if opts.is_master then local customer = box.schema.space.create('customer', { if_not_exists = true } ) customer:format({ {'customer_id', 'unsigned'}, {'bucket_id', 'unsigned'}, {'name', 'string'}, }) customer:create_index('customer_id', { parts = {'customer_id'}, if_not_exists = true, }) end end
Примечание
- Neither
vshard-router
norvshard-storage
manage spaces, indexes, or formats. You should do it within a custom role: add abox.schema.space.create()
call to your first cluster role, as shown in the example above. - Тело функции заключено в условный оператор, который позволяет вызывать функции
box
только на мастерах. Это предотвращает конфликты репликации, так как данные автоматически передаются на реплики.
- Neither
stop()
is the role’s termination function. Implement it if initialization starts a fiber that has to be stopped or does any job that needs to be undone on termination.validate_config()
andapply_config()
are functions that validate and apply the role’s configuration. Implement them if some configuration data needs to be stored cluster-wide.
Next, get a grip on the role’s life cycle to implement the functions you need.
Можно заставить кластер применить некоторые другие роли, если включена пользовательская роль.
Например:
-- Role dependencies defined in app/roles/custom-role.lua local role_name = 'custom-role' ... return { role_name = role_name, dependencies = {'cartridge.roles.vshard-router'}, ... }
Здесь роль vshard-router
будет инициализирована автоматически для каждого экземпляра, в котором включена роль custom-role
.
Для наборов реплик с ролью vshard-storage
можно задавать группы. Например, группы hot
и cold
предназначены для независимой обработки горячих и холодных данных.
Группы указаны в конфигурации кластера:
-- Specify groups in init.lua
cartridge.cfg({
vshard_groups = {'hot', 'cold'},
...
})
Если ни одна группа не указана, кластер предполагает, что все наборы реплик входят в группу default
(по умолчанию).
Если включены несколько групп, каждый набор реплик с включенной ролью vshard-storage
должен быть назначен в определенную группу. Эту настройку нельзя изменить впоследствии.
Есть еще одно ограничение – нельзя добавлять группы динамически (такая возможность появится в будущих версиях).
Finally, mind the syntax for router access.
Every instance with a vshard-router
role enabled initializes multiple
routers. All of them are accessible through the role:
local router_role = cartridge.service_get('vshard-router')
router_role.get('hot'):call(...)
If you have no roles specified, you can access a static router as before (when Tarantool Cartridge was unaware of groups):
local vhsard = require('vshard')
vshard.router.call(...)
However, when using the current group-aware API, you must call a static router with a colon:
local router_role = cartridge.service_get('vshard-router')
local default_router = router_role.get() -- or router_role.get('default')
default_router:call(...)
The cluster displays the names of all custom roles along with the built-in vshard-*
roles in the web interface.
Cluster administrators can enable and disable them for particular instances –
either via the web interface or via the cluster
public API.
For example:
cartridge.admin.edit_replicaset('replicaset-uuid', {roles = {'vshard-router', 'custom-role'}})
If you enable multiple roles on an instance at the same time, the cluster first
initializes the built-in roles (if any) and then the custom ones (if any) in the
order the latter were listed in cartridge.cfg()
.
If a custom role has dependent roles, the dependencies are registered and validated first, prior to the role itself.
Кластер вызывает функции роли в следующих случаях:
- Функция
init()
обычно выполняется один раз: либо когда администратор включает роль, либо при перезапуске экземпляра. Как правило, достаточно один раз включить роль. - Функция
stop()
– только когда администратор отключает роль, а не во время завершения работы экземпляра. - Функция
validate_config()
: сначала до автоматического вызова box.cfg()` (инициализация базы данных), а затем при каждом обновлении конфигурации. - Функция
apply_config()
– при каждом обновлении конфигурации.
As a tryout, let’s task the cluster with some actions and see the order of executing the role’s functions:
- Присоединение экземпляра или создание набора реплик (в обоих случаях с включенной ролью):
validate_config()
init()
apply_config()
- Перезапуск экземпляра с включенной ролью:
validate_config()
init()
apply_config()
- Отключение роли:
stop()
. - При вызове
cartridge.confapplier.patch_clusterwide()
:validate_config()
apply_config()
- При запущенном восстановлении после отказа:
validate_config()
apply_config()
Учитывая вышеописанное поведение:
- Функция
init()
может:- Вызывать функции
box
. - Запускать файбер, и в таком случае функция
stop()
должна позаботиться о завершении работы файбера. - Настраивать встроенный HTTP-сервер.
- Выполнять любой код, связанный с инициализацией роли.
- Вызывать функции
- The
stop()
functions must undo any job that needs to be undone on role’s termination. - Функция
validate_config()
должна валидировать любые изменения конфигурации. - Функция
apply_config()
может выполнять любой код, связанный с изменением конфигурации, например, следить за файберомexpirationd
.
The validation and application functions together allow you to change the cluster-wide configuration as described in the next section.
Доступны следующие операции:
Хранить настройки пользовательских ролей в виде разделов в конфигурации на уровне кластера, например:
# in YAML configuration file my_role: notify_url: "https://localhost:8080"
-- in init.lua file local notify_url = 'http://localhost' function my_role.apply_config(conf, opts) local conf = conf['my_role'] or {} notify_url = conf.notify_url or 'default' end
Download and upload cluster-wide configuration using the web interface or API (via GET/PUT queries to
admin/config
endpoint likecurl localhost:8081/admin/config
andcurl -X PUT -d "{'my_parameter': 'value'}" localhost:8081/admin/config
).Utilize it in your role’s
apply_config()
function.
Каждый экземпляр в кластере хранит копию конфигурационного файла в своей рабочей директории (которую можно задать с помощью cartridge.cfg({workdir = ...})
):
/var/lib/tarantool/<instance_name>/config.yml
для экземпляров, развернутых из RPM-пакетов, под управлениемsystemd
./home/<username>/tarantool_state/var/lib/tarantool/config.yml
for instances deployed from tar+gz archives.
The cluster’s configuration is a Lua table, downloaded and uploaded as YAML. If some application-specific configuration data, e.g. a database schema as defined by DDL (data definition language), needs to be stored on every instance in the cluster, you can implement your own API by adding a custom section to the table. The cluster will help you spread it safely across all instances.
Such section goes in the same file with topology-specific
and vshard
-specific sections that the cluster generates automatically.
Unlike the generated, the custom section’s modification, validation, and
application logic has to be defined.
Самый распространенный способ заключается в том, чтобы:
validate_config(conf_new, conf_old)
для валидации изменений, сделанных в новой конфигурации (conf_new
) по отношению к старой конфигурации (conf_old
).apply_config(conf, opts)
для выполнения любого кода, связанного с изменениями конфигурации. Входными данными для этой функции будут применяемая конфигурация (conf
, которая и есть новая конфигурация, проверенная чуть ранее с помощьюvalidate_config()
), а также параметры (аргументopts
включает в себя описываемый ниже логический флагis_master
).
Важно
Функция validate_config()
должна обнаружить все проблемы конфигурации, которые могут привести к ошибкам apply_config()
. Для получения дополнительной информации см. следующий раздел.
When implementing validation and application functions that call box
ones for some reason, mind the following precautions:
Жизненный цикл роли не предполагает, что кластер автоматически вызовет
box.cfg()
до вызоваvalidate_config()
.If the validation function calls any
box
functions (e.g., to check a format), make sure the calls are wrapped in a protective conditional statement that checks ifbox.cfg()
has already happened:-- Inside the validate_config() function: if type(box.cfg) == 'table' then -- Here you can call box functions end
Unlike the validation function,
apply_config()
can callbox
functions freely as the cluster applies custom configuration after the automaticbox.cfg()
call.However, creating spaces, users, etc., can cause replication collisions when performed on both master and replica instances simultaneously. The appropriate way is to call such
box
functions on masters only and let the changes propagate to replicas automatically.По выполнении
apply_config(conf, opts)
кластер передает флагis_master
в таблицеopts
, который можно использовать для заключения функций изbox
в защитный условный оператор, если они могут вызвать конфликт:-- Inside the apply_config() function: if opts.is_master then -- Here you can call box functions end
Рассмотрим следующий код как часть реализации модуля роли (custom-role.lua
):
#!/usr/bin/env tarantool
-- Custom role implementation
local cartridge = require('cartridge')
local role_name = 'custom-role'
-- Modify the config by implementing some setter (an alternative to HTTP PUT)
local function set_secret(secret)
local custom_role_cfg = cartridge.confapplier.get_deepcopy(role_name) or {}
custom_role_cfg.secret = secret
cartridge.confapplier.patch_clusterwide({
[role_name] = custom_role_cfg,
})
end
-- Validate
local function validate_config(cfg)
local custom_role_cfg = cfg[role_name] or {}
if custom_role_cfg.secret ~= nil then
assert(type(custom_role_cfg.secret) == 'string', 'custom-role.secret must be a string')
end
return true
end
-- Apply
local function apply_config(cfg)
local custom_role_cfg = cfg[role_name] or {}
local secret = custom_role_cfg.secret or 'default-secret'
-- Make use of it
end
return {
role_name = role_name,
set_secret = set_secret,
validate_config = validate_config,
apply_config = apply_config,
}
После настройки конфигурации выполните одно из следующих действий:
- продолжите разработку приложения, обращая особое внимание на управление версиями;
- (необязательно) включите авторизацию в веб-интерфейсе.
- если кластер уже развернут, примените конфигурацию для всего кластера.
With the implementation showed by the example,
you can call the set_secret()
function to apply the new configuration via
the administrative console – or an HTTP endpoint if the role exports one.
Функция set_secret()
вызывает cartridge.confapplier.patch_clusterwide()
, которая производит двухфазную фиксацию транзакций:
- Исправляет активную конфигурацию в памяти: копирует таблицу и заменяет раздел
"custom-role"
в копии на раздел, который задан функциейset_secret()
. - The cluster checks if the new configuration can be applied on all instances
except disabled and expelled. All instances subject to update must be healthy
and
alive
according to the membership module. - (Фаза подготовки) Кластер передает исправленную конфигурацию. Каждый экземпляр валидирует ее с помощью функции
validate_config()
каждой зарегистрированной роли. В зависимости от результата валидации:- В случае успеха (то есть возврата значения
true
) экземпляр сохраняет новую конфигурацию во временный файл с именемconfig.prepare.yml
в рабочей директории. - (Abort phase) Otherwise, the instance reports an error and all the other instances roll back the update: remove the file they may have already prepared.
- В случае успеха (то есть возврата значения
- (Фаза фиксации) После успешной подготовки всех экземпляров кластер фиксирует изменения. Каждый экземпляр:
- Создает жесткую ссылку активной конфигурации.
- Atomically replaces the active configuration file with the prepared one. The atomic replacement is indivisible – it can either succeed or fail entirely, never partially.
- Вызывает функцию
apply_config()
каждой зарегистрированной роли.
Если любой из этих шагов не будет выполнен, в веб-интерфейсе появится ошибка рядом с соответствующим экземпляром. Кластер не обрабатывает такие ошибки автоматически, их необходимо исправлять вручную.
Такого рода исправлений можно избежать, если функция validate_config()
сможет обнаружить все проблемы конфигурации, которые могут привести к ошибкам в apply_config()
.
Кластер запускает экземпляр httpd
-сервера во время инициализации (cartridge.cfg()
). Можно привязать порт к экземпляру через переменную окружения:
-- Get the port from an environmental variable or the default one:
local http_port = os.getenv('HTTP_PORT') or '8080'
local ok, err = cartridge.cfg({
...
-- Pass the port to the cluster:
http_port = http_port,
...
})
Чтобы использовать httpd
-экземпляр, получите к нему доступ и настройте маршруты в рамках функции init()
для какой-либо роли (например, для роли, которая предоставляет API через HTTP):
local function init(opts)
...
-- Get the httpd instance:
local httpd = cartridge.service_get('httpd')
if httpd ~= nil then
-- Configure a route to, for example, metrics:
httpd:route({
method = 'GET',
path = '/metrics',
public = true,
},
function(req)
return req:render({json = stat.stat()})
end
)
end
end
For more information on using Tarantool’s HTTP server, see its documentation.
To implement authorization in the web interface of every instance in a Tarantool cluster:
Используйте модуль, к примеру,
auth
с функциейcheck_password
. Данная функция проверяет учетные данные любого пользователя, который пытается войти в веб-интерфейс.Функция
check_password
принимает имя пользователя и пароль и возвращает результат аутентификации: пройдена или нет.-- auth.lua -- Add a function to check the credentials local function check_password(username, password) -- Check the credentials any way you like -- Return an authentication success or failure if not ok then return false end return true end ...
Передайте имя используемого модуля
auth
в качестве параметра дляcartridge.cfg()
, чтобы кластер мог использовать его:-- init.lua local ok, err = cartridge.cfg({ auth_backend_name = 'auth', -- The cluster will automatically call 'require()' on the 'auth' module. ... })
Это добавит кнопку Log in (Войти) в верхний правый угол в веб-интерфейсе, но все же позволит неавторизованным пользователям взаимодействовать с интерфейсом, что удобно для тестирования.
Примечание
Кроме того, для авторизации запросов к API кластера можно использовать базовый заголовок HTTP для авторизации.
Чтобы требовать авторизацию каждого пользователя в веб-интерфейсе даже до начальной загрузки кластера, добавьте следующую строку:
-- init.lua local ok, err = cartridge.cfg({ auth_backend_name = 'auth', auth_enabled = true, ... })
С включенной аутентификацией при использовании модуля
auth
пользователь не сможет даже загрузить кластер без входа в систему. После успешного входа в систему и начальной загрузки можно включить и отключить аутентификацию для всего кластера в веб-интерфейсе, а параметрauth_enabled
игнорируется.
Tarantool Cartridge understands semantic versioning as described at semver.org. When developing an application, create new Git branches and tag them appropriately. These tags are used to calculate version increments for subsequent packing.
Например, если версия вашего приложения – 1.2.1, пометьте текущую ветку тегом 1.2.1
(с аннотациями или без них).
Чтобы получить значение текущей версии из Git, выполните команду:
$ git describe --long --tags
1.2.1-12-g74864f2
Вывод показывает, что после версии 1.2.1 было 12 коммитов. Если мы соберемся упаковать приложение на данном этапе, его полная версия будет 1.2.1-12
, а пакет будет называться <имя_приложения>-1.2.1-12.rpm
.
Запрещается использовать не семантические теги. Вы не сможете создать пакет из ветки, если последний тег не будет семантическим.
После упаковки приложения его версия сохраняется в файл VERSION
в корневой каталог пакета.
You can add a .cartridge.ignore
file to your application repository to
exclude particular files and/or directories from package builds.
For the most part, the logic is similar to that of .gitignore
files.
The major difference is that in .cartridge.ignore
files the order of
exceptions relative to the rest of the templates does not matter, while in
.gitignore
files the order does matter.
.cartridge.ignore entry | игнорирует все… |
---|---|
target/ |
папки (поскольку в конце стоит / ) под названием target рекурсивно |
target |
файлы или папки под названием target рекурсивно |
/target |
файлы или папки под названием target в самой верхней директории (поскольку в начале стоит / ) |
/target/ |
папки под названием target в самой верхней директории (в начале и в конце стоит / ) |
*.class |
файлы или папки, оканчивающиеся на .class , рекурсивно |
#comment |
ничего, это комментарий (первый символ – # ) |
\#comment |
файлы или папки под названием #comment (\\ для выделения) |
target/logs/ |
папки под названием logs , которые представляют собой поддиректорию папки под названием target |
target/*/logs/ |
папки под названием logs на два уровня ниже папки под названием target (* не включает / ) |
target/**/logs/ |
папки под названием logs где угодно в пределах папки target (** включает / ) |
*.py[co] |
файлы или папки, оканчивающиеся на .pyc или .pyo , но не на .py! |
*.py[!co] |
файлы или папки, оканчивающиеся на что угодно, кроме c или o |
*.file[0-9] |
файлы или папки, оканчивающиеся на цифру |
*.file[!0-9] |
файлы или папки, оканчивающиеся на что угодно, кроме цифры |
* |
всё |
/* |
всё в самой верхней директории (поскольку в начале стоит / ) |
**/*.tar.gz |
файлы *.tar.gz или папки, которые находятся на один или несколько уровней ниже исходной папки |
!file |
файлы и папки будут проигнорированы, даже если они подходят под другие типы |
An important concept in cluster topology is appointing a leader. Leader is an instance which is responsible for performing key operations. To keep things simple, you can think of a leader as of the only writable master. Every replica set has its own leader, and there’s usually not more than one.
Which instance will become a leader depends on topology settings and failover configuration.
An important topology parameter is the failover priority within a replica set. This is an ordered list of instances. By default, the first instance in the list becomes a leader, but with the failover enabled it may be changed automatically if the first one is malfunctioning.
When Cartridge configures roles, it takes into account the leadership map
(consolidated in the failover.lua
module). The leadership map is composed when
the instance enters the ConfiguringRoles
state for the first time. Later
the map is updated according to the failover mode.
Every change in the leadership map is accompanied by instance
re-configuration. When the map changes, Cartridge updates the read_only
setting and calls the apply_config
callback for every role. It also
specifies the is_master
flag (which actually means is_leader
, but hasn’t
been renamed yet due to historical reasons).
It’s important to say that we discuss a distributed system where every instance has its own opinion. Even if all opinions coincide, there still may be races between instances, and you (as an application developer) should take them into account when designing roles and their interaction.
The logic behind leader election depends on the failover mode: disabled, eventual, or stateful.
This is the simplest case. The leader is always the first instance in the failover priority. No automatic switching is performed. When it’s dead, it’s dead.
In the eventual
mode, the leader isn’t elected consistently. Instead, every
instance in the cluster thinks that the leader is the first healthy instance
in the failover priority list, while instance health is determined according to
the membership status (the SWIM protocol).
The member is considered healthy if both are true:
- It reports either
ConfiguringRoles
orRolesConfigured
state; - Its SWIM status is either
alive
orsuspect
.
A suspect
member becomes dead
after the failover_timout
expires.
Leader election is done as follows. Suppose there are two replica sets in the cluster:
- a single router «R»,
- two storages, «S1» and «S2».
Then we can say: all the three instances (R, S1, S2) agree that S1 is the leader.
The SWIM protocol guarantees that eventually all instances will find a common ground, but it’s not guaranteed for every intermediate moment of time. So we may get a conflict.
For example, soon after S1 goes down, R is already informed and thinks that S2 is the leader, but S2 hasn’t received the gossip yet and still thinks he’s not. This is a conflict.
Similarly, when S1 recovers and takes the leadership, S2 may be unaware of that yet. So, both S1 and S2 consider themselves as leaders.
Moreover, SWIM protocol isn’t perfect and still can produce false-negative gossips (announce the instance is dead when it’s not).
Similarly to the eventual mode, every instance composes its own leadership map,
but now the map is fetched from an external state provider
(that’s why this failover mode called «stateful»). Nowadays there are two state
providers supported – etcd
and stateboard
(standalone Tarantool instance).
State provider serves as a domain-specific key-value storage (simply
replicaset_uuid -> leader_uuid
) and a locking mechanism.
Changes in the leadership map are obtained from the state provider with the long polling technique.
All decisions are made by the coordinator – the one that holds the lock. The coordinator is implemented as a built-in Cartridge role. There may be many instances with the coordinator role enabled, but only one of them can acquire the lock at the same time. We call this coordinator the «active» one.
The lock is released automatically when the TCP connection is closed, or it
may expire if the coordinator becomes unresponsive (in stateboard
it’s set
by the stateboard’s --lock_delay
option, for etcd
it’s a part of
clusterwide configuration), so the coordinator renews the lock from
time to time in order to be considered alive.
The coordinator makes a decision based on the SWIM data, but the decision algorithm is slightly different from that in case of eventual failover:
- Right after acquiring the lock from the state provider, the coordinator fetches the leadership map.
- If there is no leader appointed for the replica set, the coordinator appoints the first leader according to the failover priority, regardless of the SWIM status.
- If a leader becomes
dead
, the coordinator makes a decision. A new leader is the first healthy instance from the failover priority list. If an old leader recovers, no leader change is made until the current leader down. Changing failover priority doesn’t affect this. - Every appointment (self-made or fetched) is immune for a while
(controlled by the
IMMUNITY_TIMEOUT
option).
In this case instances do nothing: the leader remains a leader, read-only instances remain read-only. If any instance restarts during an external state provider outage, it composes an empty leadership map: it doesn’t know who actually is a leader and thinks there is none.
An active coordinator may be absent in a cluster either because of a failure or due to disabling the role everywhere. Just like in the previous case, instances do nothing about it: they keep fetching the leadership map from the state provider. But it will remain the same until a coordinator appears.
It differs a lot depending on the failover mode.
In the disabled and eventual modes, you can only promote a leader by changing the failover priority (and applying a new clusterwide configuration).
In the stateful mode, the failover priority doesn’t make much sense (except for
the first appointment). Instead, you should use the promotion API
(the Lua cartridge.failover_promote or
the GraphQL mutation {cluster{failover_promote()}}
)
which pushes manual appointments to the state provider.
The stateful failover mode implies consistent promotion: before becoming
writable, each instance performs the wait_lsn
operation to sync up with the
previous one.
Information about the previous leader (we call it a vclockkeeper) is also stored on the external storage. Even when the old leader is demoted, it remains the vclockkeeper until the new leader successfully awaits and persists its vclock on the external storage.
If replication is stuck and consistent promotion isn’t possible, a user has two
options: to revert promotion (to re-promote the old leader) or to force it
inconsistently (all kinds of failover_promote
API has
force_inconsistency
flag).
Consistent promotion doesn’t work for replicasets with all_rw flag enabled and for single-instance replicasets. In these two cases an instance doesn’t even try to query vclockkeeper and to perform wait_lsn. But the coordinator still appoints a new leader if the current one dies.
Neither eventual nor stateful failover modes don’t protect a replicaset from the presence of multiple leaders when the network is partitioned. But fencing does. It enforces at-most-one leader policy in a replicaset.
Fencing operates as a fiber that occasionally checks connectivity with
the state provider and with replicas. Fencing fiber runs on
vclockkeepers; it starts right after consistent promotion succeeds.
Replicasets which don’t need consistency (single-instance and
all_rw
) don’t defense, though.
The condition for fencing actuation is the loss of both the state provider quorum and at least one replica. Otherwise, if either state provider is healthy or all replicas are alive, the fencing fiber waits and doesn’t intervene.
When fencing is actuated, it generates a fake appointment locally and
sets the leader to nil
. Consequently, the instance becomes
read-only. Subsequent recovery is only possible when the quorum
reestablishes; replica connection isn’t a must for recovery. Recovery is
performed according to the rules of consistent switchover unless some
other instance has already been promoted to a new leader.
These are clusterwide parameters:
mode
: «disabled» / «eventual» / «stateful».state_provider
: «tarantool» / «etcd».failover_timeout
– time (in seconds) to marksuspect
members asdead
and trigger failover (default: 20).tarantool_params
:{uri = "...", password = "..."}
.etcd2_params
:{endpoints = {...}, prefix = "/", lock_delay = 10, username = "", password = ""}
.fencing_enabled
:true
/false
(default: false).fencing_timeout
– time to actuate fencing after the check fails (default: 10).fencing_pause
– the period of performing the check (default: 2).
It’s required that failover_timeout > fencing_timeout >= fencing_pause
.
See:
Use your favorite GraphQL client (e.g. Altair) for requests introspection:
query {cluster{failover_params{}}}
,mutation {cluster{failover_params(){}}}
,mutation {cluster{failover_promote()}}
.
Like other Cartridge instances, the stateboard supports cartridge.argprase
options:
listen
workdir
password
lock_delay
Similarly to other argparse
options, they can be passed via
command-line arguments or via environment variables, e.g.:
.rocks/bin/stateboard --workdir ./dev/stateboard --listen 4401 --password qwerty
Besides failover priority and mode, there are some other private options that influence failover operation:
LONGPOLL_TIMEOUT
(failover
) – the long polling timeout (in seconds) to fetch new appointments (default: 30);NETBOX_CALL_TIMEOUT
(failover/coordinator
) – stateboard client’s connection timeout (in seconds) applied to all communications (default: 1);RECONNECT_PERIOD
(coordinator
) – time (in seconds) to reconnect to the state provider if it’s unreachable (default: 5);IMMUNITY_TIMEOUT
(coordinator
) – minimal amount of time (in seconds) to wait before overriding an appointment (default: 15).
Cartridge orchestrates a distributed system of Tarantool instances – a cluster. One of the core concepts is clusterwide configuration. Every instance in a cluster stores a copy of it.
Clusterwide configuration contains options that must be identical on every cluster node, such as the topology of the cluster, failover and vshard configuration, authentication parameters and ACLs, and user-defined configuration.
Clusterwide configuration doesn’t provide instance-specific parameters: ports, workdirs, memory settings, etc.
Конфигурация экземпляра состоит из двух наборов параметров:
Задать эти параметры можно:
- В аргументах в командной строке.
- В переменных окружения.
- В конфигурационном файле формата YAML.
- В файле
init.lua
.
Вышеуказанный порядок определяет приоритет: аргументы в командной строке замещают переменные окружения и т.д.
Независимо от того, как вы запускаете экземпляры, необходимо задать следующие параметры cartridge.cfg()
для каждого экземпляра:
advertise_uri
– либо<ХОСТ>:<ПОРТ>
, либо<ХОСТ>:
, либо<ПОРТ>
. Используется другими экземплярами для подключения. НЕ указывайте0.0.0.0
– это должен быть внешний IP-адрес, а не привязка сокета.http_port
– порт, который используется, чтобы открывать административный веб-интерфейс и API. По умолчанию:8081
. Чтобы отключить, укажите"http_enabled": False
.workdir
– директория, где хранятся все данные: файлы снимка, журналы упреждающей записи и конфигурационный файлcartridge
. По умолчанию:.
.
Если вы запустите экземпляры, используя интерфейс командной строки cartridge
или systemctl
, сохраните конфигурацию в формате YAML, например:
my_app.router: {"advertise_uri": "localhost:3301", "http_port": 8080}
my_app.storage_A: {"advertise_uri": "localhost:3302", "http_enabled": False}
my_app.storage_B: {"advertise_uri": "localhost:3303", "http_enabled": False}
С помощью интерфейса командной строки cartridge
вы можете передать путь к этому файлу в качестве аргумента командной строки --cfg
для команды cartridge start
– или же указать путь в конфигурации cartridge
(в ./.cartridge.yml
или ~/.cartridge.yml
):
cfg: cartridge.yml
run_dir: tmp/run
apps_path: /usr/local/share/tarantool
С помощью systemctl
сохраните файл в формате YAML в /etc/tarantool/conf.d/
(по умолчанию путь systemd
) или в место, указанное в переменной окружения TARANTOOL_CFG
.
Если вы запускаете экземпляры с помощью tarantool init.lua
, необходимо также передать другие параметры конфигурации в качестве параметров командной строки и переменных окружения, например:
$ tarantool init.lua --alias router --memtx-memory 100 --workdir "~/db/3301" --advertise_uri "localhost:3301" --http_port "8080"
In the file system, clusterwide configuration is represented by a file tree.
Inside workdir
of any configured instance you can find the following
directory:
config/
├── auth.yml
├── topology.yml
└── vshard_groups.yml
This is the clusterwide configuration with three default config sections –
auth
, topology
, and vshard_groups
.
Due to historical reasons clusterwide configuration has two appearances:
- old-style single-file
config.yml
with all sections combined, and - modern multi-file representation mentioned above.
Before cartridge v2.0 it used to look as follows, and this representation is
still used in HTTP API and luatest
helpers.
# config.yml
---
auth: {...}
topology: {...}
vshard_groups: {...}
...
Beyond these essential sections, clusterwide configuration may be used for storing some other role-specific data. Clusterwide configuration supports YAML as well as plain text sections. It can also be organized in nested subdirectories.
In Lua it’s represented by the ClusterwideConfig
object (a table with
metamethods). Refer to the cartridge.clusterwide-config
module
documentation for more details.
Cartridge manages clusterwide configuration to be identical everywhere
using the two-phase commit algorithm implemented in the cartridge.twophase
module. Changes in clusterwide configuration imply applying it on
every instance in the cluster.
Almost every change in cluster parameters triggers a two-phase commit: joining/expelling a server, editing replica set roles, managing users, setting failover and vshard configuration.
Two-phase commit requires all instances to be alive and healthy, otherwise it returns an error.
For more details, please, refer to the
cartridge.config_patch_clusterwide
API reference.
Beside system sections, clusterwide configuration may be used for storing some other role-specific data. It supports YAML as well as plain text sections. And it can also be organized in nested subdirectories.
Role-specific sections are used by some third-party roles, i.e. sharded-queue and cartridge-extensions.
A user can influence clusterwide configuration in various ways. You can alter configuration using Lua, HTTP or GraphQL API. Also there are luatest helpers available.
It works with old-style single-file representation only. It’s useful when there are only few sections needed.
Example:
cat > config.yml << CONFIG
---
custom_section: {}
...
CONFIG
Upload new config:
curl -v "localhost:8081/admin/config" -X PUT --data-binary @config.yml
Download it:
curl -v "localhost:8081/admin/config" -o config.yml
It’s suitable for role-specific sections only. System sections
(topology
, auth
, vshard_groups
, users_acl
) can be neither
uploaded nor downloaded.
If authorization is enabled, use the curl
option --user username:password
.
GraphQL API, by contrast, is only suitable for managing plain-text sections in the modern multi-file appearance. It is mostly used by WebUI, but sometimes it’s also helpful in tests:
g.cluster.main_server:graphql({query = [[
mutation($sections: [ConfigSectionInput!]) {
cluster {
config(sections: $sections) {
filename
content
}
}
}]],
variables = {sections = {
{
filename = 'custom_section.yml',
content = '---\n{}\n...',
}
}}
})
Unlike HTTP API, GraphQL affects only the sections mentioned in the query. All the other sections remain unchanged.
Similarly to HTTP API, GraphQL cluster {config}
query isn’t suitable for
managing system sections.
It’s not the most convenient way to configure third-party role, but it may be useful for role development. Please, refer to the corresponding API reference:
cartridge.config_patch_clusterwide
cartridge.config_get_deepcopy
cartridge.config_get_readonly
Example (from sharded-queue
, simplified):
function create_tube(tube_name, tube_opts)
local tubes = cartridge.config_get_deepcopy('tubes') or {}
tubes[tube_name] = tube_opts or {}
return cartridge.config_patch_clusterwide({tubes = tubes})
end
local function validate_config(conf)
local tubes = conf.tubes or {}
for tube_name, tube_opts in pairs(tubes) do
-- validate tube_opts
end
return true
end
local function apply_config(conf, opts)
if opts.is_master then
local tubes = cfg.tubes or {}
-- create tubes according to the configuration
end
return true
end
Cartridge test helpers provide methods for configuration management:
cartridge.test-helpers.cluster:upload_config
,cartridge.test-helpers.cluster:download_config
.
Internally they wrap the HTTP API.
Example:
g.before_all(function()
g.cluster = helpers.Cluster.new(...)
g.cluster:upload_config({some_section = 'some_value'})
t.assert_equals(
g.cluster:download_config(),
{some_section = 'some_value'}
)
end)
After you’ve developed your application locally, you can deploy it to a test or production environment.
«Deploy» includes packing the application into a specific distribution format, installing to the target system, and running the application.
Развернуть приложение Tarantool Cartridge можно четырьмя способами:
- в виде rpm-пакета (для эксплуатационной среды);
- в виде deb-пакета (для эксплуатационной среды);
- в виде архива tar+gz (для тестирования или для эксплуатационной среды, если отсутствует доступ уровня root).
- из исходных файлов (только для локального тестирования).
The choice between DEB and RPM depends on the package manager of the target OS. For example, DEB is native for Debian Linux, and RPM – for CentOS.
Упакуйте файлы приложения в распространяемый пакет:
$ cartridge pack rpm APP_NAME # -- OR -- $ cartridge pack deb APP_NAME
Будет создан RPM-пакет (например,
./my_app-0.1.0-1.rpm
) или же DEB-пакет (например,./my_app-0.1.0-1.deb
).Загрузите пакет на необходимые серверы с поддержкой
systemctl
.Установите:
$ yum install APP_NAME-VERSION.rpm # -- OR -- $ dpkg -i APP_NAME-VERSION.deb
Configure the instance(s). Create a file called
/etc/tarantool/conf.d/instances.yml
. For example:my_app: cluster_cookie: secret-cookie my_app.instance-1: http_port: 8081 advertise_uri: localhost:3301 my_app.instance-2: http_port: 8082 advertise_uri: localhost:3302
See details here.
Запустите экземпляры Tarantool’а с соответствующими службами. Например, это можно сделать, используя systemctl:
# starts a single instance $ systemctl start my_app # starts multiple instances $ systemctl start my_app@router $ systemctl start my_app@storage_A $ systemctl start my_app@storage_B
Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.
Примечание
If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:
- In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
- In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.
Упакуйте файлы приложения в распространяемый пакет:
$ cartridge pack tgz APP_NAME
Будет создан архив tar+gz (например,
./my_app-0.1.0-1.tgz
).Upload the archive to target servers, with
tarantool
and (optionally) cartridge-cli installed.Распакуйте архив:
$ tar -xzvf APP_NAME-VERSION.tgz
Configure the instance(s). Create a file called
/etc/tarantool/conf.d/instances.yml
. For example:my_app: cluster_cookie: secret-cookie my_app.instance-1: http_port: 8081 advertise_uri: localhost:3301 my_app.instance-2: http_port: 8082 advertise_uri: localhost:3302
See details here.
Запустите экземпляры Tarantool’а. Это можно сделать, используя:
tarantoolctl, например:
$ tarantool init.lua # starts a single instance
или cartridge, например:
# in application directory $ cartridge start # starts all instances $ cartridge start .router_1 # starts a single instance # in multi-application environment $ cartridge start my_app # starts all instances of my_app $ cartridge start my_app.router # starts a single instance
Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.
Примечание
If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:
- In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
- In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.
Такой метод развертывания предназначен только для локального тестирования.
Вытяните все зависимости в директорию
.rocks
:$ tarantoolctl rocks make
Configure the instance(s). Create a file called
/etc/tarantool/conf.d/instances.yml
. For example:my_app: cluster_cookie: secret-cookie my_app.instance-1: http_port: 8081 advertise_uri: localhost:3301 my_app.instance-2: http_port: 8082 advertise_uri: localhost:3302
See details here.
Запустите экземпляры Tarantool’а. Это можно сделать, используя:
tarantoolctl, например:
$ tarantool init.lua # starts a single instance
или cartridge, например:
# in application directory cartridge start # starts all instances cartridge start .router_1 # starts a single instance # in multi-application environment cartridge start my_app # starts all instances of my_app cartridge start my_app.router # starts a single instance
Если это приложение с поддержкой кластеров, далее переходите к развертыванию кластера.
Примечание
If you’re migrating your application from local test environment to production, you can re-use your test configuration at this step:
- In the cluster web interface of the test environment, click Configuration files > Download to save the test configuration.
- In the cluster web interface of the production environment, click Configuration files > Upload to upload the saved configuration.
В зависимости от способа развертывания вы можете запускать/останавливать экземпляры, используя tarantool, интерфейс командной строки cartridge или systemctl.
Запуск/остановка с помощью tarantool
С помощью tarantool
можно запустить только один экземпляр:
$ tarantool init.lua # the simplest command
Можно также задать дополнительные параметры в командной строке или в переменных окружения.
Чтобы остановить экземпляр, используйте Ctrl+C.
Запуск/остановка с помощью CLI в cartridge
С помощью интерфейса командной строки cartridge
, можно запустить один или несколько экземпляров:
$ cartridge start [APP_NAME[.INSTANCE_NAME]] [options]
Возможные параметры:
--script FILE
Точка входа в приложение. По умолчанию:
TARANTOOL_SCRIPT
, либо./init.lua
, если запуск идет из директории приложения, или же:путь_к_приложениям/:имя_приложения/init.lua
в среде с несколькими приложениями.
--apps_path PATH
- Путь к директории с приложениями при запуске из среды с несколькими приложениями. По умолчанию:
/usr/share/tarantool
. --run_dir DIR
- Директория с файлами pid и sock. По умолчанию:
TARANTOOL_RUN_DIR
или/var/run/tarantool
. --cfg FILE
- Cartridge instances YAML configuration file.
Defaults to
TARANTOOL_CFG
or./instances.yml
. Theinstances.yml
file containscartridge.cfg()
parameters described in the configuration section of this guide. --foreground
- Не в фоне.
Например:
cartridge start my_app --cfg demo.yml --run_dir ./tmp/run --foreground
Это запустит все экземпляры Tarantool’а, указанные в файле cfg
, не в фоновом режиме с принудительным использованием переменных окружения.
Если ИМЯ_ПРИЛОЖЕНИЯ
не указано, cartridge
выделит его из имени файла ./*.rockspec
.
Если ИМЯ_ЭКЗЕМПЛЯРА
не указывается, cartridge
прочитает файл cfg
и запустит все указанные экземпляры:
# in application directory
cartridge start # starts all instances
cartridge start .router_1 # start single instance
# in multi-application environment
cartridge start my_app # starts all instances of my_app
cartridge start my_app.router # start a single instance
Чтобы остановить экземпляры, выполните команду:
$ cartridge stop [APP_NAME[.INSTANCE_NAME]] [options]
Поддерживаются следующие параметры из команды cartridge start`:
--run_dir DIR
--cfg FILE
Запуск/остановка с помощью systemctl
Чтобы запустить отдельный экземпляр:
$ systemctl start APP_NAME
This will start a
systemd
service that will listen to the port specified in instance configuration (http_port
parameter).Чтобы запустить несколько экземпляров на одном или нескольких серверах:
$ systemctl start APP_NAME@INSTANCE_1 $ systemctl start APP_NAME@INSTANCE_2 ... $ systemctl start APP_NAME@INSTANCE_N
где
ИМЯ_ПРИЛОЖЕНИЯ@ЭКЗЕМПЛЯР_N
– это имя экземпляра сервисаsystemd
с инкрементным числом N (уникальным для каждого экземпляра), которое следует добавить к порту3300
для настройки прослушивания (например,3301
,3302
и т.д.).Чтобы остановить все сервисы на сервере, используйте команду
systemctl stop
и укажите имена экземпляров по одному. Например:$ systemctl stop APP_NAME@INSTANCE_1 APP_NAME@INSTANCE_2 ... APP_NAME@INSTANCE_<N>
When running instances with systemctl
, keep these practices in mind:
You can specify instance configuration in a YAML file.
This file can contain these options; see an example here).
Save this file to
/etc/tarantool/conf.d/
(the defaultsystemd
path) or to a location set in theTARANTOOL_CFG
environment variable (if you’ve edited the application’ssystemd
unit file). The file name doesn’t matter: it can beinstances.yml
or anything else you like.Here’s what
systemd
is doing further:- obtains
app_name
(andinstance_name
, if specified) from the name of the application’ssystemd
unit file (e.g.APP_NAME@default
orAPP_NAME@INSTANCE_1
); - sets default console socket (e.g.
/var/run/tarantool/APP_NAME@INSTANCE_1.control
), PID file (e.g./var/run/tarantool/APP_NAME@INSTANCE_1.pid
) andworkdir
(e.g./var/lib/tarantool/<APP_NAME>.<INSTANCE_NAME>
). Environment=TARANTOOL_WORKDIR=${workdir}.%i
Finally,
cartridge
looks across all YAML files in/etc/tarantool/conf.d
for a section with the appropriate name (e.g.app_name
that contains common configuration for all instances, andapp_name.instance_1
that contain instance-specific configuration). As a result, Cartridge optionsworkdir
,console_sock
, andpid_file
in the YAML file cartridge.cfg become useless, becausesystemd
overrides them.- obtains
The default tool for querying logs is journalctl. For example:
# show log messages for a systemd unit named APP_NAME.INSTANCE_1 $ journalctl -u APP_NAME.INSTANCE_1 # show only the most recent messages and continuously print new ones $ journalctl -f -u APP_NAME.INSTANCE_1
If really needed, you can change logging-related
box.cfg
options in the YAML configuration file: see log and other related options.
Almost all errors in Cartridge follow the return nil, err
style, where
err
is an error object produced by Tarantool’s
errors module. Cartridge
doesn’t raise errors except for bugs and functions contracts mismatch.
Developing new roles should follow these guidelines as well.
Error classes help to locate the problem’s source. For this purpose, an error object contains its class, stack traceback, and a message.
local errors = require('errors')
local DangerousError = errors.new_class("DangerousError")
local function some_fancy_function()
local something_bad_happens = true
if something_bad_happens then
return nil, DangerousError:new("Oh boy")
end
return "success" -- not reachable due to the error
end
print(some_fancy_function())
nil DangerousError: Oh boy
stack traceback:
test.lua:9: in function 'some_fancy_function'
test.lua:15: in main chunk
For uniform error handling, errors
provides the :pcall
API:
local ret, err = DangerousError:pcall(some_fancy_function)
print(ret, err)
nil DangerousError: Oh boy
stack traceback:
test.lua:9: in function <test.lua:4>
[C]: in function 'xpcall'
.rocks/share/tarantool/errors.lua:139: in function 'pcall'
test.lua:15: in main chunk
`lua
print(DangerousError:pcall(error, 'what could possibly go wrong?'))
`
nil DangerousError: what could possibly go wrong?
stack traceback:
[C]: in function 'xpcall'
.rocks/share/tarantool/errors.lua:139: in function 'pcall'
test.lua:15: in main chunk
For errors.pcall
there is no difference between the return nil, err
and
error()
approaches.
Note that errors.pcall
API differs from the vanilla Lua
pcall. Instead of true
the former
returns values returned from the call. If there is an error, it returns
nil
instead of false
, plus an error message.
Remote net.box
calls keep no stack trace from the remote. In that
case, errors.netbox_eval
comes to the rescue. It will find a stack trace
from local and remote hosts and restore metatables.
> conn = require('net.box').connect('localhost:3301')
> print( errors.netbox_eval(conn, 'return nil, DoSomethingError:new("oops")') )
nil DoSomethingError: oops
stack traceback:
eval:1: in main chunk
during net.box eval on localhost:3301
stack traceback:
[string "return print( errors.netbox_eval("]:1: in main chunk
[C]: in function 'pcall'
However, vshard
implemented in Tarantool doesn’t utilize the errors
module. Instead it uses
its own errors.
Keep this in mind when working with vshard
functions.
Data included in an error object (class name, message, traceback) may be
easily converted to string using the tostring()
function.
GraphQL implementation in Cartridge wraps the errors
module, so a typical
error response looks as follows:
{
"errors":[{
"message":"what could possibly go wrong?",
"extensions":{
"io.tarantool.errors.stack":"stack traceback: ...",
"io.tarantool.errors.class_name":"DangerousError"
}
}]
}
Read more about errors in the GraphQL specification.
If you’re going to implement a GraphQL handler, you can add your own extension like this:
local err = DangerousError:new('I have extension')
err.graphql_extensions = {code = 403}
It will lead to the following response:
{
"errors":[{
"message":"I have extension",
"extensions":{
"io.tarantool.errors.stack":"stack traceback: ...",
"io.tarantool.errors.class_name":"DangerousError",
"code":403
}
}]
}
In a nutshell, an errors
object is a table. This means that it can be
swiftly represented in JSON. This approach is used by Cartridge to
handle errors via http:
local err = DangerousError:new('Who would have thought?')
local resp = req:render({
status = 500,
headers = {
['content-type'] = "application/json; charset=utf-8"
},
json = json.encode(err),
})
{
"line":27,
"class_name":"DangerousError",
"err":"Who would have thought?",
"file":".../app/roles/api.lua",
"stack":"stack traceback:..."
}