---
# System prepended metadata

title: OTUS — Практика «Сокращатель ссылок»
tags: [Tarantool, ' OTUS', ' practice', ' trainings']

---

---
tags: Tarantool, OTUS, trainings, practice
title: OTUS — Практика «Сокращатель ссылок»
---

План https://hackmd.io/@Mons/OTUS-Practice-Plan

# OTUS — Сервис сокращения ссылок

[TOC]

## ТЗ 

- Пользователь хотел бы сократить ссылку:
    - Чтобы, например, вместить её в смс или в твиттер
    - Или даже расположить в печати, из которой переходы могут происходить только «вручную»

## Общий план

- Настроить хранилище `Tarantool`
- Смасштабировать его
- Запустить простейший `http` сервер
- *Соединить их вместе и все готово:)*

## Инструмент

- Установка в Centos 8
```bash=
sudo dnf install tarantool tarantool-devel
```
- Установка в других ОС
  https://www.tarantool.io/en/download/os-installation/
  
## База данных и сервер приложений `Tarantool`

- Создание приложений на языке `Lua` с компиляцией «на лету» (`LuaJIT`)
- `ACID` движок базы данных c возможностью репликации между двумя и более инстансами
- Обе части в одном процессе
 
# День 1 — Архитектура

- Сегодня вы скачаете и запустите `Tarantool`
- Создадите спейс
- Создадите тапл
- Создадите индексы
- Вставите и достанете таплы из спейса

## API 

- Настройка базы данных происходит на языке `Lua`
- `box.cfg` — первоначальная настройка базы данных
    - Перенастройка базы тоже происходит в этой функции, но не все параметры при этом можно менять на лету
    - `listen` — параметр, на каком порту слушать подключения
    - Например
    ```lua
    box.cfg{}
    ```
- `box.schema.space.create(<name>)` — создание спейсов, так в `Tarantool` называются таблицы
    - Например, создать спейс `experience`, если она ещё не была создана 
    ```lua
    box.schema.space.create('experience', 
        {if_not_exists=true})
    ```
    - Таблицы в `Tarantool` называются спейсами
- После создания доступ к таблице происходит через функции `box.space.<table_name>` или `box.space['<table_name>']`
- `box.space.<table_name>:format({fields, ....})` — функция для указания какие колонки будут в таблице
    - Например
    ```lua
    box.space.experience:format({
        {name='date', type='unsigned'}, 
        {name='event', type='string'},
        {name='effect', type='string'}})
    ```
- `box.space.redirector:create_index` — создание индексов функцией самой таблицы
    - Первый созданный индекс является первичным
        - И он должен быть уникальным
    - Все остальные являются вторичными
    - Любой индекс может состоять из нескольких колонок
    - Например, первичный
    ```lua
    box.space.experience:create_index('primary',
        {parts={
            {field='date', type='unsigned'}
        }})
    ```
    - Например, вторичный неуникальный
    ```lua
    ....
    box.space.experience:create_index('samsara',
        {parts={
            {field='event', type='string'},
            {field='effect', type='string'}
        }})
    ```

### Схема данных для сокращателя ссылок

- Таблица `redirector`
    | source | short  |
    |:------:|:------:|
    | string | string |
- Для перенаправления пользователя нам понадобятся два поля:
    - «Короткая ссылка» (`short`)
    - «И исходная (длинная)» (`source`)

#### Индексация 

- В `Tarantool` таблица не может существовать без первичного индекса — одного или нескольких полей однозначно идентифицирующих запись в таблице
    - Самый первый созданный индекс на таблице и будет первичным
- В нашем случае исходная ссылка уникальна и однозначно идентифицирует строку
    - Первичный индекс `source`
- Кроме этого мы будем искать исходную ссылку для короткой для перенаправления браузера пользователя
    - Вторичный индекс `short` 

### Создание таблицы для сокращателя ссылок

- Создадим директорию проекта
    ```bash=
    mkdir makeshort && cd makeshort
    ```
- :::spoiler `storage.lua`

    ```lua=
    local fio = require('fio')

    local wrkdir = './db1'

    fio.mktree(wrkdir)

    box.cfg{
      memtx_dir=wrkdir,
      wal_dir=wrkdir,}

    local function init()
        --[[
            Создание схемы данных, индексов
        ]]

        box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true})

        --[[
            Таблица `redirector` — хранилище коротких ссылок
            if_not_exists — флаг, о том чтобы не генерировать исключение,
            если таблица уже существует 
        ]]
        box.schema.space.create('redirector', {if_not_exists=true})
        box.space.redirector:format({
                {name='source', type='string'},
                {name='short', type='string'},
        })

        --[[
            Первичный ключ, первый создаваемый индекс таблицы
        ]]
        box.space.redirector:create_index(
            'primary',
            {
                parts={
                    {field='source', type='string'}},
                if_not_exists=true}
        )

        --[[
            Вторичный индекс, для быстрого поиска строки по короткой ссылке 
        ]]
        box.space.redirector:create_index(
            'short',
            {
                parts={
                    {field='short', type='string'}},
                unique=true,
                if_not_exists=true,}
        )
    end

    return {
        init=init,
    }
    ```
    :::
- Запуск
    ```bash
    tarantool
    ```
  - `tarantool>`
    ```lua
    storage = require('storage')
    storage.init()
    ```
- Добавить данные
  - `tarantool>`
    ```lua
    box.space.redirector:insert(
        {'https://google.com', 'abcdef'})
    ```
- Запросить данные
    - По первичному ключу (уникальному)
    - `tarantool>`
        ```lua
        box.space.redirector:get('https://google.com')
        ```
    - По вторичному
    - `tarantool>`
        ```lua
        box.space.redirector.index.short:get('abcdef') 
        ```

# День 2 — Репликация


- Сегодня вы запустите два узла 
- Настроите репликацию между ними
- Научитесь анализировать состояние репликации

## Intro

