Tarantool: Хороший, Поганий, Злий

imageБагато чули про NoSQL базі даних Tarantool, знають про те, що вона вміє зберігати дані в пам'яті, дуже швидко їх обробляє і володіє високою продуктивністю. Тарантул був написаний серйозними хлопцями, які обслуговують сервіси з сотнями тисяч запитів в секунду.

Система здається складною. Незважаючи на російські коріння, спочатку навіть не було документації російською мовою. Чим же може допомогти цей потужний інструмент звичайним хлопцям — програмістам і розробникам-початківцям?

Спробуємо написати простий цікавий сервіс, здатний витримати велике навантаження. І ніякого SQL!

Наша мета
Щоб підігріти інтерес до процесу вивчення, візьмемо простий і цікавий приклад використання Тарантула в веб-розробці. Ми запустимо сайт, на якому виводиться попарне голосування за які-небудь картинки. Викладати фотографії студенток коледжу, як Марк Цукерберг, ми не будемо, але зате влаштуємо голосування по вибору стікери для месенджера Telegram. Наш алгоритм шляхом голосування буде вибирати топ з кращих наклейок, але насправді наше завдання знайти головного виродка колекції – містера The Ugly. Тепер, коли з'явилася мотивація, саме час зайнятися нудними справами.

Установка
Вороні десь бог послав шматочок сиру. Нам дістався скромний віртуальний сервер з одним процесорним ядром і оперативною пам'яттю 1 Гб — молодший нащадок з однієї благовоспитанной німецької родини. Операційна система — Debian, тому ми можемо встановити Тарантул з офіційного репозиторію: tarantool.org/download.html

В інструкції написано Copy and Paste, що ми і робимо з великим ентузіазмом:

curl http://download.tarantool.org/tarantool/1.7/gpgkey | sudo apt-key add -
release=`lsb_release -c -s`

# install https download transport for APT
sudo apt-get -y install apt-transport-https

