Приложение для Tarantool — это, по сути, набор хранимых процедур, используемых как API. Данные обрабатываются на стороне хранилища, что позволяет значительно повысить производительность. Однако поддержка хранимых процедур может превратиться в кошмар.
Может. Но не сегодня.
Сегодня мы рассмотрим вопросы обеспечения качества приложения. В частности, поговорим о тестировании, разберемся, как запуститься в production, как использовать коннекторы, а также поговорим о тонкостях миграции схемы данных.
Содержание цикла «Приложения для Tarantool 1.7»
— Часть 1. Хранимые процедуры
— Часть 2. OAuth2-авторизация
— Часть 3. Тестирование и запуск
Напомню, что все примеры в этом данном цикле статей основаны на приложении для авторизации пользователей tarantool-authman.
Тестирование
Тестирование — только один из способов повысить качество.
В случае с хранимыми процедурами, функциональные тесты — это хороший вариант поддержки актуального API. Для тестирования используется встроенный в Tarantool модуль tap (Test Anything Protocol). Он включает в себя несколько методов для базовых проверок и сравнения Lua-объектов. Давайте протестируем два кейса регистрации пользователя auth.registration(email)
.
local case = {}
local tap = require('tap')
local response = require('authman.response')
local error = require('authman.error')
local db = require('authman.db')
local config = require('test.config')
-- Инициализируем приложение для тестирования
local auth = require('authman').api(config)
local test = tap.test('registration_test')
-- Очищаем space перед каждым запуском теста
function case.before() db.truncate_spaces() end
function case.after() end
function test_registration_succes()
local ok, code
ok, code = auth.registration('test@test.ru')
-- Проверим, что регистрация прошла успешно, а тип возвращаемого параметра — строка
test:is(ok, true, 'test_registration_succes user created')
test:isstring(code, 'test_registration_succes code returned')
end
function test_registration_user_already_exists()
local ok, code, got, expected, user
ok, code = auth.registration('test@test.ru')
-- Один из методов API, который активирует пользователя с паролем
ok, user = auth.complete_registration('test@test.ru', code, 'password')
-- Проверим, что значение соответствует ожидаемой ошибке
-- Метод возвращает два параметра, поэтому для упрощения проверки вызов обернут в инициализацию Lua-таблицы
got = {auth.registration('test@test.ru'), }
expected = {response.error(error.USER_ALREADY_EXISTS), }
-- is_deeply проверяет совпадение ключей и данных в Lua-таблицах
test:is_deeply(got, expected, 'test_registration_user_already_active')
end
case.tests = {
test_registration_succes,
test_registration_user_already_exists
}
return case
Теперь напишем небольшой скрипт для запуска тестов. Не забудем также учесть методы before
и after
, которые упростят тестирование.
-- Прежде всего, необходимо запустить Tarantool
box.cfg {
listen = 3331,
}
local TEST_CASES = {
'test.case.registration',
}
function run()
for case_index = 1, #TEST_CASES do
local case = require(TEST_CASES[case_index])
for test_index = 1, #case.tests do
-- Вызов конкретного теста
case.before()
case.tests[test_index]()
case.after()
end
end
end
run()
Результат запуска тестов:
$ tarantool test/authman.test.lua
...
TAP version 13
ok — test_registration_succes user created
ok — test_registration_succes code returned
ok — test_registration_user_already_active
Запуск инстанса
Приложение написано и протестировано, а значит, пришло время разворачивать его в production-среде.
Для управления инстансами Tarantool используется встроенная утилита tarantoolctl. Сначала создадим инстанс с приложением. Не забудем добавить пользователя и выдать ему права.
box.cfg {
listen = 3331;
}
local function bootstrap()
box.schema.user.create('my_user', {password = '123'})
box.schema.user.grant('my_user', 'read,write,execute', 'universe')
end
box.once('init_user', bootstrap)
config = {
-- Конфигурации приложения
}
-- Объявляем переменную auth глобально
auth = require('auth').api(config)
Теперь создадим символьную ссылку на этот файл из директории instances.enabled:
sudo ln -s /etc/tarantool/instances.available/auth.lua /etc/tarantool/instances.enabled/auth.lua
Запустим tarantoolctl и проверим, что можем подключиться к Tarantool и использовать методы приложения:
$ tarantoolctl start auth
$ tarantoolctl enter auth
connected to unix/:/var/run/tarantool/auth.control
unix/:/var/run/tarantool/auth.control> ok, user = auth.registration('ivanov@mail.ru')
unix/:/var/run/tarantool/auth.control> ok, user = auth.complete_registration('ivanov@mail.ru', user.code, '123')
unix/:/var/run/tarantool/auth.control> user
---
- is_active: true
email: ivanov@mail.ru
id: 8cd27d26-3974-43d6-a2b2-87202664753d
...
Если что-то пошло не так, следует посмотреть логи /var/log/tarantool/auth.lua
. Подробнее об администрировании сервера Tarantool можно почитать в документации.
Коннектор
Одно из преимуществ хранимых процедур — предоставление общего интерфейса для сервисов, написанных на разных языках программирования.
Рассмотрим пример асинхронного использования Tarantool с помощью Python-коннектора.
import asyncio
import asynctnt
async def create_user():
tnt_connection = asynctnt.Connection(
host='127.0.0.1', port='3367', username='my_user', password=’123’
)
# Асинхронная регистрация пользователя
user_data = await tnt_connection.call(
'auth.registrtion', ['exaple@mail.ru', ]
)
await tnt_connection.disconnect()
# Вызовем создание пользователя
loop = asyncio.get_event_loop()
loop.run_until_complete(create_user())
Стоит отметить, что взаимодействовать с Tarantool можно также из другого инстанса Tarantool с помощью встроенного модуля net.box
. Подробнее о нём читайте в документации.
Миграции данных
Распространенная задача, когда приложение уже запущено в бою — изменение схемы данных. Например, пользователю требуется добавить поле gender
.
Для этого нужно создать скрипт, изменяющий все данные в space auth_user
. Сам скрипт можно добавить в вызов инициализации приложения, а функция box.once
ограничит повторный вызов.
local fiber = require('fiber')
local function migrations()
-- Выполним миграцию только один раз
box.once('20171101_add_gender', function ()
local counter = 0
-- Обход space с помощью итератора сократит использование оперативной памяти
-- В памяти окажется только текущий объект
for _, tuple in box.space.auth_user:pairs(
nil, {iterator=box.index.ALL}
) do
local user_tuple = tuple:totable()
-- Запишем в таблицу новые данные
user_tuple[4] = get_user_gender()
box.space.auth_user:replace(user_tuple)
counter = counter + 1
end
end)
end
migrations()
Несмотря на то, что миграция может выполняться долго, главный поток исполнения не блокируется. Это связано с тем, что метод replace
неявно вызывает yield
. Для того чтобы явно освободить поток исполнения, нужно вызвать метод fiber.sleep(0)
. Подробнее об этом можно прочитать в документации.
Что дальше?
Tarantool не стоит на месте и активно развивается.
За рамками этого обучающего цикла статей осталось несколько важных вопросов, на которые стоит обратить внимание при написании собственных сервисов:
- Шардирование и репликация. Использование модуля vshard.
- Поддержка SQL-синтаксиса в Tarantool. Оптимизация хранимых процедур с помощью SQL.
- Обзор дискового движка Vinyl. Сравнение с Memtx, или «Что делать, если закончилась оперативка?».
- Оптимизация и профилирование хранимых процедур, написанных на языке C.
Напоминаю, что все примеры кода взяты из приложения tarantool-authman. Оно тоже развивается, и теперь может быть использовано как Oauth2-server.
Если у вас остались вопросы, выходящие за рамки цикла статей, то задать их можно в телеграм-чате. Отзывчивое сообщество Tarantool, скорее всего, сможет вам помочь.
Спасибо всем, кто дождался третьей части.
До новых встреч!
Автор: Denis Linnik