- Tarantool содержит механизм копирования (репликации) данных между узлами
- Множество узлов объединённых репликацией называется репликасет (`replicaset`)
- Например, простейшая схема
    - На узел-лидер записываются данные
    - Узел-реплика эти данные с лидера забирает и применяет у себя
    ```graphviz
    digraph {
        node [shape = circle style = filled];
        node [fillcolor=white] leader;
        node [fillcolor=grey] replica;
        leader -> replica
    }
    ```
- Схема посложнее
    - Оба узла могут принимать операции записи
    - Оба узла применяют изменения соседа у себя
        - Пользователь может разрешать конфликты в конфликт-триггере
    ```graphviz
    digraph {
        node [shape = circle style = filled];
        node [fillcolor=white] leader1 leader2;
        leader1 -> leader2
        leader1 -> leader2[dir=back]
    }
    ```
- И сложные схемы
  - Мультимастер звезда
    ```graphviz
    digraph cluster_star {
        layout=circo;

        node [shape = circle style = filled];

        node [label=leader fillcolor=white] leader1;
        node [label=leader fillcolor=white] leader2 leader3;
        node [label=replica fillcolor=grey] replica1 replica2 replica3 replica12 replica22 replica32;

        leader1 -> {leader2, leader3}[style=bold]
        leader2 -> {leader1, leader3}[style=bold]
        leader3 -> {leader1, leader2}[style=bold]
        leader1 -> {replica1, replica12}[color=grey]
        leader2 -> {replica2, replica22}[color=grey]
        leader3 -> {replica3, replica32}[color=grey]
    }
    ```
  - Или даже `fullmesh`
    ```graphviz
    digraph cluster_star {
        layout=circo;
        overlap = false;
        ranksep=2.5;

        node [shape = circle, style = filled]

        node [label=leader fillcolor=white] leader1
        node [label=leader fillcolor=white] leader2 leader3
        leader1 -> {leader2, leader3}[style=bold]
        leader2 -> {leader1, leader3}[style=bold]
        leader3 -> {leader1, leader2}[style=bold]

        node [label=replica fillcolor=grey] replica1 replica2 replica3 replica12 replica22 replica32

        leader1 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey]

        leader2 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey]
        leader3 -> {replica1, replica12, replica2, replica22, replica3, replica32}[color=grey]

    }
    ```

### Настройка репликации

- `box.cfg` содержит параметры для настройки репликации
    - `replication` — адреса узлов, от которых копировать данные 
    - `instance_uuid` — идентификатор текущего узла
    - `replicaset_uuid` — идентификатор репликасета, у всех принадлежащих узлов он одинаковый
    - `read_only` — режим «только чтение» для узла, в целом может использоваться и без репликации
- Репликация работает на том порту, что был указан в `box.cfg{listen}`, таким образом `replication` это список `listen` других узлов
- В случае репликации, настройка схемы происходит **только на одном** на лидере
    - `box.info.ro` позволяет понять, текущий узел лидер или реплика

### Репликация для сокращателя ссылок

```graphviz
digraph {
    node [shape = circle style = filled];
    node [fillcolor=white label="moscow"] leader;
    node [fillcolor=grey label="moscow2"] replica;
    leader -> replica
}
```

## Конфигурация узлов

- Оба узла мы будем запускать на одной машине
- Исходный код будет использоваться один и тот же, но с разной конфигурацией
- Вынесем конфигурацию узла в переменные среды (`environment variable`)
- Конфигурация узла будет содержать:
    - Бинарный (`iproto`) адрес и порт для репликации
    - Источник репликации и режим реплики «только для чтения»
- Переменные среды
    - `tarantool_instance_uuid`
    - `tarantool_replicaset_uuid`
    - `tarantool_listen`
    - `tarantool_replication`
    - `tarantool_workdir`
- Добавим конфигурацию в исходный код
- Отредактируем модуль `storage.lua`, чтобы указать параметры репликации
- :::spoiler `storage.lua`
    ```lua=
    local fio = require('fio')

    local instance_uuid = os.getenv('tarantool_instance_uuid')
    local replicaset_uuid = os.getenv('tarantool_replicaset_uuid')
    local listen = os.getenv('tarantool_listen') or '127.0.0.1:3301'
    local replication = os.getenv('tarantool_replication')
    local wrkdir = os.getenv('tarantool_workdir') or './db1'
    fio.mktree(wrkdir)

    box.cfg{
        instance_uuid = instance_uuid,
        replicaset_uuid = replicaset_uuid,
        listen = listen,
        replication = {replication},
        memtx_dir=wrkdir,
        wal_dir=wrkdir,}

    local function init()
        if box.info.ro then
            return
        end
        --[[
            Создание схемы данных, индексов, и пользователя репликации
        ]]

        box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true})

        --[[
            Таблица `redirector` — хранилище коротких ссылок
            if_not_exists — флаг, о том чтобы не генерировать исключение,
            если таблица уже существует
        ]]
        box.schema.space.create('redirector', {if_not_exists=true})
        box.space.redirector:format({
                {name='source', type='string'},
                {name='short', type='string'},
        })

        --[[
            Первичный ключ, первый создаваемый индекс таблицы
        ]]
        box.space.redirector:create_index(
            'primary',
            {
                parts={
                    {field='source', type='string'}},
                if_not_exists=true}
        )

        --[[
            Вторичный индекс, для быстрого поиска строки по короткой ссылке
        ]]
        box.space.redirector:create_index(
            'short',
            {
                parts={
                    {field='short', type='string'}},
                unique=true,
                if_not_exists=true,}
        )
    end

    return {
        init=init,
    }
    ```
- Создадим входные точки
- :::spoiler `start_moscow.lua`
    ```lua=
    os.setenv('tarantool_instance_uuid', '00000000-0000-4000-a000-0000000000a1')
    os.setenv('tarantool_replicaset_uuid', '00000000-0000-4000-a000-000000000001') --<< одинаковые для всех узлов репликасета
    os.setenv('tarantool_workdir', 'moscow')
    os.setenv('tarantool_listen', '127.0.0.1:3301')

    local storage = require('storage')

    storage.init()
    ```
