Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

A Tarantool Project, Step-by-Step: The Good, the Bad, and the Ugly (Part 2)

DZone's Guide to

A Tarantool Project, Step-by-Step: The Good, the Bad, and the Ugly (Part 2)

Welcome back to our The Good, the Bad, and the Ugly series! In our second and final half of the series, we're going to build out our application for voting on Telegram stickers, using NGINX, PHP-FPM, and Tarantool.

· Database Zone ·
Free Resource

MariaDB TX, proven in production and driven by the community, is a complete database solution for any and every enterprise — a modern database for modern applications.

Image title

Welcome back to our The Good, the Bad, and the Ugly series! In Part 1 of this guide, we installed Tarantool and the other necessary components for our project, and also generated mock data. Now we’ll proceed with the actual program.

Writing the Program

First, we’ll need a simple request router, as it’s usually implemented in PHP:

# Get routes from request
$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();
}

Note the two variables, $vote_plus and $vote_minus, which will be passed in a POST request when a visitor votes on a sticker. Unfortunately, if someone knows the filename and its full path, it’s extremely easy to illegally increase the number of votes by using bots — and we certainly don’t want that to happen. That’s why for the voting page, we’ll be generating a couple of unique tokens, one per sticker. After a visitor votes, the tokens are deleted, so the vote can’t be cast again.

Since PHP had quite poor cryptographically secure functions prior to its 7.0 release, Tarantool’s rich cryptographic toolset comes to the rescue.

To begin with, let’s initialize a random number generator with a cryptographically secure (that is, really random) seed in the action_main function:

$r = $tarantool->evaluate(
  “digest = require(‘digest’)
  return (digest.urandom(4))”
 );
 $seed = unpack(‘L’, $r[0])[1];
 srand($seed);

The $tarantool->evaluate() function is used to directly run Lua code without the necessity of creating a stored procedure. Then the create_random_vote()function is called twice: it picks a random record from the space and creates a URL for the sticker and tokens.

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
  );
}

Two other functions are used in the snippet above: $tarantool->call() for calling stored procedures, and $tarantool->insert() for inserting a new record.

Below is an example of a procedure called update_votes that updates the rating of a record:

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
      )
   )
 );
}

You can find a full list of methods from the Tarantool class in the documentation.

A couple of things to note here: the “op” => “=” parameter means the field of an existing tuple gets updated with a new value. There are also other parameters, such as “+” or “-.” Normally, to change a value in a database, we must first read a field and then update it. To ensure data consistency, it’s necessary to block access to the table and use transactions. However, in Tarantool, thanks to its architecture, the update and upsert commands are atomic within the server process and thus don’t block the database. This allows us to build lightning-fast systems!

Below you can find the code contained in the index.php file at the time when this article was written:

<?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 negative update
const NO_UPDATE = 0;
const COOKIE = ‘uuid’;   // Cookie name
const HIDDEN = ‘/img/Question.svg’; // Picture for hidden element

# Get routes from request
$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 from request 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 = ‘Good and bad Telegram stickers’;
  include_once(‘main.html’);
}

function action_good() {
  $title = ‘Top best Telegram stickers’;
  $top = get_top(10,Tarantool::ITERATOR_LE);
  $active_good =’class=”active”’;
  $active_bad =’’;

  include_once(‘top.html’);
}

function action_bad() {
  $title = ‘Top worst Telegram stickers’;
  $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 = ‘The ugliest Telegram sticker’;
  $top = get_top(1,Tarantool::ITERATOR_GE);
  $votes = get_session($user);
# Hide the ugly until getting enough votes
  if ($votes < MIN_VOTES) {
    $ugly_message = “Please vote “ . MIN_VOTES . “ times to see the result<br>”;
    $ugly_message .= (MIN_VOTES — $votes) . “ more votes to go”;
    $ugly_img = HIDDEN;
  } else {
    $ugly_img = $top[0][4];
  }
  include_once(‘ugly.html’);
}

function action_about() {
  $title = ‘How is it done?’;
  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(‘uuid’)
# uuid()
#
##################################
    $uuid = $tarantool->evaluate(
      “uuid = require(‘uuid’)
      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);
}
?>

What’s left is to create HTML templates that will display stickers to vote on and rating pages.

Here is example code for the containers that hold images to vote on:

<!-- BEGINNING OF VOTING -->
<div class=”voting container”>
  <div class=”voting-zone”>
<!-- FIRST IMAGE CONTAINER -->
    <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>
<!-- SECOND IMAGE CONTAINER -->
<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>
<!-- END OF VOTING -->

Has anyone ever met a webpage designer who perfectly formats HTML code?

As can clearly be seen in the code snippet above, we assigned the same pair of tokens to the left and right images and just switched vote_plus and vote_minus. So the image that a visitor clicks gets a plus and the other receives a minus. The loser gets pelted with more and more downvotes, which drags them even lower into the infernal abyss. Good riddance! MWAHAHA

Conclusion

Those of you who’ve made it through all the trolling to the end of the article, after having repeatedly lost your temper at the author’s stupidity, deserve a reward. What can be more fun than a real working example that you can play with? Come to my site to vote on Telegram stickers and learn which one is the ugliest. Experience a Russian-made, high-load service built with NGINX, PHP-FPM, and Tarantool! Oh, and don’t forget to send the link to your friends — it takes lots of votes to get a statistically valid picture of user preferences.

MariaDB AX is an open source database for modern analytics: distributed, columnar and easy to use.

Topics:
database

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}