---
# System prepended metadata

title: OTUS Apps Practice (4/5)
tags: [Tarantool, ' trainings', ' otus', ' apps']

---

---
title: OTUS Apps Practice (4/5)
tags: Tarantool, trainings, otus, apps 
description:
---

# Практика: Сервис сокращения ссылок

## ТЗ 

- Пользователь хотел бы сократить ссылку:
    - Чтобы вместить её в смс или в твиттер
    - Сохранить в QR код
    - Или даже расположить в печатной продукции
- Необходимо хранение соответствий

## Общий план

- Настроить хранилище Tarantool (понедельник)
- Настроить репликацию (вторник)
- Настроить масштабирование (вчера)
- Запустить http-сервер (сегодня)
- *Собрать проект вместе и все готово :)* (завтра)

## День 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` парсер воспользовавшись только первой строкой протокола
- Доступ к данным будет выполняться всё также через `vshard.router.callrw`(`-ro`)

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

- Возьмём топологию попроще для удобства запуска и отладки

```graphviz
digraph onnode {
    subgraph cluster_1 {
        label="West Shard"
        
        subgraph cluster_mcs_west {
            label="MCS";
            mcs_west_r[label="Router"]
            mcs_west_s[label="West Storage\nReplica 2" shape=cylinder]
        }
        
        style=dashed
    }
    
    subgraph cluster_2 {
        label="East Shard"
        
        subgraph cluster_mcs_east {
            label="MCS";
            mcs_east_r[label="Router"]
            mcs_east_s[label="East Storage\nReplica 2" shape=cylinder]
        }
        
        style=dashed
    }

    mcs_west_r -> mcs_west_s [weight=100]
    #aws_west_r -> aws_west_s [weight=100]
    mcs_east_r -> mcs_east_s [weight=100]
    #aws_east_r -> aws_east_s [weight=100]
    
    mcs_west_r -> mcs_east_s # aws_east_s, aws_west_s, 
    #aws_west_r -> mcs_west_s, mcs_east_s, aws_east_s
    mcs_east_r -> mcs_west_s#, aws_west_s, aws_east_s
    #aws_east_r -> mcs_west_s, aws_west_s, mcs_east_s
    
    #aws_west_s -> mcs_west_s [dir="both", constraint=false]
    #aws_east_s -> mcs_east_s [dir="both", constraint=false]
}
```

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

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

    --[[
        HTTP сервер обработки запросов
    ]]
    local serveraddr = '0.0.0.0'
    local port = 8080
    local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки
    --[[
        Утилиты для декодирования значений 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 function handler(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 = app.shorten(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 = app.url(short)
            if source == nil then
                client:write(response404())
            end

            client:write(redirect(source))
        end
    end

    local server

    local function init()
        server = socket.tcp_server(
            serveraddr, port,
            handler)
        if server ~= nil then
            server:listen()
        end
    end

    local function stop()
        if server ~= nil then
            server:close()
        end
        server = nil
    end

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

    local instance_name = assert(
        os.getenv('TT_INSTANCE_NAME'), "TT_INSTANCE_NAME required"
    )
    local data_dir = os.getenv('TT_DATADIR') or "data/"..instance_name
    if not fio.stat(data_dir) then
        fio.mktree(data_dir)
    end

    vshard.storage.cfg({
            bucket_count = topology.BUCKET_COUNT,
            sharding = topology.sharding,
            memtx_dir = data_dir,
            wal_dir = data_dir,
            replication_connect_quorum = 1,
        },
        assert(os.getenv('TT_INSTANCE_UUID',"TT_INSTANCE_UUID required"))
    )

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

    schema.init()
    app = require 'app'

    httpd = require 'httpd'
    httpd.init()
    ```
    
### Запуск

- Удалим устаревшие рабочие директории
    ```bash
    rm -rf data/*
    ```
- Запустим два узла
    ```bash
    ./mcs_west_01.sh
    ```
    ```bash
    ./mcs_east_01.sh
    ```
- Вновь требуется бутстрап
    ```
    tarantoolctl connect 127.0.0.1:3301
    ```
    - `tarantool>`
      ```lua
      vshard.router.bootstrap()
      ```

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

- Откроем браузер на `http://<ip_address>:8080/`
    - Должна появится форма для ввода `url`
- Введем любой `url`
- Получим короткую ссылку
- Перейдем по короткой ссылке
- Или можно взять `curl`
  ```bash
   curl -G "http://localhost:8080/"\
     --data-urlencode "url=https://mail.ru"
   ```
   ```bash
   curl -L $(!!)
   ```
- Или можно воспользоваться готовым скриптом, который пытается укоротить `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
    ```