- :::spoiler `start_moscow2.lua`
    ```lua=
    os.setenv('tarantool_instance_uuid', '00000000-0000-4000-a000-0000000000a2')
    os.setenv('tarantool_replicaset_uuid', '00000000-0000-4000-a000-000000000001') --<< одинаковые для всех узлов репликасета
    os.setenv('tarantool_workdir', 'moscow2')
    os.setenv('tarantool_listen', '127.0.0.1:3302')
    os.setenv('tarantool_replication', '127.0.0.1:3301')

    local storage = require('storage')

    storage.init()
    ```

#### Запуск

```bash
tarantool start_moscow.lua
```
```bash
tarantool start_moscow2.lua
```

#### Проверка

- Добавить данные на `moscow`
    ```bash 
    tarantoolctl connect 127.0.0.1:3301
    ```
  - `tarantool>`
    ```lua
    box.space.redirector:insert(
        {'https://google.com', 'abcdef'})
    ```
- Запросить данные с реплики `moscow2`
    ```bash
    tarantoolctl connect 127.0.0.1:3302
    ```
  - `tarantool>`
    ```lua
    box.space.redirector:get('https://google.com')
    ```
    ```lua
    box.space.redirector.index.short:get('abcdef')
    ```

### Мониторинг репликации

- Проверим репликацию на реплике `moscow2`
    ```bash
    tarantoolctl connect 127.0.0.1:3302
    ```
- `tarantool>`
  ```lua
  box.info().replication
  ```
- Примерный вывод
    ```yaml
    ---
    - 1:
        id: 1
        uuid: 00000000-0000-4000-a000-0000000000a1
        lsn: 8
        upstream:
          status: follow
          idle: 0.73673253925517
          peer: 127.0.0.1:3301
          lag: 0.00011086463928223
      2:
        id: 2
        uuid: 00000000-0000-4000-a000-0000000000a2
        lsn: 1
    ...
    ```
- `upstream.status`
    - `follow` — репликация работает
- `upstream.lag`
    - репликация отстаёт на `lag` секунд
- Если `status` не `follow` — репликация не идёт, необходим анализ и вмешательство
    - `upstream.idle` — укажет как давно сломалась репликация

# День 3 — Шардирование

- Сегодня вы запустите шардированный кластер из четырех узлов
- Адаптируете хранилище под шардинг 

## Intro
 
- Шардирование позволяет распределить данные по нескольким репликасетам `Tarantool`
- Данные распределяются по корзинам
- Корзины распределяются по репликасетам
    - Внутри репликасетов данные реплицируются между узлами
- Обработка данных происходит на узлах-роутерах
- Хранение данных происходит на узлах-стораджах
- В случае добавления новых репликасетов с узлами-стораджами, происходит автоматическая перебалансировка корзин с данными на новый репликасет
- Общее количество корзин `bucket_count` задаётся только один раз при первом старте приложения
- Распределение данных по стораджам происходит на узлах-роутерах
    - Но в целом на одном узле может быть запущено обе роли: роутер и сторадж
- Каждая таблица, что хочет шардирования, должна содержать неуникальный индекс `bucket_id`

### Топология, для сокращателя ссылок

- Для простоты расположим роутеры на каждом из шардов
```graphviz
digraph onnode {
    subgraph cluster_1 {
        label="Московский шард"
        
        subgraph cluster_2 {
            label="Москва";
            moscow_router[label="Роутер"]
            moscow_storage[label="Хранилище" shape=cylinder]
        }
        style=dashed
    }
    
    subgraph cluster_3 {
        label="Питерский шард"
        
        subgraph cluster_4 {
            label="Питер"
            spb_router[label="Роутер"]
            spb_storage[label="Хранилище" shape=cylinder]
        }
        
        style=dashed
    }
    moscow_router -> moscow_storage
    moscow_router -> spb_storage
    spb_router -> spb_storage
    spb_router -> moscow_storage
}
```

### Шардирование сокращателя ссылок

- Удалим устаревшие рабочие директории
    ```bash
    rm -rf moscow*
    ```
- Установим фреймворк шардирования `vshard`
    ```bash
    tarantoolctl rocks install vshard
    ```
- `topology.lua`
    ```lua=
    local cfg = {
        BUCKET_COUNT = 16,
        sharding = {
            ['00000000-0000-4000-a000-000000000001'] = { -- replicaset #1
                replicas = {
                    ['00000000-0000-4000-a000-0000000000a1'] = {
                        uri = 'rep:rep@127.0.0.1:3301',
                        name = 'storage_moscow',
                        master = true
                    },
                },
            }, -- replicaset #1
            ['00000000-0000-4000-a000-000000000002'] = { -- replicaset #2
                replicas = {
                    ['00000000-0000-4000-a000-0000000000b1'] = {
                        uri = 'rep:rep@127.0.0.1:3303',
                        name = 'storage_spb',
                        master = true

                    },
                },
            }, -- replicaset #2
        }, -- sharding
    }

    return cfg
    ```
- `storage.lua`
    ```lua=
    local function init()
        if box.info.ro then
            return
        end
        --[[
            Создание схемы данных, индексов, и пользователя репликации
        ]]

        box.schema.user.grant('guest', 'super', nil, nil, {if_not_exists=true})

        --[[
            Таблица `redirector` — хранилище коротких ссылок
            if_not_exists — флаг, о том чтобы не генерировать исключение,
            если таблица уже существует
        ]]
        box.schema.space.create('redirector', {if_not_exists=true})
        box.space.redirector:format({
                {name='source', type='string'},
                {name='short', type='string'},
                {name='bucket_id', type='unsigned'},})

        --[[
            Первичный ключ, первый создаваемый индекс таблицы
        ]]
        box.space.redirector:create_index(
            'primary',
            {
                parts={
                    {field='source', type='string'}},
                if_not_exists=true})

        --[[
            Вторичный индекс, для быстрого поиска строки по короткой ссылке
        ]]
        box.space.redirector:create_index(
            'short',
            {
                parts={
                    {field='short', type='string'}},
                unique=true,
                if_not_exists=true,})

        --[[
            Индекс для шардирования данных
        ]]
        box.space.redirector:create_index(
            'bucket_id',
            {
                parts={
                    {field='bucket_id', type='unsigned'}},
                unique=false,
                if_not_exists=true,})
        
        box.schema.user.grant(
            'rep', 'read,write,execute', 'universe',
            nil, {if_not_exists=true}
        )

    end

    return {
        init=init,
    }
    ```