# append two lines to a list of source repositories
sudo rm -f /etc/apt/sources.list.d/*tarantool*.list
sudo tee /etc/apt/sources.list.d/tarantool_1_7.list <<- EOF
deb http://download.tarantool.org/tarantool/1.7/debian/ $release main
deb-src http://download.tarantool.org/tarantool/1.7/debian/ $release main
EOF

# install
sudo apt-get update
sudo apt-get -y install tarantool

Маленький сюрприз від розробників: у офіційному репозиторії не підтримується архітектура i386, хоча сервіс хостингу пакетів справно надає всі файли, крім основного. Переконайтеся, що ви встановлюєте пакети на систему AMD64, щоб уникнути неприємностей.

Початок роботи
Після установки, у нас з'являється процес tarantool, запущений з тестовою конфігурацією example.lua, приблизно такий:

ps xauf | grep taran
root 2735 0.0 0.2 13972 2132 pts/0 S+ 22:05 0:00 \_ grep taran
taranto+ 568 0.0 0.8 812304 8632 ? Ssl 17:03 0:03 tarantool example.lua <running>

Для роботи з Tarantool використовується скриптова мова Lua, на якому пишуться команди управління сервером, описуються збережені процедури і тригери. Можна довантажити готові програмні модулі або написати свої.

Запустимо утиліту tarantoolctl, надрукуємо першу програму і переконаємося, що все працює.

tarantoolctl connect '3301'
connected to localhost:3301
localhost:3301> print ('The good!')
---
...
localhost:3301>

Зараз ми знаходимося в тестовій конфігурації, яка поставляється разом з дистрибутивом. Згідно ідеології Debian, налаштування знаходяться в директорії /etc/tarantool/instances.available/*, а засобом конфігурація програми створюється за допомогою символічного посилання в директорії /etc/tarantool/instances.enabled/*. Скопіюємо файл прикладу під новим маєтком і створимо наш проект.

Наш проект називається the good, the bad and the ugly, скорочено — gbu. Використовуйте скорочення повної назви проекту до перших букв, і ваші колеги завжди будуть з повагою і трепетом ставитися до вашої роботи!

Тепер трохи поправимо gbu.cfg і запустимо сервіс. Нагадаю, що для конфігурації використовується синтаксис мови Lua, в якому коментарі починаються з двох дефісів.

box.cfg {
- Хороша звичка відразу міняти порт за замовчуванням для сервісу
listen = 3311;
-- Підганяємо розмір пам'яті під наші потреби
slab_alloc_arena = 0.2; 
-- Інші параметри залишаємо без змін
}

local function bootstrap()
local space = box.schema.create_space('example')
space:create_index('primary')
-- Закомментіруем користувача за промовчанням
-- box.schema.user.grant('guest', 'read,write,execute', 'всесвіт')
-- Створимо нового користувача
box.schema.user.create('good', { password = 'secret' })
box.schema.user.grant('good', 'read,write,execute', 'всесвіт')
end
-- При першому запуску створюємо простір і призначаємо привілеї
box.once('example-2.0', bootstrap)

Запускаємо новий інстанси командою tarantoolctl start gbu і переконуємося, що все працює як треба:

tarantoolctl connect "good:secret@0:3311"
connected to 0:3311
0:3311>

Ми в справі!

База даних
  • Записи в Tarantool зберігаються в просторах (space), це такий аналог таблиці в реляційній базі даних SQL. Їх можна зробити стільки, скільки буде потрібно — до 65 тисяч
  • Всередині простору знаходяться кортежі (tuples), які схожі і на рядок у таблиці SQL, і на JSON-масив даних. Максимальний розмір кортежу — до 1 Мб при налаштуваннях за замовчуванням. Цей параметр можна змінювати.
  • Щоб від бази даних була користь, необхідно створити індекси, так само, як в SQL. Завдяки їм, замість перебору повного списку всіх елементів пошук відбувається по більш швидкого алгоритму. Також доступна сортування. Вибір індексу залежить від типу даних.
HASH-індекс: значення повинно бути унікальним, і може бути довільним. Так організуються всім відомі Key/Value-сховища, відомі також як Map. Хороший приклад — контрольна сума файлу MD5 hash.

TREE-індекс: значення може бути неунікальним, але має бути «щільним» для організації сортованого списку. Виходить масив (Array), у якого можуть бути пропущені елементи. Хороший приклад — номер замовлення, який збільшується на одиницю.

Якщо потрібно унікальне значення, то ви можете використовувати HASH або TREE, при цьому HASH буде швидше на розріджених даних. Якщо ж потрібно не унікальне поле, по якому буде виконуватися сортування, то можна використовувати тільки TREE-індекс.

Також є індекси RTREE для пошуку на двомірній площині і BITSET для роботи з растровими даними, але нам вони не потрібні. Докладніше про це написано в документації.

Картинка з статті Євгена Шадріна Освоюємо Тарантул. До речі, хороше керівництво для початкового знайомства.

Tarantool data model

Модель даних проекту
В моделі даних нашого додатка створимо простір stikers для зберігання інформації про файли. Зверніть увагу, що нумерація полів починається з 1, оскільки використовується синтаксис мови Lua. Кортеж містить такі поля:

  1. unsigned id — унікальний номер стікера
  2. integer rating — рейтинг стікера
  3. string pack — назва стікер-пака
  4. string name — назва файлу стікера
  5. (string) path — URL стікера
  6. (number) up — кількість голосів за стікер
  7. (number) down — кількість голосів проти стікера.
У просторі packs ми будемо зберігати список стікер-паків:

  1. string pack — назва стікер-пака
  2. integer rating — рейтинг стікер-пака
  3. (string) path — посилання на сторінку опису
У просторі secrets ми будемо зберігати токен для шифрування посилання на картинку, щоб реалізувати найпростішу захист від накрутки:

  1. string token — випадковий токен для стікера
  2. integer time — час створення токена (нагоді для видалення старих)
  3. (integer) id — унікальний номер стікера (ключ простору stickers)
  4. (string) url — URL стікера
У просторі sessions ми будемо записувати відвідувачів і збирати статистику:

  1. string uuid — унікальний символьний ідентифікатор відвідувача
  2. integer uuid_time — час створення сесії (в нагоді для видалення старих)
  3. (number) user_votes — скільки разів проголосував відвідувач
  4. (string) ip — IP-адреса відвідувача
  5. (string) agent — тип браузера відвідувача
У просторі server будемо просто збирати статистику роботи сайту:

  1. Integer id — просто ключ
  2. (number) visitors — кількість унікальних користувачів
  3. (number) votes — загальна кількість показів голосувань
  4. (number) clicks — загальна кількість кліків в голосуванні
Зверніть увагу, що для призначення індексу потрібно явно вказати тип поля. Цей тип повинен бути вибраний зі списку можливих варіантів Тарантула.

Інші поля можуть мати довільний тип, який підтримує вбудований інтерпретатор Lua. Цей дуалізм типів даних є особливістю Тарантула і згадується в документації. Для зручності, ми вказали тип даних Lua в дужках при описі моделі.

Важлива частина в моделюванні — складання індексів. Велика перевага Тарантула полягає в тому, що він вміє робити складні складові індекси. Завдяки цьому, ми можемо писати швидкі аналітичні запити на основі різних полів кортежу без зменшення швидкодії системи. Додамо первинний індекс типу TREE для поля id, щоб забезпечити випадковий вибір елемента для голосування. Другий індекс — типу TREE по полю Raiting, щоб виводити рейтинг, звичайно ж! Додамо індекс по полях Pack + Emoj типу HASH, який повинен бути унікальним. Його можна використовувати для аналізу популярності наборів стікерів.

Код створення бази даних ми розмістимо у нашому файлі gbu.lua, процедури ініціалізації

function bootstrap()
local function bootstrap()

box.schema.user.create('good', { password = 'secret' })
box.schema.user.grant('good', 'read,write,execute', 'всесвіт')

-----------------------------------
-- Простір стікерів
local stickers = box.schema.create_space('stickers')
-- Індекс для файлу
stickers:create_index('primary', { 
type = 'TREE', parts = {1, 'unsigned'}
})

-- Індекс для рейтингу
stickers:create_index('secondary', {
type = 'TREE',
unique = false,
parts = {2, 'integer'}
})

-- Індекс для назв стікерів
stickers:create_index('ternary', {
type ='HASH', parts = {3, 'string', 4, 'string'}
})

-----------------------------------
-- Простір стікер-пака
local packs = box.schema.create_space('packs')
-- Індекс для стікер-пака
packs:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})

-- Індекс для рейтингу пака
packs:create_index('secondary', {
type = 'TREE',
unique = false, 
parts = {2, 'integer'}
})

-----------------------------------
-- Простір секретних посилань
local secret = box.schema.create_space('secret')

-- Індекс для секретного ключа
secret:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})

-- Індекс часу для створення ключа
secret:create_index('secondary', {
type = 'TREE',
unique = false, 
parts = {2, 'integer'}
})

-----------------------------------
-- Простір сесій користувачів
local sessions = box.schema.create_space('sessions')

-- Індекс для унікального користувача 
sessions:create_index('primary', {
type = 'HASH', parts = {1, 'string'}
})

-- Індекс для часу створення сесії
sessions:create_index('secondary', {
type = 'TREE',
unique = false, 
parts = {2, 'integer'}
})

-----------------------------------
-- Простір статистики сервера
local server = box.schema.create_space('server')

-- Просто індекс 
server:create_index('primary', {
type = 'TREE', parts = {1, 'unsigned'}
})
-- Створимо запис
server:insert{1, 0, 0, 0}

end


Перш ніж перезапустити сервер з налаштуваннями, спробуйте створити схему командами в консолі. Якщо щось піде не так, можна видалити простір цілком командою:
box.space.stickers:drop()

або окремий індекс:
box.space.stickers.index.ternary:drop()


Не соромтеся використовувати підказку клавішею TAB. Для зручності роботи в консолі, назви створюваних елементів схеми ми пишемо з маленької літери. Команди для роботи у консолі стають інтуїтивно зрозумілі після короткого ознайомлення з документацією.
Очистити простір:
box.space.stickers:truncate()

Видалити простір:
box.space.stickers:truncate()


Все відбувається миттєво, як і годиться для In-memory Database!

Установка компонентів
Сильну заяву
Хороший сучасний мову програмування повинен мати статичну строгу типізацію, Homoiconicity — властивість, що дозволяє маніпулювати кодом як даними, підтримку ООП, FFI до бібліотек на C, підтримку дженериків, конкурентного програмування, Functions as first-class citizens, lambdas.

Нічого цього в PHP, звичайно ж, немає! Тому ми будемо писати код приклад саме на ньому.

Для початку поставимо перевірені інструменти веб — сервер Nginx і інтерпретатор PHP — php-fpm: wiki.debian.org/ru/nginx/nginx+php-fpm

В кореневий шлях конфігурації nginx додамо правило перезапису запитів:

location / {
try_files $uri $uri/ /index.php?q=$uri&$args;
}

Таким чином, ми можемо отримувати в PHP-скрипті красиві посилання виду /good із масиву
$_REQUEST['q']
, і реалізуємо роутинг HTTP-запитів.

Ще у нас є локейшин для виконання CGI-запитів:

location ~* \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
expires -1;
}

Командою
expires -1;
ми відключаємо кешування запитів, воно не потрібно для сторінок голосування і виведення Топ-чартів. Інші локейшины кешируют дані на 24 години або 30 днів з вищестоящих налаштувань HTTP. Напевно, у кожного є свій збірник готових опції Nginx.

Тепер необхідно поставити модуль для роботи з Тарантулом:

sudo apt-get install php5-cli php5-dev php-pear
pecl channel-discover tarantool.github.io/tarantool-php/pecl
pecl install Tarantool-PHP/Tarantool-beta

Читаємо, що написано у висновку програми-інсталятора. В моєму випадку було повідомлення:

Build process completed successfully
Installing '/usr/lib/php5/20131226/tarantool.so'
install ok: channel://tarantool.github.io/tarantool-php/pecl/Tarantool-0.0.13
configuration option "php_ini" is not set to php.ini location
You should add "extension=tarantool.so" to php.ini

Додаємо зазначену рядок у файл конфігурації у файлі/etc/php5/fpm/php.ini /etc/php5/cli/php.ini. На жаль, при запуску PHP отримуємо помилку! Щоб не страждати з налагодженням web-сервера, ми додали нову бібліотеку і у cli-конфігурацію, тому можна перевірити працездатність з командного рядка.

php -v
PHP Warning: PHP Startup: Unable to load dynamic library '/usr/lib/php5/20131226/tarantool.so' - /usr/lib/php5/20131226/tarantool.so: undefined symbol: tarantool_schema_destroy in Unknown on line 0
PHP 5.6.29-0+deb8u1 (cli) (built: Dec 13 2016 16:02:08)

На момент написання статті модуль в репозиторії PEAR містив помилку, тому залишається лише шлях джедая — збірка драйверів з вихідних кодів.

pecl uninstall Tarantool-PHP/Tarantool-beta
cd ~
git clone https://github.com/tarantool/tarantool-php.git
cd tarantool-php
phpize
./configure
make
make install

Завантаження даних
Створимо перший файл і назвемо його test.php, в ньому ми перевіримо роботу нашої бази даних.

<?php
$tarantool = new Tarantool('localhost', 3311, 'good', 'secret');
try {
$tarantool->ping();
} catch (Exception $e) {
echo "Exception: ", $e->getMessage(), "\n";
}
?> 

Запускаємо з командного рядка
php config.php
і перевіряємо, як спрацювало. Якщо неправильно, то отримаємо повідомлення про помилку. Перевірте!

Тепер можна написати парсер, який збере дані з потрібного нам сайту. Ми будемо досліджувати tlgrm.ru/stickers. Спочатку завантажимо таблицю pack, в якій у нас лежить перепис стікер-паків. Ось так виглядає команда insert в командному рядку tarantool:

box.space.packs:upsert({'key1',0}, {{'=',2,0}})

Ця команда додає новий ключ «key1» (поле 1) і значення 0 (поле 2). Якщо запис існує, то оновлюється для цієї ж запису (знак =) в поле 2 значенням 0. Як ми пам'ятаємо, у полі 2 у нас є рейтинг, який ми спочатку в 0. Команду upsert зручно використовувати для багаторазового запуску програми при налагодженні, щоб не видаляти кожен раз внесені дані. PHP-варіант команди буде виглядати так:

$tarantool->upsert('packs', array ($pack,0), array (
array(
"field" => 1,
"op" => "=",
"arg" => 0
)
));

Ай-ай! В PHP нумерація полів з 0, а в Lua c 1. Тому
"field" => 1
з масиву PHP відповідає запису
{'=',2,0}
Lua. Скрізь, де масиви починаються з нуля, поточні коннектори працюють так само. Це поведінка було змінено починаючи з версії 1.6. Читаючи приклади в інтернеті, звертайте увагу на версію Тарантула! Ця стаття написана по версії 1.7, а про версію 1.5 розробники просять не згадувати зовсім.

Запис для стікера ми вносимо за допомогою вбудованої процедури auto_increment, яка автоматично збільшує первинний індекс. Команда Тарантула:

box.space.stickers:auto_increment({0,'pack2','sticker2'})

PHP:

$tarantool->call('box.space.stickers:auto_increment', array(
array(0,$pack, $i . '.png', $url, 0, 0)
));

Отже, скрипт написаний. Запускаємо його — вжух, і магія! Тепер у нас є база даних з 16 000 записів!

Пишемо програму
Для початку зробимо найпростіший роутер запитів, як це зазвичай робиться в PHP:

# Get routes request from
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : ";
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : ";

switch ($route) {
case '/good':
action_good();
break;
case '/bad':
action_bad();
break;
case '/ugly':
action_ugly();
break;
case '/about':
action_about();
break;
default:
if (!empty($vote_plus) && !empty($vote_minus)) {
sleep (1);
do_post($vote_plus, $vote_minus);
}
action_main();
}

Зазначимо наявність двох змінних $vote_plus та $vote_minus, які будуть передаватися в POST-запит при голосуванні за ту чи іншу картинку. Справа в тому, що, знаючи ім'я і шлях файлу дуже легко накрутити голосування ботами, а нам це не потрібно. Тому ми будемо сторінки для голосування генерувати кілька унікальних токенів, по одному на кожну картинку. Після голосування токен буде видалятися, роблячи неможливим повторне використання голосу.

Оскільки в PHP до виходу версії 7.0 справи з криптобезопасными функціями йдуть сумно, то дуже допоможу багаті можливості Тарантула по роботі з криптографією.

Для початку в функції action_main ініціюємо генератор випадкових чисел криптобезопасным (тобто дійсно випадковим) seed:

$r = $tarantool->evaluate(
"digest = require('digest')
return (digest.urandom(4))"
);

$seed = unpack('L', $r[0])[1];
srand($seed);

Функція $tarantool->evaluate() використовується безпосередньо для запуску коду Lua без метушні з створенням збереженої процедури. Потім два рази викликаємо функцію create_random_vote(), яка обере випадковий елемент в просторі і створить URL картинки і токенів.

function create_random_vote()
function create_random_vote() {
# Get random sticker id

global $tarantool;
$tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
$id = $tuple[0][0];
$url = $tuple[0][4];

# Create random sticker token
$token = $tarantool->evaluate(
"digest = require('digest')
return ( digest.md5_hex(digest.urandom(32)))"
)[0];

$time = time();

# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
$tarantool->insert('secret', array ($token, $time, $id, $url));

return array (
$url,
$token
);
}


Тут використовувалися ще дві функції:$tarantool->call() для виклику вбудованих процедур і $tarantol->insert() для вставки нового запису.

Наведемо приклад процедури оновлення голосування, яка оновлює рейтинг запису:

function update_votes($id, $plus, $minus)
function update_votes($id, $plus, $minus) {
global $tarantool;

###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
$tarantool->update("stickers", $id,
array (
array(
"field" => 5,
"op" => "+",
"arg" => $plus
),
array(
"field" => 6,
"op" => "+",
"arg" => $minus
)
)
);
}


Повний перелік методів класу Tarantool дивіться в документації tarantool-php.

Зверніть увагу на параметр "
op" => "="
, який означає, що відбувається заміна поля в існуючому кортежі. Також є параметр +,- і деякі інші операції. Вони виконують дуже важливу задачу. Зазвичай для заміни значення в базі даних ми спочатку читаємо якесь поле, потім змінюємо його. Щоб зберегти консистентним даних, доводиться блокувати доступ до таблиці і використовувати транзакції. У Тарантуле ж, завдяки його архітектурі, команди update та upsert спрацьовують атомарно всередині процесу сервера без блокування бази даних. Це дозволяє будувати біса швидкі системи!

Код файлу index.php на момент написання статті знаходиться під спойлером:

index.php
<?php

# Init database
$tarantool = new Tarantool('localhost', 3301, 'good', 'bad');

try {
$tarantool->ping();
} catch (Exception $e) {
echo "Exception: ", $e->getMessage(), "\n";
}

const MIN_VOTES = 20; // Number of votes to show the ugly
const UPDATE_PLUS = 1; // Increment for positive update
const UPDATE_MINUS = -1; // Increment for update negative
const NO_UPDATE = 0;
const COOKIE = 'job'; // Cookie name
const HIDDEN = '/img/Question.svg';// Picture for hidden element

# Get routes request from
$route = isset($_REQUEST['q']) ? $_REQUEST['q'] : '/';
$vote_plus = isset($_REQUEST['vote_plus']) ? $_REQUEST['vote_plus'] : ";
$vote_minus = isset($_REQUEST['vote_minus']) ? $_REQUEST['vote_minus'] : ";

# Get cookie request from or create new value
$cookie = isset($_COOKIE[COOKIE]) ? $_COOKIE[COOKIE] : update_user(");

switch ($route) {
case '/good':
action_good();
break;
case '/bad':
action_bad();
break;
case '/ugly':
action_ugly($cookie);
break;
case '/about':
action_about();
break;
default:
# This is post request:
if (!empty($vote_plus) && !empty($vote_minus)) {
sleep (1);

$cookie = update_user($cookie);
do_post($vote_plus, $vote_minus);

}
setcookie(COOKIE, $cookie, time() + (86400 * 30), "/");
action_main();
}

exit();

function action_main() {
global $tarantool;

# Get crypto safe random seed from Tarantool LUA module
# https://tarantool.org/doc/reference/reference_lua/digest.html
$r = $tarantool->evaluate(
"digest = require('digest')
return (digest.urandom(4))"
);

$seed = unpack('L', $r[0])[1];
srand($seed);

list ($left_url, $left_token_plus) = create_random_vote();
list ($right_url, $right_token_plus) = create_random_vote();

$left_token_minus = $right_token_plus;
$right_token_minus = $left_token_plus;

update_stats(UPDATE_PLUS, NO_UPDATE);

$title = 'Хороші і погані стікери Telegram';
include_once('main.html');
}

function action_good() {
$title = 'ТОП кращих стікерів Telegram';
$top = get_top(10,Tarantool::ITERATOR_LE);
$active_good ='class="active"';
$active_bad =";

include_once('top.html');
}

function action_bad () {
$title = 'ТОП найгірших стікерів Telegram';
$active_bad ='class="active"';
$active_good =";
$top = get_top(10,Tarantool::ITERATOR_GE);
# Hide the ugly
$top[0][4] = HIDDEN;
include_once('top.html');
}

function action_ugly($user) {
$title = 'Найгірший стікер Telegram';
$top = get_top(1,Tarantool::ITERATOR_GE);
$votes = get_session($user);
# Hide the ugly until getting enough votes
if ($votes < MIN_VOTES) {
$ugly_message = "Голосуй " . MIN_VOTES . " раз щоб побачити результат<br>";
$ugly_message .= "Залишилося " . (MIN_VOTES - $votes) . " голосувань";
$ugly_img = HIDDEN;
} else {
$ugly_img = $top[0][4];
}
include_once('ugly.html');
}

function action_about() {
$title = 'Як це зроблено?';
list($stickers, $shows, $votes, $visitors) = get_server_stats();
include_once('about.html');
}

function do_post($vote_plus, $vote_minus) {
global $tarantool;

$tuple_plus = $tarantool->select("secret", $vote_plus);
$tuple_minus = $tarantool->select("secret", $vote_minus);

$id_plus = $tuple_plus[0][2];
$id_minus = $tuple_minus[0][2];

# Clean up used tokens
if (!empty($vote_plus) && !empty($vote_minus)) {
$tarantool->delete("secret", $vote_plus);
$tarantool->delete("secret", $vote_minus);
}

# Get actual tuple data
if (!empty($id_plus) && !empty($id_minus)) {

$raiting = +1;
update_rating($id_plus, $raiting);

$raiting = -1;
update_rating($id_minus, $raiting);

update_votes($id_plus, UPDATE_PLUS, NO_UPDATE);
update_votes($id_minus, NO_UPDATE, UPDATE_MINUS);

update_stats(NO_UPDATE, UPDATE_PLUS);
}
}

function create_random_vote() {
# Get random sticker id

global $tarantool;
$tuple = $tarantool->call("box.space.stickers.index.primary:random", array(rand()));
$id = $tuple[0][0];
$url = $tuple[0][4];

# Create random sticker token
$token = $tarantool->evaluate(
"digest = require('digest')
return ( digest.md5_hex(digest.urandom(32)))"
)[0];

$time = time();

# Set secure token to protect post action
##################################################################
#
# box.space.secret:insert({key', 0, 456, 'bla-bla'})
#
##################################################################
$tarantool->insert('secret', array ($token, $time, $id, $url));

return array (
$url,
$token
);
}

function update_rating($id, $update) {
global $tarantool;

#################################################
#
# box.space.stickers:update(7856, {{'+', 2, 10}})
#
#################################################
$tarantool->update("stickers", $id, array (
array(
"field" => 1,
"op" => "+",
"arg" => $update
)
));
}

function update_votes($id, $plus, $minus) {
global $tarantool;

###########################################################
#
# box.space.stickers:update(1, {{'+', 6, 1}, {'+', 7, -1}})
#
###########################################################
$tarantool->update("stickers", $id,
array (
array(
"field" => 5,
"op" => "+",
"arg" => $plus
),
array(
"field" => 6,
"op" => "+",
"arg" => $minus
)
)
);
}

function update_user($cookie) {
global $tarantool;

# Create uuid if first time user
if (empty($cookie)) {

##################################
#
# uuid = require('job')
# uuid()
#
##################################
$uuid = $tarantool->evaluate(
"uuid = require('job')
return (uuid.str())"
)[0];
} else {
$uuid = $cookie;
}
$time = time();
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ";
$agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ";

# Create session or update user stat inside
###########################################################
#
# box.space.sessions:upsert({'111222333', 123456, 0, 'ip', 'agent'},
# {{'=', 2, 1}, {'+', 3, 1}, {'=', 4, 'ip'}, {'=', 5, 'agent'}})
#
###########################################################
# Please check https://github.com/tarantool/tarantool-php/issues/111
$tarantool->upsert("sessions", array($uuid, $time, 0, $ip, $agent),
array (
array(
"field" => 1
"op" => "=",
"arg" => $time
),
array(
"field" => 2
"op" => "+",
"arg" => 1
),
array(
"field" => 3
"op" => "=",
"arg" => $ip
),
array(
"field" => 4,
"op" => "=",
"arg" => $agent
)
)
);
return($uuid);
}

function update_stats($vote, $click) {
global $tarantool;

########################################################
#
# box.space.server update(1, {{'+', 3, 1}, {'+', 4, 1}})
#
########################################################
$tarantool->update("server",1,
array (
array(
"field" => 2,
"op" => "+",
"arg" => $vote
),
array(
"field" => 3,
"op" => "+",
"arg" => $click
)
)
);
}

function get_session($sid) {
global $tarantool;

##########################################
#
# box.space.sessions:select('id')
#
#########################################
if (strlen($sid) > 16) {
return $tarantool->select("sessions", $sid)[0][2];
} else {
return 0;
}
}

function get_top($limit, $iterator) {
global $tarantool;

######################################################################################
#
# box.space.stickers.index.secondary:select({primary}, {iterator = box.index.GE, offset=0, limit=10})
#
######################################################################################
$result = $tarantool->select("stickers", null, 'secondary', $limit, 0, $iterator);
return $result;
}
function get_server_stats() {
global $tarantool;

$time = time() - 30*86400; // one month before
$stickers = $tarantool->call('box.space.stickers:count')[0][0];

$tuple = $tarantool->select('server',1);

$shows = $tuple[0][2];
$votes = $tuple[0][3];

$visitors = $tarantool->call('box.space.sessions.index.secondary:count',
array($time, array('iterator' => Tarantool::ITERATOR_GE))
)[0][0];

# $shows, $votes, $visitors) = get_server_stats();

return array($stickers, $shows, $votes, $visitors);
}
?>


Залишилося зробити HTML-шаблони, які будуть виводити дані з картинками для голосування на сайт і сторінки рейтингів.

Приклад коду для картинок з голосуванням
<!-- ПОЧАТОК ГОЛОСУВАННЯ -->
<div class="voting container">
<div class="voting-zone">
<!-- ПЕРШИЙ КОНТЕЙНЕР З КАРТИНКОЮ -->
<div class="sticker" onclick="myFunction()">
<form name="voteFormLeft" id="idForm" method ="POST" action="/" >
<input class= "pic1" id="left_url" type="image" src="<?php echo $left_url?>" alt="Vote left" >
<input type="hidden" name="vote_plus" value="<?php echo $left_token_plus?>">
<input type="hidden" name="vote_minus" value="<?php echo $left_token_minus?>">
</form>
</div>
<!-- ДРУГИЙ КОНТЕЙНЕР З КАРТИНКОЮ -->
<div class="sticker" onclick="myFunction()">
<form name="voteFormRight" id="idForm" method ="POST" action="/">
<input class= "pic2" id="right_url" type="image" src="<?php echo $right_url?>" alt="Vote, right" >
<input type="hidden" name="vote_plus" value="<?php echo $right_token_plus?>" >
<input type="hidden" name="vote_minus" value="<?php echo $right_token_minus?>" >
</form>
</div>
</div>
</div>
<!-- КІНЕЦЬ ГОЛОСУВАННЯ -->


Кому-небудь вдалося зустріти в житті дизайнера-верстальника, який би ідеально відформатував HTML-код?

З вищенаведеного коду неважко здогадатися, що ми видали для лівої і правої картинки однакову пару токенів, тільки поміняли місцями vote_plus та vote_minus. (Зазвичай такими фразами автор як би підкреслює свою інтелектуальну перевагу над читачами). Таким чином, на яку картинку не клікнув користувач, вона отримає плюс, а її конкурент — мінус. Невдаха з кожним разом стає все більше мінусів і валиться все нижче в пекло. Туди йому і дорога ХА – ХА – ХА!

Читачеві, який дістався до кінця статті, витерпів тролінг автора і не раз вскипал від його дурниці, покладається заслужена нагорода. Що може бути цікавіше, ніж реальний працюючий приклад, який можна потикати і поклікати? Приходь і голосуй за стікери Telegram на сайт ugly.begetan.me, щоб дізнатися, який же з них найбільш потворний. Поковыряй мишкою наш російський хайлоад, зібраний з NGNIX, PHP-FPM і Tarantool. І не забудь показати посилання своїй подружці, адже необхідно досить багато голосів, щоб отримати статистично-достовірну картину голосування.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.