- `start_moscow.lua`
    ```lua=
    vshard = require('vshard')
    local topology = require('topology')

    local storage = require('storage')

    local fio = require('fio')
    fio.mktree('moscow')

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = 'moscow',
            wal_dir = 'moscow',
            replication_connect_quorum = 1,
        },
        '00000000-0000-4000-a000-0000000000a1')

    storage.init()

    vshard.router.cfg({
        bucket_count = topology.BUCKET_COUNT,
        sharding = topology.sharding,})
    ```
- `start_spb.lua`
    ```lua=
    vshard = require('vshard')
    local topology = require('topology')

    local storage = require('storage')

    local fio = require('fio')
    fio.mktree('spb')

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = 'spb',
            wal_dir = 'spb',
            replication_connect_quorum = 1,
        },
        '00000000-0000-4000-a000-0000000000b1')

    storage.init()
    
    vshard.router.cfg({
        bucket_count = topology.BUCKET_COUNT,
        sharding = topology.sharding,})
    ```

### Запуск


```bash
tarantool start_moscow.lua
```

```bash
tarantool start_spb.lua
```


### Первоначальная настройка

- На данный момент в логах
    ```
    Some buckets are not active, retry rebalancing later
    ```
- Требуется Бутстрап Хранилища
    ```
    tarantoolctl connect 127.0.0.1:3301
    ```
- И в запущенном подключении выполнить бутстрап
- `tarantool>`
  ```lua
  vshard.router.bootstrap()
  ```

### Вставка данных

- Доступ к данным теперь происходит через роутер
- Подключимся к роутеру
    ```
    tarantoolctl connect 127.0.0.1:3301
    ```
- Подготовим данные
- Для каждого объекты мы теперь должны вычислять `bucket_id` — прямо сейчас захардкодим эти значения
    - В будущем мы выберем по какому полю шардировать данные, что сценарий работы с сокращателем ссылок был удобен для пользователя
- Для отправки данных на шард используем функцию `vshard.router.callrw`
- `tarantool>`
    ```lua=
    bucket_id=1
    google={'https://google.com', 'abcdef', bucket_id}
    vshard.router.callrw(bucket_id, 
        'box.space.redirector:put', {google})

    bucket_id=2
    ya={'https://ya.ru', 'qwerty', bucket_id}
    vshard.router.callrw(bucket_id, 
        'box.space.redirector:put', {ya})

    bucket_id=3
    apple={'https://apple.com', 'aaa', bucket_id}
    vshard.router.callrw(bucket_id, 
        'box.space.redirector:put', {apple})
    ```
- Для запроса данных по ключу шардирования `vshard.router.callro`
    ```lua=
    vshard.router.callro(3, 
        'box.space.redirector:get', 
        {'https://apple.com'})
    ```

## Запрос данных `map/reduce`

- `vshard.router.routeall` возвращает все репликасеты
    - Функции роутера работают только на роутере
- `replica:call*` вызывает функцию на подходящей реплике
    ```lua
    do
        local resultset = {}
        shards, err = vshard.router.routeall()
        if err ~= nil then
            print(err)
            return
        end
        for uid, replica in pairs(shards) do
            local set, err = replica:callro('box.space.redirector:select', {})
            if err ~= nil then
                print(err)
                return
            end

            for _, link in ipairs(set) do
                table.insert(resultset, link)
            end
        end
        table.sort(resultset, function(a, b) return a[1] < b[1] end)
        return resultset
    end
    ```


### Отказоустойчивый шардинг

- Доработаем исходники
- `topology.lua`
    ```lua=
    local cfg = {
        BUCKET_COUNT = 16,
        sharding = {
            ['00000000-0000-4000-a000-000000000001'] = { -- replicaset #1
                replicas = {
                    ['00000000-0000-4000-a000-0000000000a1'] = {
                        uri = 'rep:rep@127.0.0.1:3301',
                        name = 'storage_moscow',
                        master = true
                    },
                    ['00000000-0000-4000-a000-0000000000a2'] = {
                        uri = 'rep:rep@127.0.0.1:3302',
                        name = 'storage_moscow2',
                    },
                },
            }, -- replicaset #1
            ['00000000-0000-4000-a000-000000000002'] = { -- replicaset #2
                replicas = {
                    ['00000000-0000-4000-a000-0000000000b1'] = {
                        uri = 'rep:rep@127.0.0.1:3303',
                        name = 'storage_spb',
                        master = true

                    },
                    ['00000000-0000-4000-a000-0000000000b2'] = {
                        uri = 'rep:rep@127.0.0.1:3304',
                        name = 'storage_spb2',
                    },
                },
            }, -- replicaset #2
        }, -- sharding
    }

    return cfg
    ```
- `start_moscow2.lua`
    ```lua=
    vshard = require('vshard')
    local topology = require('topology')

    local storage = require('storage')

    local fio = require('fio')
    fio.mktree('moscow2')

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = 'moscow2',
            wal_dir = 'moscow2',
            replication_connect_quorum = 1,
        },
        '00000000-0000-4000-a000-0000000000a2')

    storage.init()

    vshard.router.cfg({
        bucket_count = topology.BUCKET_COUNT,
        sharding = topology.sharding,})
    ```
- `start_spb2.lua`
    ```lua=
    vshard = require('vshard')
    local topology = require('topology')

    local storage = require('storage')

    local fio = require('fio')
    fio.mktree('spb2s')

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = 'spb2',
            wal_dir = 'spb2',
            replication_connect_quorum = 1,
        },
        '00000000-0000-4000-a000-0000000000b2')

    storage.init()
    
    vshard.router.cfg({
        bucket_count = topology.BUCKET_COUNT,
        sharding = topology.sharding,})
    ```
- Запуск
```bash
tarantool start_moscow.lua
```

```bash
tarantool start_spb.lua
```
```bash
tarantool start_moscow2.lua
```

```bash
tarantool start_spb2.lua
```

### Запросы

```
tarantoolctl connect 127.0.0.1
```
- `tarantool>`
  - Запись
    ```lua
    vshard.router.callrw(1, 'print', {'<<CALL>>'})
    ```
  - Чтение предпочитая лидера
    ```lua
    vshard.router.callro(1, 'print', {'<<CALL>>'})
    ```
  - Чтение с балансировкой
    ```lua
    vshard.router.callbro(1, 'print', {'<<CALL>>'})
    ```
  - Чтение с балансировкой предпочитая реплику
    ```lua
    vshard.router.callbro(1, 'print', {'<<CALL>>'})
    ```

# День 4 — Апп сервер

- Сегодня вы добавите логику приложения к своему хранилищу
- Напишете легковесный `http` сервер
- Реализуете `http` `api` в своем приложении

## Intro

### Lua за 15 минут

- https://learnxinyminutes.com/docs/ru-ru/lua-ru/

### Код

- Код на языке `Lua` выполняется в файберах (их также называют корутины, горутины, зеленые треды, легковесные сопрограммы)
- Файберы работают по принципу кооперативной многозадачности
    - В текущий момент времени выполняется только один файбер, и этот файбер, если операция ввода-вывода или явно, передаёт управление другому файберу
- `Tarantool` содержит встроенные модули, некоторыми из которых мы воспользуемся:
  - `log` — журналирование событий приложения и базы данных
  - `console` — предлагает возможность выполнения и отладки кода в уже запущенном инстансе тарантула
  - `clock` — функции для работы с системным временем
  - `digest` — вычисление хеша от строки
  - `fio` — работа с файлами
  - `socket` — работа с сетью
  - `box` — настройка и хранение данных
- Подробнее обо всех модулях: https://www.tarantool.io/en/doc/latest/reference/


## Сервер приложений

- Сделаем легковесный `http` сервер
- `socket.tcp_server(server, port, client_handler)` создаёт файбер, который
    - слушает tcp/ip `server:port`
    - для каждого нового клиента запускает `client_handler` в отдельном файбере
    - `client_handler` принимает первым аргументом соединение с клиентом
- Функция обработки `http` запросов:
    - для `GET /` вернет html форму для ввода ссылки
    - для `GET /?url=*` сгенерирует и вернет короткую ссылку
    - для `GET /*` перенаправит на исходную ссылку
    - Таким rest api я слегка хитрю, на этом этапе мы быстренько напишем `http` парсер воспользовавшись только первой строкой протокола
- Модуль `digest` помогает сгенерировать хеш от строки
    - Прежде чем вычислить хеш, к исходной строке добавляется рандомная «соль»
- Доступ к данным будет выполняться всё также через `vshard.router.callrw`(`-ro`)

### Топология

- Возьмём топологию попроще для удобства запуска и отладки
```graphviz
digraph onnode {
    subgraph cluster_shard1 {
        label="Московский шард"
        
        subgraph cluster_1 {
            label="Москва"
            appserver[label="Апп сервер" shape=box]
            moscow_router[label="Роутер"]
            moscow_storage[label="Хранилище" shape=cylinder]
            moscow_storage[label="Хранилище" shape=cylinder]
        }
        
        style=dashed
        
    }
    
    subgraph cluster_shard2 {
        label="Питерский шард"
        
        subgraph cluster_3 {
            label="Питер"
            spb_storage[label="Хранилище" shape=cylinder]
        }
        
        style=dashed
    }
    appserver -> moscow_router
    moscow_router -> moscow_storage
    moscow_router -> spb_storage
}
```


### Сервер обработки http запросов

- `appserver.lua`
    ```lua=
    local log = require('log')
    local socket = require('socket')
    local clock = require('clock')
    local digest = require('digest')

    local vshard = require('vshard')

    local serveraddr = '0.0.0.0'
    local port = 8080
    local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки

    --[[
        Сердце сервиса — функция генерации короткой ссылки
        Генерируем хеш с рандомной солью и вставляем в хранилище
        Шардируем по значению короткой ссылки
    ]]
    local function make_it_short(source)
        local short = nil

        short = digest.base64_encode(
            digest.urandom(10),
            {nopad=true, nowrap=true, urlsafe=true})

        local bucket_id = vshard.router.bucket_id_mpcrc32(short)
        local now = math.floor(clock.time())

        local rc, err = vshard.router.callrw(bucket_id, 'box.space.redirector:insert',
                                             {{source, short, now, bucket_id}})
        if err ~= nil then
            return nil, err
        end
        return short
    end

    --[[
        Тоже сердце — функция поиска исходной ссылки по короткой
    ]]
    local function take_me_source(short)
        local bucket_id = vshard.router.bucket_id_mpcrc32(short)
        local tuple, err = vshard.router.callro(bucket_id, 'box.space.redirector.index.short:get',
                                                {short})
        if err ~= nil then
            log.warn(err)
            return nil, err
        end
        if tuple == nil then
            return nil
        end
        return tuple[1]
    end

    --[[
        HTTP сервер обработки запросов
    ]]
    --[[
        Утилиты для декодирования значений GET параметров url строки
    ]]
    local hex_to_char = function(x)
        return string.char(tonumber(x, 16))
    end

    local unescape = function(url)
        return url:gsub("%%(%x%x)", hex_to_char)
    end

    --[[
        HTML форма для создания короткой ссылки
    ]]
    local form = [[
    <html>
    <head></head>
    <body>
      <form>
        <input name=url type=text />
        <input type=submit />
      </form>
    </body>
    </html>
    ]]

    --[[
        Функция возвращающая успешный (200) http пакет
        с данными `data`
    ]]
    local function response200(data)
        return ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: %u\r\nConnection: close\r\n\r\n%s")
            :format(#data, data)
    end

    --[[
        Возврат ошибки 404
    ]]
    local function response404()
        return "HTTP/1.1 404 Not Found\r\nContent-Type: text; charset=UTF-8\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found"
    end

    --[[
        Редирект на страницу target
    ]]
    local function redirect(target)
        return ("HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
            :format(target)
    end

    --[[
        TCP/IP сервер с оооочень простой (прямо таки простейшей)
        логикой http протокола
        Для каждого соединения в отдельном файбере запускается
        функция `function(client)`, которая обрабатывает http запросы
        и отвечает http ответы
    ]]
    --[[
        (ну почти REST) API
            GET /          — html форма для создания короткой cсылки
            GET /?url=*    — создание короткой ссылки по исходной в url параметре
            GET /*         - переход по короткой ссылке
    ]]
    local server

    local function init(opts)
        server = socket.tcp_server(
            serveraddr, port,
            function(client)
                local request_line = client:read('\r\n', 10)
                if request_line == nil then
                    log.info('Timeout')
                    return
                end
                --[[
                    Если запрос корня — предлагаем форму ввода
                ]]
                if request_line:startswith('GET / HTTP') then
                    client:write(response200(form))
                    --[[
                        Запрос на создание короткой ссылки
                    ]]
                elseif request_line:startswith('GET /?url=') then
                    local url = request_line:sub(#'GET /?url=')
                    url = url:sub(2, url:find(' ') - 1)
                    url = unescape(url)

                    local short = make_it_short(url)
                    if short == nil then
                        client:write(response404())
                        return
                    end

                    local pasteable_short = 'http://' .. hostname .. ':' .. tostring(port) .. '/' .. short
                    client:write(response200(pasteable_short))
                    --[[
                        Запрос на переход по короткой ссылке
                        Редиректим на исходную ссылку, если она была в базе
                    ]]
                elseif request_line:startswith('GET /') then
                    local short = request_line:sub(#'GET /')
                    short = short:sub(2, short:find(' ') - 1)
                    local source = take_me_source(short)
                    if source == nil then
                        client:write(response404())
                    end

                    client:write(redirect(source))
                end
        end)
        server:listen()
    end

    local function stop()
        server:close()
    end

    --[[
        Возвращаем
        - имя роли для использования в GUI и rpc API
        - колбеки жизненного цикла
        - зависимости от других ролей
        - vshard-router - роль, которая поможет запрашивать шардированные данные
    ]]
    return {
        init=init,
        stop=stop,
    }
    ```
- `start_moscow.lua`
    ```lua=
    vshard = require('vshard')
    local topology = require('topology')

    local storage = require('storage')

    local fio = require('fio')
    fio.mktree('moscow')

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = 'moscow',
            wal_dir = 'moscow',
            replication_connect_quorum = 1,
        },
        '00000000-0000-4000-a000-0000000000a1')

    storage.init()

    vshard.router.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
    })

    local appserver = require('appserver')

    appserver.init()
    ```

## Запуск


```bash
tarantool start_moscow.lua
```

```bash
tarantool start_spb.lua
```



## Оно заработало?

- Откроем браузер на `http://<ip_address>:8080/`
    - Должна появится форма для ввода `url`
- Введем любой `url`
- Получим короткую ссылку
- Перейдем по короткой ссылке
- Или можно воспользоваться готовым скриптом, который пытается укоротить `https://google.com/?q=<random-string>`
- `test.lua`
    ```lua=
    local log = require('log')
    local client = require('http.client')

    --[[
        Простой тест для проверки укорачивания ссылок
    ]]
    local host = '127.0.0.1'
    local port = 8080

    local source = 'https://google.com/?q=' .. tostring(os.time())
    local makeshort = 'http://' .. host .. ':' .. port .. '/?url='
    local response = client.get(makeshort .. source)
    local short = response.body
    print(short, '->', source)
    local redirect = client.get(short, {follow_location=true})
    assert(redirect.headers['location'] == source, 
        "Source url and redirect not equal")
    ```
- Запуск теста
    ```bash
    tarantool test.lua
    ```
- Примерный вывод
    ```bash
    http://localhost:8080/XniDtG2jrBa23Q	
      ->	https://google.com/?q=1607896232
    ```


# День 5 — Cartridge приложение

- Соберете приложение в пакет `Cartridge`
- Поуправляете кластером через веб интерфейс
- Создадите кластерный тест к своему приложению

## Intro

- `Cartidge` — фреймворк, который предлагает простой способ разработки и управления кластерным приложением
- Приложение разделяется на модули специальным образом — такие модули называются «роли»
- После чего вы запускаете одно или несколько экземпляров приложения и конфигурируете из них кластер с помощью `web ui`
- `Cartridge` содержит модуль для тестирования приложений в кластерном виде
- Для деплоя `Cartridge` предоставляет интерфейс для упаковки приложения в `rpm`/`deb`/`targz` пакет

### Роль

- Роль — модуль реализующий API
    - `role_name`
    - `init`
    - `validate_config`
    - `apply_config`
    - `stop`
    - `dependencies`
- Роли могут взаимодействовать друг с другом:
    - Локально, если одна роль зависит от другой
        - Тогда `Cartridge` их запускает на одном узле
    - Удаленно, тогда между ролями используется вызов `cartridge.rpc_call`
- `init`/`stop` вызываются при запуске и останове роли соответственно
- `validate_config`/`apply_config` вызываются при изменении конфигурации кластера:
    - Топологии
    - Переключении мастера
    - Настроек failover-а
    - Пользовательских настроек

## Подготовка

- Удаление устаревших рабочих директорий
    ```bash
    rm -rf moscow* spb*
    ```
- Удаление устаревших файлов
    ```bash
    rm -rf start_* topology.lua
    ```
- Установка фреймворка
    ```bash
    tarantoolctl rocks install cartridge 2.3.0
    ```
- У всех узлов теперь входная точка с инициализацией `cartridge`
- `init.lua`
    ```lua=
    #!/usr/bin/env tarantool

    --[[
        Устанавливаем текущую директорию,
        как начальный путь загрузки всех модулей
    ]]
    package.setsearchroot()

    local cartridge = require('cartridge')
    local log = require('log')

    --[[
        Конфигурируем и запускаем cartridge на узле
        Задаем небольшое количество корзин шардирования
        для удобства разработки и отладки
        Указываем какие роли мы будем использовать в кластере
          - vshard storage для хранения шардированных данных
          - vshard router для доступа к ним
        Указываем рабочую директорию для хранения снапов, икслогов
        и конфигурации приложения `one`
    ]]
    local _, err = cartridge.cfg({
        workdir = 'moscow',
        bucket_count = 32,
        roles = {
            'appserver',
            'storage',
            'cartridge.roles.vshard-storage',
            'cartridge.roles.vshard-router'
        },
    })
    if err ~= nil then
        log.info(err)
        os.exit(1)
    end
    ```
- Для модуля апсервера добавим API для кластеризации 
- :::spoiler `appserver.lua`
    ```lua=
    local log = require('log')
    local socket = require('socket')
    local clock = require('clock')
    local digest = require('digest')
    
    local vshard = require('vshard')

    local serveraddr = '0.0.0.0'
    local port = 8080
    local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки

    --[[
        Сердце сервиса — функция генерации короткой ссылки
        Генерируем хеш с рандомной солью и вставляем в хранилище
        Шардируем по значению короткой ссылки
    ]]
    local function make_it_short(source)
        local short = nil

        short = digest.base64_encode(
            digest.urandom(10),
            {nopad=true, nowrap=true, urlsafe=true})

        local bucket_id = vshard.router.bucket_id_mpcrc32(short)
        local now = math.floor(clock.time())

        local rc, err = vshard.router.callrw(bucket_id, 'box.space.redirector:insert',
                                             {{source, short, now, bucket_id}})
        if err ~= nil then
            return nil, err
        end
        return short
    end

    --[[
        Тоже сердце — функция поиска исходной ссылки по короткой
    ]]
    local function take_me_source(short)
        local bucket_id = vshard.router.bucket_id_mpcrc32(short)
        local tuple, err = vshard.router.callro(bucket_id, 'box.space.redirector.index.short:get',
                                                {short})
        if err ~= nil then
            log.warn(err)
            return nil, err
        end
        if tuple == nil then
            return nil
        end
        return tuple[1]
    end

    --[[
        HTTP сервер обработки запросов
    ]]
    --[[
        Утилиты для декодирования значений GET параметров url строки
    ]]
    local hex_to_char = function(x)
        return string.char(tonumber(x, 16))
    end

    local unescape = function(url)
        return url:gsub("%%(%x%x)", hex_to_char)
    end

    --[[
        HTML форма для создания короткой ссылки
    ]]
    local form = [[
    <html>
    <head></head>
    <body>
      <form>
        <input name=url type=text />
        <input type=submit />
      </form>
    </body>
    </html>
    ]]

    --[[
        Функция возвращающая успешный (200) http пакет
        с данными `data`
    ]]
    local function response200(data)
        return ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: %u\r\nConnection: close\r\n\r\n%s")
            :format(#data, data)
    end

    --[[
        Возврат ошибки 404
    ]]
    local function response404()
        return "HTTP/1.1 404 Not Found\r\nContent-Type: text; charset=UTF-8\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found"
    end

    --[[
        Редирект на страницу target
    ]]
    local function redirect(target)
        return ("HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
            :format(target)
    end

    --[[
        TCP/IP сервер с оооочень простой (прямо таки простейшей)
        логикой http протокола
        Для каждого соединения в отдельном файбере запускается
        функция `function(client)`, которая обрабатывает http запросы
        и отвечает http ответы
    ]]
    --[[
        (ну почти REST) API
            GET /          — html форма для создания короткой cсылки
            GET /?url=*    — создание короткой ссылки по исходной в url параметре
            GET /*         - переход по короткой ссылке
    ]]
    local server

    local function init(opts)
        server = socket.tcp_server(
            serveraddr, port,
            function(client)
                local request_line = client:read('\r\n', 10)
                if request_line == nil then
                    log.info('Timeout')
                    return
                end
                --[[
                    Если запрос корня — предлагаем форму ввода
                ]]
                if request_line:startswith('GET / HTTP') then
                    client:write(response200(form))
                    --[[
                        Запрос на создание короткой ссылки
                    ]]
                elseif request_line:startswith('GET /?url=') then
                    local url = request_line:sub(#'GET /?url=')
                    url = url:sub(2, url:find(' ') - 1)
                    url = unescape(url)

                    local short = make_it_short(url)
                    if short == nil then
                        client:write(response404())
                        return
                    end

                    local pasteable_short = 'http://' .. hostname .. ':' .. tostring(port) .. '/' .. short
                    client:write(response200(pasteable_short))
                    --[[
                        Запрос на переход по короткой ссылке
                        Редиректим на исходную ссылку, если она была в базе
                    ]]
                elseif request_line:startswith('GET /') then
                    local short = request_line:sub(#'GET /')
                    short = short:sub(2, short:find(' ') - 1)
                    local source = take_me_source(short)
                    if source == nil then
                        client:write(response404())
                    end

                    client:write(redirect(source))
                end
        end)
        server:listen()
    end

    local function stop()
        server:close()
    end

    --[[
        Возвращаем
        - имя роли для использования в GUI и rpc API
        - колбеки жизненного цикла
        - зависимости от других ролей
        - vshard-router - роль, которая поможет запрашивать шардированные данные
    ]]
    return {
        role_name="appserver",

        init=init,
        stop=stop,

        dependencies={'cartridge.roles.vshard-router'}
    }
    ```
    :::
- :::spoiler `storage.lua`
    ```lua=
    --[[
        Запуск роли
        В случае запуска на лидере репликасета создаём таблицы
        И для неё создаем первичный индекс и индекс для шардирования данных
        В случае, когда роль перезапускается, используем флаг `if_not_exists=true`
        для игнорирования в случае уже созданных объектов
    ]]
    local function init(opts)
        if not opts.is_master then
            return
        end

        --[[
            Таблица `redirector` — хранилище коротких ссылок
            if_not_exists — флаг, о том чтобы не генерировать исключение,
            если таблица уже существует
        ]]
        box.schema.space.create('redirector', {if_not_exists=true})
        box.space.redirector:format({
                {name='source', type='string'},
                {name='short', type='string'},
                {name='bucket_id', type='unsigned'},})

        --[[
            Первичный ключ, первый создаваемый индекс таблицы
        ]]
        box.space.redirector:create_index(
            'primary',
            {
                parts={
                    {field='source', type='string'}},
                if_not_exists=true})

        --[[
            Вторичный индекс, для быстрого поиска строки по короткой ссылке
        ]]
        box.space.redirector:create_index(
            'short',
            {
                parts={
                    {field='short', type='string'}},
                unique=true,
                if_not_exists=true,})

        --[[
            Индекс для шардирования данных
        ]]
        box.space.redirector:create_index(
            'bucket_id',
            {
                parts={
                    {field='bucket_id', type='unsigned'}},
                unique=false,
                if_not_exists=true,})
    end

    --[[
        Роль не пользуется кластерным конфигом
    ]]
    local function validate_config(new_conf, old_conf)
        return true
    end
    local function apply_config(conf, opts)
        return true
    end

    --[[
        Удаление таблиц предпочтительно не автоматизировать
    ]]
    local function stop()
    end

    --[[
        Возвращаем
          - имя роли для использования в GUI и rpc API
          - колбеки жизненного цикла
          - зависимости от других ролей
            - vshard-storage, роль которая будет заботиться о шардировании данных
              всех спейсов, у которых есть индекс `bucket_id`
    ]]
    return {
        role_name="storage",

        init=init,
        validate_config=validate_config,
        apply_config=apply_config,
        stop=stop,

        dependencies={'cartridge.roles.vshard-storage'}
    }
    ```
    :::

## Однонодовый запуск

- Запустим приложение
    ```bash
    tarantool init.lua
    ```
- Откроем страницу администрирования http://127.0.0.1:8081
- На вкладке `Cluster` на единственном репликасете выберем `Edit` и включим роли `appserver`, `storage`
    - Автоматически включатся зависимые роли

## Bootstrap vshard

- Если сейчас вставлять данные, будет ошибка о том, что шардированное хранилище не инициализировано
- Откроем страницу администрирования http://127.0.0.1:8081
- На вкладке `Cluster` нажмем `Bootstrap vshard`

## Кластер

- Запустим ещё один узел приложения
  - В другую директорию
  - С другим портом
  - С отключенным `http` сервером
  ```bash
  tarantool init.lua --advertise-uri 127.0.0.1:3302 \
    --http-enabled false --workdir spb
  ```
- На `web gui` на вкладке `Cluster` должен отобразиться несконфигурированный узел
  - Если не появился, нужно нажать `Probe uri` и ввести `advertise-uri` нового запущенного сервера `127.0.0.1:3302`
- Нажмем `Configure`, укажем роль `storage`, создадим новый репликасет с этим узлом `Create replica set`
- Таким образом получится два шарда
- Запустим тест для проверки, что всё получилось
    ```bash
    tarantool test.lua
    ```

## Интеграционное тестирование

- Особенностью тестирования является возможность собирать произвольную топологию кластера, и проводить сценарии используя один или даже все узлы
- Установить фреймворк для тестирования
    ```bash
    tarantoolctl rocks install luatest
    ```
- В директории `test`
- Имя файла заканчивается на `_test`
- <details> <summary><code>test/basic_test.lua</code></summary>

    ```lua=
    local fio = require('fio')
    local log = require('log')
    local client = require('http.client')
    local uri = require('uri')
    local t = require('luatest')
    local g = t.group('starwars')

    local helpers = require('cartridge.test-helpers')

    g.before_all(function()
        local tempdir = fio.tempdir()

        local cluster = helpers.Cluster:new({
                datadir = tempdir,
                server_command = 'init.lua',
                use_vshard = true,
                replicasets = {{
                    roles = {'appserver'},
                    servers = {{
                            alias = 'appserver',
                    }}},
                {
                    roles = {'storage'},
                    servers = {{
                            alias = 'storage',
                }}},
                {
                    roles = {'storage'},
                    servers = {{
                            alias = 'storage2',
                }}},
                },
        })
        cluster:start()
        g.cluster = cluster
    end)

    g.after_all(function()
        g.cluster:stop()
    end)

    function g.test_starwars_storage()
        local cluster = g.cluster

        local server = cluster:server('appserver')

        --[[
            Простой тест для проверки укорачивания ссылок
        ]]
        local url = uri.parse(server.advertise_uri)
        local host = url.host
        local port = 8080

        local source = 'https://google.com/?q=' .. tostring(os.time())
        local makeshort = 'http://' .. host .. ':' .. port .. '/?url='
        local response = client.get(makeshort .. source)
        local short = response.body
        log.info(short, '->', source)

        local redirect = client.get(short, {follow_location=true})
        t.assert_equals(redirect.headers['location'], source, 
                            "Source url and redirect not equal")

    end
    ```
    </details>
- `chmod +x init.lua`
- Завершить запущенный кластер
  ```bash
  pkill -9 tarantool
  ```
- Запуск тестов
    ``` bash
    .rocks/bin/luatest test/basic_test.lua
    ```
    ```
    Ran 1 tests in 2.233 seconds, 1 success, 0 failures
    ```

# TODO

- github.com/tarantool/metrics
    - curl | grep `box.space.redirector` запрос
- prometheus, grafana (подчеркнуть опциональность)
