diff --git a/README.md b/README.md
index 821df0a376957811e09a9c10494f08ab5608fb0d..d848211dae901237a339fb9c1ca0966e65c1484d 100644
--- a/README.md
+++ b/README.md
@@ -4,10 +4,9 @@
 
 http://tarantool.org
 
-Tarantool is an efficient NoSQL database and a
-Lua application server.
+Tarantool is an in-memory database and application server.
 
-Key features of the Lua application server:
+Key features of the application server:
  * 100% compatible drop-in replacement for Lua 5.1,
    based on LuaJIT 2.0.
    Simply use #!/usr/bin/tarantool instead of
diff --git a/doc/sphinx/book/administration.rst b/doc/sphinx/book/administration.rst
index 2b1ec8d20a2af9af2f40ee940e259a88ce1b267b..e030a78b86a929b8be518b42ceb3931b847fa625 100644
--- a/doc/sphinx/book/administration.rst
+++ b/doc/sphinx/book/administration.rst
@@ -87,7 +87,7 @@ sensitive so ``insert`` and ``Insert`` are not the same thing.
 String literals are: Any sequence of zero or more characters enclosed in
 single quotes. Double quotes are legal but single quotes are preferred.
 Enclosing in double square brackets is good for multi-line strings as
-described in `Lua documentation`_. Examples: 'Hello, world', 'A', [[A\B!]].
+described in `Lua documentation`_. Examples: 'Hello, world', 'A', [[A\\B!]].
 
 .. _Lua documentation: http://www.lua.org/pil/2.4.html
 
diff --git a/doc/sphinx/book/app_d_plugins.rst b/doc/sphinx/book/app_d_plugins.rst
index 85f75ae6180e8c28463be89990a0e92a13082ce3..c8f45a3321587002dcfc9596c0280ca542e53c91 100644
--- a/doc/sphinx/book/app_d_plugins.rst
+++ b/doc/sphinx/book/app_d_plugins.rst
@@ -20,7 +20,7 @@ To call another DBMS from Tarantool, the essential requirements are: another
 DBMS, and Tarantool.
 
 It will be necessary to build Tarantool from source, as described in
-“ :ref:`building-from-source` ”
+“:ref:`building-from-source`”.
 
 .. _Tarantool Plugin API wiki page: https://github.com/tarantool/tarantool/wiki/Plugin-API
 
@@ -34,8 +34,9 @@ which work best when the application can work on both SQL and Tarantool inside
 the same Lua routine.
 
 The connection method is
-``box.net.sql.connect('mysql'|'pg', host, port, user, password, database)``.
-The methods for select/insert/etc. are the same as the ones in the net.box package.
+:samp:`box.net.sql.connect('mysql'|'pg', {host}, {port}, {user}, {password}, {database})`.
+The methods for select/insert/etc. are the same as the ones in the
+:ref:`net.box <package_net_box>` package.
 
 
 ===========================================================
@@ -49,123 +50,121 @@ The example was run on a Linux machine where the base directory had a copy of
 the Tarantool source on ~/tarantool, and a copy of MySQL on ~/mysql-5.5. The
 mysqld server is already running on the local host 127.0.0.1.
 
-::
-
-    # Check that the include subdirectory exists by looking for .../include/mysql.h.
-    # (If this fails, there's a chance that it's in .../include/mysql/mysql.h instead.)
-    $ [ -f ~/mysql-5.5/include/mysql.h ] && echo "OK" || echo "Error"
-    OK
-
-    # Check that the library subdirectory exists and has the necessary .so file.
-    $ [ -f ~/mysql-5.5/lib/libmysqlclient.so ] && echo "OK" || echo "Error"
-    OK
-
-    # Check that the mysql client can connect using some factory defaults:
-    # port = 3306, user = 'root', user password = '', database = 'test'.
-    # These can be changed, provided one uses the changed values in
-    # all places.
-    $ ~/mysql-5.5/bin/mysql --port=3306 -h 127.0.0.1 --user=root --password= --database=test
-    Welcome to the MySQL monitor.  Commands end with ; or \g.
-    Your MySQL connection id is 25
-    Server version: 5.5.35 MySQL Community Server (GPL)
-    ...
-    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
-
-    # Insert a row in database test, and quit.
-    mysql> CREATE TABLE IF NOT EXISTS test (s1 INT, s2 VARCHAR(50));
-    Query OK, 0 rows affected (0.13 sec)
-    mysql> INSERT INTO test.test VALUES (1,'MySQL row');
-    Query OK, 1 row affected (0.02 sec)
-    mysql> QUIT
-    Bye
-
-    # Build the Tarantool server. Make certain that "cmake" gets the right
-    # paths for the MySQL include directory and the MySQL libmysqlclient
-    # library which were checked earlier.
-    $ cd ~/tarantool
-    $ make clean
-    $ rm CMakeCache.txt
-    $ cmake . -DWITH_MYSQL=on -DMYSQL_INCLUDE_DIR=~/mysql-5.5/include\
-    >  -DMYSQL_LIBRARIES=~/mysql-5.5/lib/libmysqlclient.so
-    ...
-    -- Found MySQL includes: ~/mysql-5.5/include/mysql.h
-    -- Found MySQL library: ~/mysql-5.5/lib/libmysqlclient.so
-    ...
-    -- Configuring done
-    -- Generating done
-    -- Build files have been written to: ~/tarantool
-    $ make
-    ...
-    Scanning dependencies of target mysql
-    [ 79%] Building CXX object src/module/mysql/CMakeFiles/mysql.dir/mysql.cc.o
-    Linking CXX shared library libmysql.so
-    [ 79%] Built target mysql
-    ...
-    [100%] Built target man
-    $
-
-    # The MySQL module should now be in ./src/module/mysql/mysql.so.
-    # If a "make install" had been done, then mysql.so would be in a
-    # different place, for example
-    # /usr/local/lib/x86_64-linux-gnu/tarantool/box/net/mysql.so.
-    # In that case there should be additional cmake options such as
-    # -DCMAKE_INSTALL_LIBDIR and -DCMAKE_INSTALL_PREFIX.
-    # For this example we assume that "make install" is not done.
-
-    # Change directory to a directory which can be used for temporary tests.
-    # For this example we assume that the name of this directory is
-    # /home/pgulutzan/tarantool_sandbox. (Change "/home/pgulutzan" to whatever
-    # is the actual base directory for the machine that's used for this test.)
-    # Now, to help tarantool find the essential mysql.so file, execute these lines:
-    cd /home/pgulutzan/tarantool_sandbox
-    mkdir box
-    mkdir box/net
-    cp ~/tarantool/src/module/mysql/mysql.so ./box/net/mysql.so
-
-    # Start the Tarantool server. Do not use a Lua initialization file.
-
-    $ ~/tarantool/src/tarantool
-    ~/tarantool/src/tarantool: version 1.6.3-439-g7e1011b
-    type 'help' for interactive help
-    tarantool>  box.cfg{}
-    ...
-    # Enter the following lines on the prompt (again, change "/home/pgulutzan"
-    # to whatever the real directory is that contains tarantool):
-    package.path = "/home/pgulutzan/tarantool/src/module/sql/?.lua;"..package.path
-    require("sql")
-    if type(box.net.sql) ~= "table" then error("net.sql load failed") end
-    require("box.net.mysql")
-    # ... Make sure that tarantool replies "true" for both calls to "require()".
-
-    # Create a Lua function that will connect to the MySQL server,
-    # (using some factory default values for the port and user and password),
-    # retrieve one row, and display the row.
-    # For explanations of the statement types used here, read the
-    # Lua tutorial earlier in the Tarantool user manual.
-    tarantool> console = require('console'); console.delimiter('!')
-    tarantool> function mysql_select ()
-            ->   local dbh = box.net.sql.connect(
-            ->       'mysql', '127.0.0.1', 3306, 'root', '', 'test')
-            ->   local test = dbh:select('SELECT * FROM test WHERE s1 = 1')
-            ->    local row = ''
-            ->   for i, card in pairs(test) do
-            ->     row = row .. card.s2 .. ' '
-            ->     end
-            ->   return row
-            ->   end!
-    ---
-    ...
-    tarantool> console.delimiter('')!
-    tarantool>
-
-    # Execute the Lua function.
-    tarantool> mysql_select()
-    ---
-    - 'MySQL row '
-    ...
-    # Observe the result. It contains "MySQL row".
-    # So this is the row that was inserted into the MySQL database.
-    # And now it's been selected with the Tarantool client.
+    | :codenormal:`# Check that the include subdirectory exists by looking for .../include/mysql.h.`
+    | :codenormal:`# (If this fails, there's a chance that it's in .../include/mysql/mysql.h instead.)`
+    | :codenormal:`$` :codebold:`[ -f ~/mysql-5.5/include/mysql.h ] && echo "OK" || echo "Error"`
+    | :codenormal:`OK`
+    |
+    | :codenormal:`# Check that the library subdirectory exists and has the necessary .so file.`
+    | :codenormal:`$` :codebold:`[ -f ~/mysql-5.5/lib/libmysqlclient.so ] && echo "OK" || echo "Error"`
+    | :codenormal:`OK`
+    |
+    | :codenormal:`# Check that the mysql client can connect using some factory defaults:`
+    | :codenormal:`# port = 3306, user = 'root', user password = '', database = 'test'.`
+    | :codenormal:`# These can be changed, provided one uses the changed values in`
+    | :codenormal:`# all places.`
+    | :codenormal:`$` :codebold:`~/mysql-5.5/bin/mysql --port=3306 -h 127.0.0.1 --user=root --password= --database=test`
+    | :codenormal:`Welcome to the MySQL monitor.  Commands end with ; or \\g.`
+    | :codenormal:`Your MySQL connection id is 25`
+    | :codenormal:`Server version: 5.5.35 MySQL Community Server (GPL)`
+    | :codenormal:`...`
+    | :codenormal:`Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.`
+    |
+    | :codenormal:`# Insert a row in database test, and quit.`
+    | :codenormal:`mysql>` :codebold:`CREATE TABLE IF NOT EXISTS test (s1 INT, s2 VARCHAR(50));`
+    | :codenormal:`Query OK, 0 rows affected (0.13 sec)`
+    | :codenormal:`mysql>` :codebold:`INSERT INTO test.test VALUES (1,'MySQL row');`
+    | :codenormal:`Query OK, 1 row affected (0.02 sec)`
+    | :codenormal:`mysql>` :codebold:`QUIT`
+    | :codenormal:`Bye`
+    |
+    | :codenormal:`# Build the Tarantool server. Make certain that "cmake" gets the right`
+    | :codenormal:`# paths for the MySQL include directory and the MySQL libmysqlclient`
+    | :codenormal:`# library which were checked earlier.`
+    | :codenormal:`$` :codebold:`cd ~/tarantool`
+    | :codenormal:`$` :codebold:`make clean`
+    | :codenormal:`$` :codebold:`rm CMakeCache.txt`
+    | :codenormal:`$` :codebold:`cmake . -DWITH_MYSQL=on -DMYSQL_INCLUDE_DIR=~/mysql-5.5/include\\`
+    | :codenormal:`>` |nbsp| |nbsp| :codebold:`-DMYSQL_LIBRARIES=~/mysql-5.5/lib/libmysqlclient.so`
+    | :codenormal:`...`
+    | :codenormal:`-- Found MySQL includes: ~/mysql-5.5/include/mysql.h`
+    | :codenormal:`-- Found MySQL library: ~/mysql-5.5/lib/libmysqlclient.so`
+    | :codenormal:`...`
+    | :codenormal:`-- Configuring done`
+    | :codenormal:`-- Generating done`
+    | :codenormal:`-- Build files have been written to: ~/tarantool`
+    | :codenormal:`$` :codebold:`make`
+    | :codenormal:`...`
+    | :codenormal:`Scanning dependencies of target mysql`
+    | :codenormal:`[ 79%] Building CXX object src/module/mysql/CMakeFiles/mysql.dir/mysql.cc.o`
+    | :codenormal:`Linking CXX shared library libmysql.so`
+    | :codenormal:`[ 79%] Built target mysql`
+    | :codenormal:`...`
+    | :codenormal:`[100%] Built target man`
+    | :codenormal:`$`
+    |
+    | :codenormal:`# The MySQL module should now be in ./src/module/mysql/mysql.so.`
+    | :codenormal:`# If a "make install" had been done, then mysql.so would be in a`
+    | :codenormal:`# different place, for example`
+    | :codenormal:`# /usr/local/lib/x86_64-linux-gnu/tarantool/box/net/mysql.so.`
+    | :codenormal:`# In that case there should be additional cmake options such as`
+    | :codenormal:`# -DCMAKE_INSTALL_LIBDIR and -DCMAKE_INSTALL_PREFIX.`
+    | :codenormal:`# For this example we assume that "make install" is not done.`
+    |
+    | :codenormal:`# Change directory to a directory which can be used for temporary tests.`
+    | :codenormal:`# For this example we assume that the name of this directory is`
+    | :codenormal:`# /home/pgulutzan/tarantool_sandbox. (Change "/home/pgulutzan" to whatever`
+    | :codenormal:`# is the actual base directory for the machine that's used for this test.)`
+    | :codenormal:`# Now, to help tarantool find the essential mysql.so file, execute these lines:`
+    | :codebold:`cd /home/pgulutzan/tarantool_sandbox`
+    | :codebold:`mkdir box`
+    | :codebold:`mkdir box/net`
+    | :codebold:`cp ~/tarantool/src/module/mysql/mysql.so ./box/net/mysql.so`
+    |
+    | :codenormal:`# Start the Tarantool server. Do not use a Lua initialization file.`
+    |
+    | :codenormal:`$` :codebold:`~/tarantool/src/tarantool`
+    | :codenormal:`~/tarantool/src/tarantool: version 1.6.3-439-g7e1011b`
+    | :codenormal:`type 'help' for interactive help`
+    | :codenormal:`tarantool>` :codebold:`box.cfg{}`
+    | :codenormal:`...`
+    | :codenormal:`# Enter the following lines on the prompt (again, change "/home/pgulutzan"`
+    | :codenormal:`# to whatever the real directory is that contains tarantool):`
+    | :codenormal:`package.path = "/home/pgulutzan/tarantool/src/module/sql/?.lua;"..package.path`
+    | :codenormal:`require("sql")`
+    | :codenormal:`if type(box.net.sql) ~= "table" then error("net.sql load failed") end`
+    | :codenormal:`require("box.net.mysql")`
+    | :codenormal:`# ... Make sure that tarantool replies "true" for both calls to "require()".`
+    |
+    | :codenormal:`# Create a Lua function that will connect to the MySQL server,`
+    | :codenormal:`# (using some factory default values for the port and user and password),`
+    | :codenormal:`# retrieve one row, and display the row.`
+    | :codenormal:`# For explanations of the statement types used here, read the`
+    | :codenormal:`# Lua tutorial earlier in the Tarantool user manual.`
+    | :codenormal:`tarantool>` :codebold:`console = require('console'); console.delimiter('!')`
+    | :codenormal:`tarantool>` :codebold:`function mysql_select ()`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`local dbh = box.net.sql.connect(`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| :codebold:`'mysql', '127.0.0.1', 3306, 'root', '', 'test')`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`local test = dbh:select('SELECT * FROM test WHERE s1 = 1')`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`local row = ''`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`for i, card in pairs(test) do`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| :codebold:`row = row .. card.s2 .. ' '`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| :codebold:`end`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`return row`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| :codebold:`end!`
+    | :codenormal:`---`
+    | :codenormal:`...`
+    | :codenormal:`tarantool>` :codebold:`console.delimiter('')!`
+    | :codenormal:`tarantool>`
+    |
+    | :codenormal:`# Execute the Lua function.`
+    | :codenormal:`tarantool>` :codebold:`mysql_select()`
+    | :codenormal:`---`
+    | :codenormal:`- 'MySQL row '`
+    | :codenormal:`...`
+    | :codenormal:`# Observe the result. It contains "MySQL row".`
+    | :codenormal:`# So this is the row that was inserted into the MySQL database.`
+    | :codenormal:`# And now it's been selected with the Tarantool client.`
 
 ===========================================================
                   PostgreSQL Example
@@ -175,9 +174,7 @@ This example assumes that a recent version of PostgreSQL has been installed.
 The PostgreSQL library and include files are also necessary. On Ubuntu they
 can be installed with
 
-.. code-block:: bash
-
-    $ sudo apt-get install libpq-dev
+    | :codebold:`$ sudo apt-get install libpq-dev`
 
 If that works, then cmake will find the necessary files without requiring any
 special user input. However, because not all platforms are alike, for this
@@ -189,108 +186,106 @@ The example was run on a Linux machine where the base directory had a copy of
 the Tarantool source on ~/tarantool, and a copy of PostgreSQL on /usr. The
 postgres server is already running on the local host 127.0.0.1.
 
-::
-
-    # Check that the include subdirectory exists
-    # by looking for /usr/include/postgresql/libpq-fe-h.
-    $ [ -f /usr/include/postgresql/libpq-fe.h ] && echo "OK" || echo "Error"
-    OK
-
-    # Check that the library subdirectory exists and has the necessary .so file.
-    $ [ -f /usr/lib/libpq.so ] && echo "OK" || echo "Error"
-    OK
-
-    # Check that the psql client can connect using some factory defaults:
-    # port = 5432, user = 'postgres', user password = 'postgres', database = 'postgres'.
-    # These can be changed, provided one changes them in all places.
-    # Insert a row in database postgres, and quit.
-    $ psql -h 127.0.0.1 -p 5432 -U postgres -d postgres
-    Password for user postgres:
-    psql (9.3.0, server 9.3.2)
-    SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
-    Type "help" for help.
-
-    postgres=# CREATE TABLE test (s1 INT, s2 VARCHAR(50));
-    CREATE TABLE
-    postgres=# INSERT INTO test VALUES (1,'PostgreSQL row');
-    INSERT 0 1
-    postgres=# \q
-    $
-
-    # Build the Tarantool server. Make certain that "cmake" gets the right
-    # paths for the PostgreSQL include directory and the PostgreSQL libpq
-    # library which were checked earlier.
-    $ cd ~/tarantool
-    $ make clean
-    $ rm CMakeCache.txt
-    $ cmake . -DWITH_POSTGRESQL=on -DPostgreSQL_LIBRARY=/usr/lib/libpq.so\
-    >  -DPostgreSQL_INCLUDE_DIR=/usr/include/postgresql
-    ...
-    -- Found PostgreSQL: /usr/lib/libpq.so (found version "9.3.2")
-    ...
-    -- Configuring done
-    -- Generating done
-    -- Build files have been written to: ~/tarantool
-    $ make
-    ...
-    [ 79%] Building CXX object src/plugin/pg/CMakeFiles/pg.dir/pg.cc.o
-    Linking CXX shared library libpg.so
-    [ 79%] Built target pg
-    ...
-    [100%] Built target man
-    $
-
-    # Change directory to a directory which can be used for temporary tests.
-    # For this example we assume that the name of this directory is
-    # /home/pgulutzan/tarantool_sandbox. (Change "/home/pgulutzan" to whatever
-    # is the actual base directory for the machine that's used for this test.)
-    # Now, to help tarantool find the essential mysql.so file, execute these lines:
-    cd /home/pgulutzan/tarantool_sandbox
-    mkdir box
-    mkdir box/net
-    cp ~/tarantool/src/module/pg/pg.so ./box/net/pg.so
-
-    # Start the Tarantool server. Do not use a Lua initialization file.
-
-    $ ~/tarantool/src/tarantool
-    ~/tarantool/src/tarantool: version 1.6.3-439-g7e1011b
-    type 'help' for interactive help
-    tarantool>   box.cfg{}
-
-    # Enter the following lines on the prompt (again, change "/home/pgulutzan"
-    # to whatever the real directory is that contains tarantool):
-    package.path = "/home/pgulutzan/tarantool/src/module/sql/?.lua;"..package.path
-    require("sql")
-    if type(box.net.sql) ~= "table" then error("net.sql load failed") end
-    require("box.net.pg")
-    # ... Make sure that tarantool replies "true" for the calls to "require()".
-
-    # Create a Lua function that will connect to the PostgreSQL server,
-    # retrieve one row, and display the row.
-    # For explanations of the statement types used here, read the
-    # Lua tutorial in the Tarantool user manual.
-    tarantool> console = require('console'); console.delimiter('!')
-    tarantool> function postgresql_select ()
-            ->   local dbh = box.net.sql.connect(
-            ->       'pg', '127.0.0.1', 5432, 'postgres', 'postgres', 'postgres')
-            ->   local test = dbh:select('SELECT * FROM test WHERE s1 = 1')
-            ->   local row = ''
-            ->   for i, card in pairs(test) do
-            ->     row = row .. card.s2 .. ' '
-            ->     end
-             >   return row
-            ->   end!
-    ---
-    ...
-    tarantool> console.delimiter('')!
-    tarantool>
-
-    # Execute the Lua function.
-    tarantool> postgresql_select()
-    ---
-    - 'PostgreSQL row '
-    ...
-
-    # Observe the result. It contains "PostgreSQL row".
-    # So this is the row that was inserted into the PostgreSQL database.
-    # And now it's been selected with the Tarantool client.
+    | :codenormal:`# Check that the include subdirectory exists`
+    | :codenormal:`# by looking for /usr/include/postgresql/libpq-fe-h.`
+    | :codenormal:`$` :codebold:`[ -f /usr/include/postgresql/libpq-fe.h ] && echo "OK" || echo "Error"`
+    | :codenormal:`OK`
+    |
+    | :codenormal:`# Check that the library subdirectory exists and has the necessary .so file.`
+    | :codenormal:`$` :codebold:`[ -f /usr/lib/libpq.so ] && echo "OK" || echo "Error"`
+    | :codenormal:`OK`
+    |
+    | :codenormal:`# Check that the psql client can connect using some factory defaults:`
+    | :codenormal:`# port = 5432, user = 'postgres', user password = 'postgres', database = 'postgres'.`
+    | :codenormal:`# These can be changed, provided one changes them in all places.`
+    | :codenormal:`# Insert a row in database postgres, and quit.`
+    | :codenormal:`$` :codebold:`psql -h 127.0.0.1 -p 5432 -U postgres -d postgres`
+    | :codenormal:`Password for user postgres:`
+    | :codenormal:`psql (9.3.0, server 9.3.2)`
+    | :codenormal:`SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)`
+    | :codenormal:`Type "help" for help.`
+    |
+    | :codenormal:`postgres=#` :codebold:`CREATE TABLE test (s1 INT, s2 VARCHAR(50));`
+    | :codenormal:`CREATE TABLE`
+    | :codenormal:`postgres=#` :codebold:`INSERT INTO test VALUES (1,'PostgreSQL row');`
+    | :codenormal:`INSERT 0 1`
+    | :codenormal:`postgres=#` :codebold:`\\q`
+    | :codenormal:`$`
+    |
+    | :codenormal:`# Build the Tarantool server. Make certain that "cmake" gets the right`
+    | :codenormal:`# paths for the PostgreSQL include directory and the PostgreSQL libpq`
+    | :codenormal:`# library which were checked earlier.`
+    | :codenormal:`$` :codebold:`cd ~/tarantool`
+    | :codenormal:`$` :codebold:`make clean`
+    | :codenormal:`$` :codebold:`rm CMakeCache.txt`
+    | :codenormal:`$` :codebold:`cmake . -DWITH_POSTGRESQL=on -DPostgreSQL_LIBRARY=/usr/lib/libpq.so\\`
+    | :codenormal:`>` |nbsp| :codebold:`-DPostgreSQL_INCLUDE_DIR=/usr/include/postgresql`
+    | :codenormal:`...`
+    | :codenormal:`-- Found PostgreSQL: /usr/lib/libpq.so (found version "9.3.2")`
+    | :codenormal:`...`
+    | :codenormal:`-- Configuring done`
+    | :codenormal:`-- Generating done`
+    | :codenormal:`-- Build files have been written to: ~/tarantool`
+    | :codenormal:`$` :codebold:`make`
+    | :codenormal:`...`
+    | :codenormal:`[ 79%] Building CXX object src/plugin/pg/CMakeFiles/pg.dir/pg.cc.o`
+    | :codenormal:`Linking CXX shared library libpg.so`
+    | :codenormal:`[ 79%] Built target pg`
+    | :codenormal:`...`
+    | :codenormal:`[100%] Built target man`
+    | :codenormal:`$`
+    |
+    | :codenormal:`# Change directory to a directory which can be used for temporary tests.`
+    | :codenormal:`# For this example we assume that the name of this directory is`
+    | :codenormal:`# /home/pgulutzan/tarantool_sandbox. (Change "/home/pgulutzan" to whatever`
+    | :codenormal:`# is the actual base directory for the machine that's used for this test.)`
+    | :codenormal:`# Now, to help tarantool find the essential mysql.so file, execute these lines:`
+    | :codebold:`cd /home/pgulutzan/tarantool_sandbox`
+    | :codebold:`mkdir box`
+    | :codebold:`mkdir box/net`
+    | :codebold:`cp ~/tarantool/src/module/pg/pg.so ./box/net/pg.so`
+    |
+    | :codenormal:`# Start the Tarantool server. Do not use a Lua initialization file.`
+    |
+    | :codenormal:`$` :codebold:`~/tarantool/src/tarantool`
+    | :codenormal:`~/tarantool/src/tarantool: version 1.6.3-439-g7e1011b`
+    | :codenormal:`type 'help' for interactive help`
+    | :codenormal:`tarantool>` :codebold:`box.cfg{}`
+    |
+    | :codenormal:`# Enter the following lines on the prompt (again, change "/home/pgulutzan"`
+    | :codenormal:`# to whatever the real directory is that contains tarantool):`
+    | :codenormal:`package.path = "/home/pgulutzan/tarantool/src/module/sql/?.lua;"..package.path`
+    | :codenormal:`require("sql")`
+    | :codenormal:`if type(box.net.sql) ~= "table" then error("net.sql load failed") end`
+    | :codenormal:`require("box.net.pg")`
+    | :codenormal:`# ... Make sure that tarantool replies "true" for the calls to "require()".`
+    |
+    | :codenormal:`# Create a Lua function that will connect to the PostgreSQL server,`
+    | :codenormal:`# retrieve one row, and display the row.`
+    | :codenormal:`# For explanations of the statement types used here, read the`
+    | :codenormal:`# Lua tutorial in the Tarantool user manual.`
+    | :codenormal:`tarantool>` :codebold:`console = require('console'); console.delimiter('!')`
+    | :codenormal:`tarantool>` :codebold:`function postgresql_select ()`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`local dbh = box.net.sql.connect(`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| |nbsp| :codebold:`'pg', '127.0.0.1', 5432, 'postgres', 'postgres', 'postgres')`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`local test = dbh:select('SELECT * FROM test WHERE s1 = 1')`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`local row = ''`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`for i, card in pairs(test) do`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| |nbsp| :codebold:`row = row .. card.s2 .. ' '`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| |nbsp| |nbsp| :codebold:`end`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`return row`
+    | |nbsp| |nbsp| |nbsp| |nbsp| |nbsp| :codenormal:`->` |nbsp| |nbsp| :codebold:`end!`
+    | :codenormal:`---`
+    | :codenormal:`...`
+    | :codenormal:`tarantool>` :codebold:`console.delimiter('')!`
+    | :codenormal:`tarantool>`
+    |
+    | :codenormal:`# Execute the Lua function.`
+    | :codenormal:`tarantool>` :codebold:`postgresql_select()`
+    | :codenormal:`---`
+    | :codenormal:`- 'PostgreSQL row '`
+    | :codenormal:`...`
+    |
+    | :codenormal:`# Observe the result. It contains "PostgreSQL row".`
+    | :codenormal:`# So this is the row that was inserted into the PostgreSQL database.`
+    | :codenormal:`# And now it's been selected with the Tarantool client.`
diff --git a/doc/sphinx/book/box/box_introspection.rst b/doc/sphinx/book/box/box_introspection.rst
index 5c07ac15a993a6b97dc23245da007e1169d8be5c..343a3ee17afc7d550571dc2ab182ae83d086da74 100644
--- a/doc/sphinx/book/box/box_introspection.rst
+++ b/doc/sphinx/book/box/box_introspection.rst
@@ -67,7 +67,6 @@ you can use **date -d@** 1306964594.980.
     | |nbsp| :codenormal:`id: 1`
     | |nbsp| :codenormal:`pid: 32561`
     | |nbsp| :codenormal:`version: 1.6.4-411-gcff798b`
-    | |nbsp| :codenormal:`snapshot_pid: 0`
     | |nbsp| :codenormal:`status: running`
     | |nbsp| :codenormal:`vclock: {1: 158}`
     | |nbsp| :codenormal:`replication:`
@@ -110,10 +109,6 @@ you can use **date -d@** 1306964594.980.
     | :codenormal:`---`
     | :codenormal:`- 1306964594.980`
     | :codenormal:`...`
-    | :codenormal:`tarantool>` :codebold:`box.info.snapshot_pid`
-    | :codenormal:`---`
-    | :codenormal:`- 0`
-    | :codenormal:`...`
 
 =====================================================================
                          Package `box.slab`
diff --git a/doc/sphinx/book/box/net_box.rst b/doc/sphinx/book/box/net_box.rst
index b1d4873d6f6c9bccae57cf923d45ca9c73df8e6d..78176aefdec49005629c53115d4f9d0cd38c15eb 100644
--- a/doc/sphinx/book/box/net_box.rst
+++ b/doc/sphinx/book/box/net_box.rst
@@ -1,3 +1,5 @@
+.. _package_net_box:
+
 -----------------------------------------------------------------------------------------
                             Package `net.box` -- working with networked Tarantool peers
 -----------------------------------------------------------------------------------------
diff --git a/doc/sphinx/book/box/triggers.rst b/doc/sphinx/book/box/triggers.rst
index 8ccc3cba121a7f52561ed70283e83ffa1f40220d..d00b628b8869ae6b5cec13128554a2e12936b0e2 100644
--- a/doc/sphinx/book/box/triggers.rst
+++ b/doc/sphinx/book/box/triggers.rst
@@ -5,7 +5,7 @@
 -------------------------------------------------------------------------------
 
 Triggers, also known as callbacks, are functions which the server executes when
-certain events happen. Currently the two types of triggers are `connection triggers`_,
+certain events happen. Currently the main types of triggers are `connection triggers`_,
 which are executed when a session begins or ends, and `replace triggers`_ which are
 for database events,
 
@@ -13,12 +13,12 @@ All triggers have the following characteristics.
 
 * They associate a `function` with an `event`. The request to "define a trigger"
   consists of passing the name of the trigger's function to one of the
-  "on_`event-name` ..." functions: ``on_connect()``, ``on_disconnect()``,
-  or ``on_replace()``.
+  ":samp:`on_{event-name}()`" functions: :code:`on_connect()`, :code:`on_auth()`,
+  :code:`on_disconnect()`, or :code:`on_replace()`.
 * They are `defined by any user`. There are no privilege requirements for defining
   triggers.
 * They are called `after` the event. They are not called if the event ends
-  prematurely due to an error.
+  prematurely due to an error. (Exception: :code:`on_auth()` is called before the event.)
 * They are in `server memory`. They are not stored in the database. Triggers
   disappear when the server is shut down. If there is a requirement to make
   them permanent, then the function definitions and trigger settings should
@@ -109,6 +109,34 @@ Here is what might appear in the log file in a typical installation:
     2014-12-15 13:22:19.289 [11360] main/103/iproto I>
         Disconnection. user=guest id=3
 
+===========================================================
+                    Authentication triggers
+===========================================================
+
+.. function:: box.session.on_auth(trigger-function [, old-trigger-function-name])
+
+    Define a trigger for execution during authentication.
+    The on_auth trigger function is invoked after the on_connect trigger function,
+    if and only if the connection has succeeded so far.
+    For this purpose, connection and authentication are considered to be separate steps.
+
+    Unlike other trigger types, on_auth trigger functions are invoked `before`
+    the event. Therefore a trigger function like ":code:`function auth_function () v = box.session.user(); end`"
+    will set :code:`v` to "guest", the user name before the authentication is done.
+    To get the user name after the authentication is done, use the special syntax:
+    ":code:`function auth_function (user_name) v = user_name; end`"
+
+    If the trigger fails by raising an
+    error, the error is sent to the client and the connection is closed.
+
+    :param function trigger-function: function which will become the trigger function
+    :param function old-trigger-function: existing trigger function which will be replaced by trigger-function
+    :return: nil
+
+    If the parameters are (nil, old-trigger-function), then the old trigger is deleted.
+
+    Example: :codenormal:`function f () x = x + 1 end; box.session.on_auth(f)`
+
 
 ===========================================================
                     Replace triggers
diff --git a/doc/sphinx/book/user_guide_getting_started.rst b/doc/sphinx/book/user_guide_getting_started.rst
new file mode 100644
index 0000000000000000000000000000000000000000..cc9e12261728fccd22be3f08e4048408a6e25134
--- /dev/null
+++ b/doc/sphinx/book/user_guide_getting_started.rst
@@ -0,0 +1,177 @@
+-------------------------------------------------------------------------------
+                        Getting started
+-------------------------------------------------------------------------------
+
+
+If the installation has already been done, then you should try it out. So we've
+provided some instructions that you can use to make a temporary “sandbox”. In a
+few minutes you can start the server and type in some database-manipulation
+statements. The section about sandbox is “`Starting Tarantool and making your first database`_”.
+
+.. _first database:
+
+=====================================================================
+        Starting Tarantool and making your first database
+=====================================================================
+
+Here is how to create a simple test database after installing.
+
+Create a new directory. It's just for tests, you can delete it when the tests are over.
+
+ | :codebold:`mkdir` :codebolditalic:`~/tarantool_sandbox`
+ | :codebold:`cd` :codebolditalic:`~/tarantool_sandbox`
+
+Start the server. The server name is tarantool.
+
+ | :codebold:`#if you downloaded a binary with apt-get or yum, say this:`
+ | :codebold:`/usr/bin/tarantool`
+ | :codebold:`#if you downloaded and untarred a binary tarball to ~/tarantool, say this:`
+ | :codebold:`~/tarantool/bin/tarantool`
+ | :codebold:`#if you built from a source download, say this:`
+ | :codebold:`~/tarantool/src/tarantool`
+
+The server starts in interactive mode and outputs a command prompt.
+To turn on the database, :mod:`configure <box.cfg>` it:
+:codenormal:`tarantool>` :codebold:`box.cfg{listen=3301}`
+(this minimal example is sufficient).
+
+If all goes well, you will see the server displaying progress as it
+initializes, something like this:
+
+ | :codenormal:`tarantool> box.cfg{listen=3301}`
+ | :codenormal:`2014-08-07 09:41:41.077 ... version 1.6.3-439-g7e1011b`
+ | :codenormal:`2014-08-07 09:41:41.077 ... log level 5`
+ | :codenormal:`2014-08-07 09:41:41.078 ... mapping 1073741824 bytes for a shared arena...`
+ | :codenormal:`2014-08-07 09:41:41.079 ... initialized`
+ | :codenormal:`2014-08-07 09:41:41.081 ... initializing an empty data directory`
+ | :codenormal:`2014-08-07 09:41:41.095 ... creating './00000000000000000000.snap.inprogress'`
+ | :codenormal:`2014-08-07 09:41:41.095 ... saving snapshot './00000000000000000000.snap.inprogress'`
+ | :codenormal:`2014-08-07 09:41:41.127 ... done`
+ | :codenormal:`2014-08-07 09:41:41.128 ... primary: bound to 0.0.0.0:3301`
+ | :codenormal:`2014-08-07 09:41:41.128 ... ready to accept requests`
+
+Now that the server is up, you could start up a different shell
+and connect to its primary port with |br|
+:codebold:`telnet 0 3301` |br|
+but for example purposes it is simpler to just leave the server
+running in "interactive mode". On production machines the
+interactive mode is just for administrators, but because it's
+convenient for learning it will be used for most examples in
+this manual. Tarantool is waiting for the user to type instructions.
+
+To create the first space and the first :ref:`index <box.index>`, try this:
+
+ | :codenormal:`tarantool>` :codebold:`s = box.schema.space.create('tester')`
+ | :codenormal:`tarantool>` :codebold:`i = s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}})`
+
+To insert three “tuples” (our name for “records”) into the first “space” of the database try this:
+
+ | :codenormal:`tarantool>` :codebold:`t = s:insert({1})`
+ | :codenormal:`tarantool>` :codebold:`t = s:insert({2, 'Music'})`
+ | :codenormal:`tarantool>` :codebold:`t = s:insert({3, 'Length', 93})`
+
+To select a tuple from the first space of the database, using the first defined key, try this:
+
+ | :codenormal:`tarantool>` :codebold:`s:select{3}`
+
+Your terminal screen should now look like this:
+
+ | :codenormal:`tarantool> s = box.schema.space.create('tester')`
+ | :codenormal:`2014-06-10 12:04:18.158 ... creating './00000000000000000002.xlog.inprogress'`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ | :codenormal:`tarantool> s:create_index('primary', {type = 'hash', parts = {1, 'NUM'}})`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ | :codenormal:`tarantool> t = s:insert{1}`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ | :codenormal:`tarantool> t = s:insert{2, 'Music'}`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ | :codenormal:`tarantool> t = s:insert{3, 'Length', 93}`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ | :codenormal:`tarantool> s:select{3}`
+ | :codenormal:`---`
+ | :codenormal:`- - [3, 'Length', 93]`
+ | :codenormal:`...`
+ |
+ | :codenormal:`tarantool>`
+
+Now, to prepare for the example in the next section, try this:
+
+ | :codenormal:`tarantool>` :codebold:`box.schema.user.grant('guest','read,write,execute','universe')`
+
+.. _tarantool.org/dist/stable: http://tarantool.org/dist/stable
+.. _tarantool.org/dist/master: http://tarantool.org/dist/master
+
+
+=====================================================================
+        Starting another Tarantool instance and connecting remotely
+=====================================================================
+
+In the previous section the first request was with "box.cfg{listen=3301}".
+The "listen" value can be any form of URI (uniform resource identifier);
+in this case it's just a local port: port 3301.
+It's possible to send requests to the listen URI via (a) telnet,
+(b) a connector (which will be the subject of Chapter 8),
+or (c) another instance of Tarantool. Let's try (c).
+
+Switch to another terminal.
+On Linux, for example, this means starting another instance of a Bash shell.
+There is no need to use cd to switch to the :codenormal:`~/tarantool_sandbox` directory.
+
+Start the second instance of Tarantool. The server name is tarantool.
+
+  | :codebold:`#if you downloaded a binary with apt-get or yum, say this:`
+  | :codebold:`/usr/bin/tarantool`
+  | :codebold:`#if you downloaded and untarred a binary tarball to ~/tarantool, say this:`
+  | :codebold:`~/tarantool/bin/tarantool`
+  | :codebold:`#if you built from a source download, say this:`
+  | :codebold:`~/tarantool/src/tarantool`
+
+Try these requests:
+
+  | :codebold:`console = require('console')`
+  | :codebold:`console.connect('localhost:3301')`
+  | :codebold:`box.space.tester:select{2}`
+
+The requests are saying "use the :ref:`console package <package-console>`
+to connect to the Tarantool server that's listening
+on localhost:3301, send a request to that server,
+and display the result." The result in this case is
+one of the tuples that was inserted earlier.
+Your terminal screen should now look like this:
+
+ | :codenormal:`...`
+ |
+ | :codenormal:`tarantool> console = require('console')`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ |
+ | :codenormal:`tarantool> console.connect('localhost:3301')`
+ | :codenormal:`2014-08-31 12:46:54.650 [32628] main/101/interactive I> connected to localhost:3301`
+ | :codenormal:`---`
+ | :codenormal:`...`
+ |
+ | :codenormal:`localhost:3301> box.space.tester:select{2}`
+ | :codenormal:`---`
+ | :codenormal:`- - [2, 'Music']`
+ | :codenormal:`...`
+ |
+ | :codenormal:`localhost:3301>`
+
+You can repeat box.space...:insert{} and box.space...:select{}
+indefinitely, on either Tarantool instance.
+When the testing is over: To drop the space: :codenormal:`s:drop()`.
+To stop tarantool: :codenormal:`Ctrl+C`. To stop tarantool (an alternative):
+:codenormal:`os.exit()`. To stop tarantool (from another terminal):
+:codebold:`sudo pkill -f` :codebolditalic:`tarantool`.
+To destroy the test: :codebold:`rm -r` :codebolditalic:`~/tarantool_sandbox`.
+
+To review ... If you followed all the instructions
+in this chapter, then so far you have: installed Tarantool
+from either a binary or a source repository,
+started up the Tarantool server, inserted and selected tuples.
+
diff --git a/doc/sphinx/dev_guide/building_from_source.rst b/doc/sphinx/dev_guide/building_from_source.rst
index 556dadce7c011f76429abdf3d8a9d689276dd136..b5e8f612c862e4c56ca2180f69992c81f8199196 100644
--- a/doc/sphinx/dev_guide/building_from_source.rst
+++ b/doc/sphinx/dev_guide/building_from_source.rst
@@ -16,7 +16,7 @@ explain what the steps are, then on the Internet you can look at some example sc
      end in `-src.tar.gz`), the latest complete source downloads are on github.com, and
      from github one gets with git.
 
-   * A C/C++ compiler. |br| Ordinarily the compiler is ``GCC`` version 4.6 or later, on
+   * A C/C++ compiler. |br| Ordinarily the compiler is `gcc` and ``g++`` version 4.6 or later, on
      Mac OS X it should be ``Clang`` version 3.2 or later.
 
    * A program for managing the build process. |br| This is always ``CMake``
@@ -25,33 +25,20 @@ explain what the steps are, then on the Internet you can look at some example sc
    Here are names of tools and libraries which may have to be installed in advance,
    using ``sudo apt-get`` (for Ubuntu), ``sudo yum install`` (for CentOS), or the
    equivalent on other platforms. Different platforms may use slightly different
-   names. Do not worry about the ones marked `optional, for build with -DENABLE_DOC`
-   unless you intend to work on the documentation.
+   names. Do not worry about the ones marked `optional, only in Mac OS scripts`
+   unless your platform is Mac OS.
 
-   * **binutils-dev** or **binutils-devel**   # contains GNU BFD for printing stack traces
-   * **gcc or clang**                         # see above
+   * **gcc and g++, or clang**                # see above
    * **git**                                  # see above
    * **cmake**                                # see above
-   * **libreadline-dev**                      # for interactive mode
-   * **libncurses5-dev** or **ncurses-devel** # see above
-   * **xsltproc**                             # optional, for build with -DENABLE_DOC
-   * **lynx**                                 # optional, for build with -DENABLE_DOC
-   * **jing**                                 # optional, for build with -DENABLE_DOC
-   * **libxml2-utils**                        # optional, for build with -DENABLE_DOC
-   * **docbook5-xml**                         # optional, for build with -DENABLE_DOC
-   * **docbook-xsl-ns**                       # optional, for build with -DENABLE_DOC
-   * **w3c-sgml-lib**                         # optional, for build with -DENABLE_DOC
-   * **libsaxon-java**                        # optional, for build with -DENABLE_DOC
-   * **libxml-commons-resolver1.1-java**      # optional, for build with -DENABLE_DOC
-   * **libxerces2-java**                      # optional, for build with -DENABLE_DOC
-   * **libxslthl-java**                       # optional, for build with -DENABLE_DOC
-   * **autoconf**                             # optional, appears only in Mac OS scripts
-   * **zlib1g** or **zlib**                   # optional, appears only in Mac OS scripts
+   * **libreadline-dev or libreadline6-dev**  # for interactive mode
+   * **autoconf**                             # optional, only in Mac OS scripts
+   * **zlib1g** or **zlib**                   # optional, only in Mac OS scripts
 
 2. Set up python modules for running the test suite or creating documentation.
    This step is optional. Python modules are not necessary for building Tarantool
-   itself, unless one intends to use the ``-DENABLE_DOC`` option in step 6 or the
-   "Run the test suite" option in step 8. Say:
+   itself, unless one intends to use the ``-DENABLE_DOC`` option in step 5 or the
+   "Run the test suite" option in step 7. Say:
 
    .. code-block:: bash
 
@@ -64,19 +51,21 @@ explain what the steps are, then on the Internet you can look at some example sc
 
    .. code-block:: bash
 
+     # For both test suite and documentation
+     sudo apt-get install python-pip python-dev python-yaml
      # For test suite
-     sudo apt-get install python-daemon python-yaml python-argparse
+     sudo apt-get install python-daemon
      # For documentation
-     sudo apt-get install python-jinja2 python-markdown
-
+     sudo apt-get install python-sphinx python-pelican python-beautifulsoup
 
    On CentOS too you can get modules from the repository:
 
    .. code-block:: bash
 
-     sudo yum install python26 python26-PyYAML python26-argparse
+     sudo yum install python26 python26-PyYAML etc.
 
-   But in general it is best to set up the modules by getting a tarball and
+   If modules are not available on a repository,
+   it is best to set up the modules by getting a tarball and
    doing the setup with ``python setup.py``, thus:
 
    .. code-block:: bash
@@ -100,20 +89,6 @@ explain what the steps are, then on the Internet you can look at some example sc
      cd python-daemon-1.5.5
      sudo python setup.py install
 
-     # python module for text-to-html conversion (markdown):
-     # For documentation: (If wget fails, check the
-     # http://pypi.python.org/pypi/Markdown/
-     # to see what the current version is.)
-     cd ~
-     wget https://pypi.python.org/packages/source/M/Markdown/Markdown-2.3.1.tar.gz
-     tar -xzvf Markdown-2.3.1.tar.gz
-     cd Markdown-2.3.1
-     sudo python setup.py install
-
-     # python module which includes Jinja2 template engine:
-     # For documentation:
-     sudo pip install pelican
-
      # python module for HTML scraping: For documentation:
      cd ~
      wget http://www.crummy.com/software/BeautifulSoup/bs3/download//3.x/BeautifulSoup-3.2.1.tar.gz
@@ -121,7 +96,26 @@ explain what the steps are, then on the Internet you can look at some example sc
      cd BeautifulSoup-3.2.1
      sudo python setup.py install
 
-5. Use ``git`` again so that third-party contributions will be seen as well.
+   Finally, use Python :code:`pip` to bring in Python packages
+   that may not be up-to-date in the distro repositories.
+
+   .. code-block:: bash
+
+     # For test suite
+     pip install tarantool\>0.4 --user
+     # For documentation
+     sudo pip install pelican
+     sudo pip install -U sphinx
+
+3. Use :code:`git` to download the latest source code from the
+   Tarantool 1.6 master branch on github.com.
+
+   .. code-block:: bash
+
+     cd ~
+     git clone https://github.com/tarantool/tarantool.git ~/tarantool
+
+4. Use ``git`` again so that third-party contributions will be seen as well.
    This step is only necessary once, the first time you do a download. There
    is an alternative -- say ``git clone --recursive`` in step 3 -- but we
    prefer this method because it works with older versions of ``git``.
@@ -136,7 +130,7 @@ explain what the steps are, then on the Internet you can look at some example sc
    On rare occasions, the submodules will need to be updated again with the
    command: ``git submodule update --init --recursive``.
 
-6. Use CMake to initiate the build.
+5. Use CMake to initiate the build.
 
    .. code-block: bash
 
@@ -145,19 +139,27 @@ explain what the steps are, then on the Internet you can look at some example sc
      rm CMakeCache.txt  # unnecessary, added for good luck
      cmake .            # Start build with build type=Debug, no doc
 
-   The option for specifying build type is ``-DCMAKE_BUILD_TYPE=<type>`` where
-   ``type = <None | Debug | Release | RelWithDebInfo | MinSizeRel>`` and a
+   On some platforms it may be necessary to specify the C and C++ versions,
+   for example
+
+   .. code-block: bash
+
+     CC=gcc-4.8 CXX=g++-4.8 cmake .
+
+   The option for specifying build type is :samp:`-DCMAKE_BUILD_TYPE={type}` where
+   :samp:`{type} = None | Debug | Release | RelWithDebInfo | MinSizeRel` and a
    reasonable choice for production is ``-DCMAKE_BUILD_TYPE=RelWithDebInfo``
    (``Debug`` is used only by project maintainers and ``Release`` is used only
    when the highest performance is required).
 
-   The option for asking to build documentation is ``-DENABLE_DOC=<true|false>``
-   and the assumption is that only a minority will need to rebuild the
-   documentation (such as what you're reading now), so details about
-   documentation are in the developer manual, and the reasonable choice
+   The option for asking to build documentation is ``-DENABLE_DOC=true|false`,
+   which outputs HTML documentation (such as what you're reading now) to the
+   subdirectory doc/www/output/doc. Tarantool uses the Sphinx simplified markup system.
+   Since most users do not need to rebuild the documentation,
+   the reasonable option
    is ``-DENABLE_DOC=false`` or just don't use the ``-DENABLE_DOC`` clause at all.
 
-7. Use make to complete the build.
+6. Use make to complete the build.
 
    .. code-block:: bash
 
@@ -165,7 +167,7 @@ explain what the steps are, then on the Internet you can look at some example sc
 
    It's possible to say ``make install`` too, but that's not generally done.
 
-8. Run the test suite. This step is optional. |br| Tarantool's developers always
+7. Run the test suite. This step is optional. |br| Tarantool's developers always
    run the test suite before they publish new versions. You should run the test
    suite too, if you make any changes in the code. Assuming you downloaded to
    ``~/tarantool``, the principal steps are:
@@ -208,10 +210,9 @@ explain what the steps are, then on the Internet you can look at some example sc
      rmdir ~/tarantool/bin
 
 
-9. Make an rpm. |br| This step is optional. It's only for people who want to
+8. Make an rpm. |br| This step is optional. It's only for people who want to
    redistribute Tarantool. Package maintainers who want to build with rpmbuild
-   should consult the
-   :doc:`Tarantool Developer Guide <index>`
+   should consult the rpm-build instructions for the appropriate platform.
 
 This is the end of the list of steps to take for source downloads.
 
@@ -229,49 +230,5 @@ For your added convenience, github.com has README files with example scripts:
 These example scripts assume that the intent is to download from the master
 branch, build the server (but not the documentation), and run tests after build.
 
-To build with SUSE 13.1, the steps are as described above, except that the
-appropriate YaST2 package names are: binutils-devel, cmake, ncurses-devel,
-lynx, jing, libxml2-devel, docbook_5, saxon, libxslt-devel. |br|
 The python connector can be installed with ``sudo easy_install pip`` and ``sudo pip install tarantool``.
 
-===========================================================
-                How to build the XML manual
-===========================================================
-
-To build XML manual, you'll need:
-
-* xsltproc
-* docbook5-xml
-* docbook-xsl-ns
-* w3c-sgml-lib
-* libsaxon-java (for saxon processing)
-* libxml-commons-resolver1.1-java
-* libxml2-utils
-* libxerces2-java
-* libxslthl-java
-* lynx
-* jing
-
-When all pre-requisites are met, you should run:
-
-.. code-block:: bash
-
-    $ cmake . -DENABLE_DOC=YES
-
-to enable documentation builder.
-
-If you want to make tarantool user guide, you should run the
-following command from tarantool root directory:
-
-.. code-block:: bash
-
-    $ make html
-
-or
-
-.. code-block:: bash
-
-    $ cd doc/user
-    $ make
-
-The html version of the user guide will be generated in doc/www/content/doc
diff --git a/doc/sphinx/getting_started.rst b/doc/sphinx/getting_started.rst
index 736e7696cc0cf99f6ff863bf8bf2a85687848591..e36f84a6353fd66dfc16c137de8c35e77ee48850 100644
--- a/doc/sphinx/getting_started.rst
+++ b/doc/sphinx/getting_started.rst
@@ -197,7 +197,7 @@ and make install.
 
 .. _tarantool.org/dist/master/tarantool.rb: http://tarantool.org/dist/master/tarantool.rb
 
-.. _first database:
+.. _dup first database:
 
 =====================================================================
         Starting Tarantool and making your first database
diff --git a/doc/sphinx/reference/expirationd.rst b/doc/sphinx/reference/expirationd.rst
index ed7fde470557e01cbe9375b3c3418c5cd9d97f34..ac40eaf86f0784fa27dc5397663ff1ad3d2e1e5f 100644
--- a/doc/sphinx/reference/expirationd.rst
+++ b/doc/sphinx/reference/expirationd.rst
@@ -68,7 +68,7 @@ For those who like to see things run, here are the exact steps to get
 expirationd through the test.
 
 1. Get ``expirationd.lua``. There are standard ways - it is after all part
-   of a standard rock - but for this purpose just copy the contents of
+   of a `standard rock <https://luarocks.org/modules/rtsisyk/expirationd>`_  - but for this purpose just copy the contents of
    expirationd.lua_ to a default directory.
 2. Start the Tarantool server as described before.
 3. Execute these requests:
diff --git a/doc/sphinx/reference/fiber.rst b/doc/sphinx/reference/fiber.rst
index c35668e36346627e3f0b729ad771b3ba85aa61ec..6f0c2723ef9fe9901b1e8d4faba05fa8c0e829f8 100644
--- a/doc/sphinx/reference/fiber.rst
+++ b/doc/sphinx/reference/fiber.rst
@@ -83,7 +83,7 @@ can be reused when another fiber is created.
 
     Return information about all fibers.
 
-    :return: the name, id, and backtrace of all fibers.
+    :return: the name, id, available memory, used memory, and backtrace of all fibers.
     :rtype: table
 
 .. function:: kill(id)
diff --git a/doc/sphinx/reference/socket.rst b/doc/sphinx/reference/socket.rst
index 3ab7d3671b148e23327c8b17c0c2f244c5c5c7a2..d6ae0dbcec7f8762c81323047ad48545fbd67033 100644
--- a/doc/sphinx/reference/socket.rst
+++ b/doc/sphinx/reference/socket.rst
@@ -88,7 +88,7 @@ the function invocations will look like ``sock:function_name(...)``.
 .. function:: __call(domain, type, protocol)
 
     Create a new TCP or UDP socket. The argument values
-    are the same as in the `Linux man page <http://man7.org/linux/man-pages/man2/socket.2.html>`_.
+    are the same as in the `Linux socket(2) man page <http://man7.org/linux/man-pages/man2/socket.2.html>`_.
 
     :param domain:
     :param type:
@@ -151,7 +151,7 @@ the function invocations will look like ``sock:function_name(...)``.
     .. method:: sysconnect(host, port)
 
         Connect a socket to a remote host. The argument values are the same as
-        in the Linux man page [1]_.
+        in the `Linux connect(2) man page <http://man7.org/linux/man-pages/man2/connect.2.html>`_.
         The host must be an IP address.
 
         Parameters:
@@ -333,7 +333,7 @@ the function invocations will look like ``sock:function_name(...)``.
     .. method:: setsockopt(level, name, value)
 
         Set socket flags. The argument values are the same as in the
-        Linux man page [2]_.
+        `Linux getsockopt(2) man page <http://man7.org/linux/man-pages/man2/setsockopt.2.html>`_.
         The ones that Tarantool accepts are:
 
             * SO_ACCEPTCONN
@@ -372,7 +372,7 @@ the function invocations will look like ``sock:function_name(...)``.
     .. method:: linger([active])
 
         Set or clear the SO_LINGER flag. For a description of the flag, see
-        Linux man page [3]_.
+        the `Linux man page <http://man7.org/linux/man-pages/man1/loginctl.1.html>`_.
 
         :param boolean active:
 
@@ -561,7 +561,4 @@ The strings "A", "B", "C" are printed.
 
 .. _luasocket: https://github.com/diegonehab/luasocket
 
-.. [1] http://man7.org/linux/man-pages/man2/connect.2.html
-.. [2] http://man7.org/linux/man-pages/man2/setsockopt.2.html
-.. [3] http://man7.org/linux/man-pages/man1/loginctl.1.html
 
diff --git a/doc/www/content/newsite/index.yml b/doc/www/content/newsite/index.yml
index 42b7d0ab9dcb742ba933abb9f8e74e96193e91ca..f396d11c27d354fc1cc1d57e0eaaca5128422381 100644
--- a/doc/www/content/newsite/index.yml
+++ b/doc/www/content/newsite/index.yml
@@ -6,10 +6,7 @@ template: "index"
 blocks  :
   header:
     - "Tarantool"
-    - "A NoSQL database running in a Lua application server"
-    - >
-      Tarantool combines the network programming power of Node.JS
-      with data persitence capabilities of Redis.
+    - "Get your data in RAM. Get compute close to data. Enjoy the performance"
   features:
     - format: rst
       content: >
@@ -27,9 +24,9 @@ blocks  :
         MessagePack based client-server protocol
     - format: rst
       content: >
-        two data engines: 100% in-memory with optional persistence
-        and a `2-level disk-based B-tree <http://sphia.org>`_,
-        to use with large data sets
+        a built-in database with two data engines: 100% in-memory
+        with optional persistence and a `2-level disk-based B-tree
+        <http://sphia.org>`_, to use with large data sets
     - "secondary key and index iterators support"
     - "asynchronous master-master replication"
     - "authentication and access control"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 573e325ef591182d38c3fa668003fee6ccf29c39..d0e39a5469d203ed1fc2285279927e7742cf0d29 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -15,6 +15,7 @@ include_directories(${READLINE_INCLUDE_DIR})
 set(lua_sources)
 lua_source(lua_sources lua/init.lua)
 lua_source(lua_sources lua/fiber.lua)
+lua_source(lua_sources lua/buffer.lua)
 lua_source(lua_sources lua/uuid.lua)
 lua_source(lua_sources lua/digest.lua)
 lua_source(lua_sources lua/msgpackffi.lua)
@@ -43,9 +44,11 @@ set (core_sources
      say.cc
      memory.cc
      fiber.cc
+     cbus.cc
      exception.cc
      coro.cc
      object.cc
+     reflection.c
      assoc.c
  )
 
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 20464e854c4ec5097d1f66ddaa55fc113e3d45a8..b46f631a7d5e21d6388040ba72e3c0e64c339211 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -44,7 +44,7 @@ add_library(box
     alter.cc
     schema.cc
     session.cc
-    port.c
+    port.cc
     request.cc
     txn.cc
     box.cc
@@ -55,7 +55,8 @@ add_library(box
     cluster.cc
     recovery.cc
     replica.cc
-    replication.cc
+    relay.cc
+    wal.cc
     ${lua_sources}
     lua/call.cc
     lua/tuple.cc
diff --git a/src/box/alter.cc b/src/box/alter.cc
index c3d2accefac83339179a11edee72776938e9b929..6fe1c0fa2393bb2e154a395cd381711656d3ac64 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -43,6 +43,11 @@
 #include "cluster.h" /* for cluster_set_uuid() */
 #include "session.h" /* to fetch the current user. */
 
+/**
+ * Lock of scheme modification
+ */
+struct latch schema_lock = LATCH_INITIALIZER(schema_lock);
+
 /** _space columns */
 #define ID               0
 #define UID              1
@@ -925,6 +930,9 @@ on_drop_space(struct trigger * /* trigger */, void *event)
 static void
 on_replace_dd_space(struct trigger * /* trigger */, void *event)
 {
+	latch_lock(&schema_lock);
+	auto lock_guard = make_scoped_guard([&]{ latch_unlock(&schema_lock); });
+
 	struct txn *txn = (struct txn *) event;
 	txn_check_autocommit(txn, "Space _space");
 	struct txn_stmt *stmt = txn_stmt(txn);
@@ -1044,6 +1052,9 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 static void
 on_replace_dd_index(struct trigger * /* trigger */, void *event)
 {
+	latch_lock(&schema_lock);
+	auto lock_guard = make_scoped_guard([&]{ latch_unlock(&schema_lock); });
+
 	struct txn *txn = (struct txn *) event;
 	txn_check_autocommit(txn, "Space _index");
 	struct txn_stmt *stmt = txn_stmt(txn);
diff --git a/src/box/alter.h b/src/box/alter.h
index d66e810df2c32ffa81127c8c1a07158c5fa25897..5438023c657fa977d8f9517e73a0227f582ee874 100644
--- a/src/box/alter.h
+++ b/src/box/alter.h
@@ -29,6 +29,7 @@
  * SUCH DAMAGE.
  */
 #include "trigger.h"
+#include "latch.h"
 
 extern struct trigger alter_space_on_replace_space;
 extern struct trigger alter_space_on_replace_index;
@@ -38,4 +39,9 @@ extern struct trigger on_replace_func;
 extern struct trigger on_replace_priv;
 extern struct trigger on_replace_cluster;
 
+/**
+ * Lock of schema modification
+ */
+extern struct latch schema_lock;
+
 #endif /* INCLUDES_TARANTOOL_BOX_ALTER_H */
diff --git a/src/box/box.cc b/src/box/box.cc
index 500c72c6d0074dca63ca002b2de8ea5ccfde35f8..35b4143699987e12584a6f901cbe7d72f3233129 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -32,7 +32,7 @@
 #include "iproto.h"
 #include "iproto_constants.h"
 #include "recovery.h"
-#include "replication.h"
+#include "relay.h"
 #include "replica.h"
 #include <stat.h>
 #include "main.h"
@@ -58,9 +58,8 @@ box_process_func box_process = process_ro;
 
 struct recovery_state *recovery;
 
-static struct evio_service binary; /* iproto binary listener */
+bool snapshot_in_progress = false;
 
-int snapshot_pid = 0; /* snapshot processes pid */
 static void
 process_ro(struct request *request, struct port *port)
 {
@@ -188,11 +187,7 @@ extern "C" void
 box_set_listen(const char *uri)
 {
 	box_check_uri(uri, "listen");
-	if (evio_service_is_active(&binary))
-		evio_service_stop(&binary);
-
-	if (uri != NULL)
-		coio_service_start(&binary, uri);
+	iproto_set_listen(uri);
 }
 
 extern "C" void
@@ -383,6 +378,7 @@ box_free(void)
 	user_cache_free();
 	schema_free();
 	tuple_free();
+	port_free();
 	recovery_exit(recovery);
 	recovery = NULL;
 	engine_shutdown();
@@ -469,7 +465,8 @@ box_init(void)
 			      cfg_getd("wal_dir_rescan_delay"));
 	title("hot_standby", NULL);
 
-	iproto_init(&binary);
+	port_init();
+	iproto_init();
 	box_set_listen(cfg_gets("listen"));
 
 	int rows_per_wal = box_check_rows_per_wal(cfg_geti("rows_per_wal"));
@@ -505,8 +502,7 @@ box_load_cfg()
 void
 box_atfork()
 {
-	if (recovery == NULL)
-		return;
+	/* box.coredump() forks to save a core. */
 	recovery_atfork(recovery);
 }
 
diff --git a/src/box/box.h b/src/box/box.h
index e08e6cf22b7e1689736cf44f688c8ae31538a6e2..ecf26e1dc4086617460156ea63b752a3d4a9cc05 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -68,8 +68,8 @@ box_set_ro(bool ro);
 bool
 box_is_ro(void);
 
-/** Non zero if snapshot is in progress. */
-extern int snapshot_pid;
+/** True if snapshot is in progress. */
+extern bool snapshot_in_progress;
 /** Incremented with each next snapshot. */
 extern uint32_t snapshot_version;
 
diff --git a/src/box/engine.cc b/src/box/engine.cc
index 87f34144f4417665cec78b36e8c325a86cebabde..2d661fc306f3ba7b8aac225d1179403a7f8c97cb 100644
--- a/src/box/engine.cc
+++ b/src/box/engine.cc
@@ -33,9 +33,12 @@
 #include "salad/rlist.h"
 #include <stdlib.h>
 #include <string.h>
+#include <latch.h>
 
 RLIST_HEAD(engines);
 
+extern bool snapshot_in_progress;
+extern struct latch schema_lock;
 
 Engine::Engine(const char *engine_name)
 	:name(engine_name),
@@ -149,11 +152,11 @@ engine_end_recovery()
 int
 engine_checkpoint(int64_t checkpoint_id)
 {
-	static bool snapshot_is_in_progress = false;
-	if (snapshot_is_in_progress)
+	if (snapshot_in_progress)
 		return EINPROGRESS;
 
-	snapshot_is_in_progress = true;
+	snapshot_in_progress = true;
+	latch_lock(&schema_lock);
 
 	/* create engine snapshot */
 	Engine *engine;
@@ -172,14 +175,16 @@ engine_checkpoint(int64_t checkpoint_id)
 	engine_foreach(engine) {
 		engine->commitCheckpoint();
 	}
-	snapshot_is_in_progress = false;
+	latch_unlock(&schema_lock);
+	snapshot_in_progress = false;
 	return 0;
 error:
 	int save_errno = errno;
 	/* rollback snapshot creation */
 	engine_foreach(engine)
 		engine->abortCheckpoint();
-	snapshot_is_in_progress = false;
+	latch_unlock(&schema_lock);
+	snapshot_in_progress = false;
 	return save_errno;
 }
 
diff --git a/src/box/errcode.h b/src/box/errcode.h
index 24226031a1103e2ee76ffb38c55af1f75adb15cc..142e931a2902a5beff20eb864fb53a7f6e8f56ac 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -58,7 +58,7 @@ struct errcode_record {
 	/*  4 */_(ER_TUPLE_NOT_FOUND,		2, "Tuple doesn't exist in index '%s' in space '%s'") \
 	/*  5 */_(ER_UNSUPPORTED,		2, "%s does not support %s") \
 	/*  6 */_(ER_NONMASTER,			2, "Can't modify data on a replication slave. My master is: %s") \
-	/*  7 */_(ER_READONLY,			2, "Can't modify data because this server in read-only mode.") \
+	/*  7 */_(ER_READONLY,			2, "Can't modify data because this server is in read-only mode.") \
 	/*  8 */_(ER_INJECTION,			2, "Error injection '%s'") \
 	/*  9 */_(ER_CREATE_SPACE,		2, "Failed to create space '%s': %s") \
 	/* 10 */_(ER_SPACE_EXISTS,		2, "Space '%s' already exists") \
@@ -150,8 +150,8 @@ struct errcode_record {
 	/* 96 */_(ER_GUEST_USER_PASSWORD,       2, "Setting password for guest user has no effect") \
 	/* 97 */_(ER_TRANSACTION_CONFLICT,      2, "Transaction has been aborted by conflict") \
 	/* 98 */_(ER_UNSUPPORTED_ROLE_PRIV,     2, "Unsupported role privilege '%s'") \
-	/* 98 */_(ER_LOAD_FUNCTION,		2, "Failed to dynamically load function '%s': %s") \
-	/* 98 */_(ER_FUNCTION_LANGUAGE,		2, "Unsupported language '%s' specified for function '%s'") \
+	/* 99 */_(ER_LOAD_FUNCTION,		2, "Failed to dynamically load function '%s': %s") \
+	/*100 */_(ER_FUNCTION_LANGUAGE,		2, "Unsupported language '%s' specified for function '%s'") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/error.cc b/src/box/error.cc
index 6ccad390efea130d3fd09508ab4f50ae352755bd..5ec6435bb98e301074925c46541b88752249c579 100644
--- a/src/box/error.cc
+++ b/src/box/error.cc
@@ -28,11 +28,17 @@
  */
 #include "error.h"
 #include <stdio.h>
-#include <typeinfo>
+
+static struct method clienterror_methods[] = {
+	make_method(&type_ClientError, "code", &ClientError::errcode),
+	METHODS_SENTINEL
+};
+const struct type type_ClientError = make_type("ClientError", &type_Exception,
+	clienterror_methods);
 
 ClientError::ClientError(const char *file, unsigned line,
 			 uint32_t errcode, ...)
-	: Exception(file, line)
+	: Exception(&type_ClientError, file, line)
 {
 	m_errcode = errcode;
 	va_list ap;
@@ -44,7 +50,7 @@ ClientError::ClientError(const char *file, unsigned line,
 
 ClientError::ClientError(const char *file, unsigned line, const char *msg,
 			 uint32_t errcode)
-	: Exception(file, line)
+	: Exception(&type_ClientError, file, line)
 {
 	m_errcode = errcode;
 	strncpy(m_errmsg, msg, sizeof(m_errmsg) - 1);
@@ -61,10 +67,10 @@ ClientError::log() const
 uint32_t
 ClientError::get_errcode(const Exception *e)
 {
-	const ClientError *error = dynamic_cast<const ClientError *>(e);
-	if (error)
-		return error->errcode();
-	if (typeid(*e) == typeid(OutOfMemory))
+	ClientError *client_error = type_cast(ClientError, e);
+	if (client_error)
+		return client_error->errcode();
+	if (type_cast(OutOfMemory, e))
 		return ER_MEMORY_ISSUE;
 	return ER_PROC_LUA;
 }
diff --git a/src/box/error.h b/src/box/error.h
index caaebc0c6f54ffda39173c14d742c62d44f0e113..3473879793b32c1f87fe4d1a71e01130c7616b7d 100644
--- a/src/box/error.h
+++ b/src/box/error.h
@@ -31,6 +31,7 @@
 #include "errcode.h"
 #include "exception.h"
 
+extern const struct type type_ClientError;
 class ClientError: public Exception {
 public:
 	virtual void raise()
diff --git a/src/box/func.cc b/src/box/func.cc
index 21708cb6a0b2945c188ae2e63b0a4714ac43a6d7..73f00a346f9f97d6ea5e97cf23c4da1c054db906 100644
--- a/src/box/func.cc
+++ b/src/box/func.cc
@@ -90,16 +90,16 @@ func_load(struct func *func)
 	 * Extract package name from function name.
 	 * E.g. name = foo.bar.baz, function = baz, package = foo.bar
 	 */
+	const char *sym;
 	const char *package = func->def.name;
-	const char *package_end = NULL;
-	const char *sym = package;
-	while ((sym = strchr(sym, '.')))
-		package_end = sym;
-	if (package_end == NULL) {
+	const char *package_end = strrchr(package, '.');
+	if (package_end != NULL) {
+		/* module.submodule.function => module.submodule, function */
+		sym = package_end + 1;
+	} else {
+		/* package == function => function, function */
 		sym = package;
 		package_end = package + strlen(package);
-	} else {
-		sym = package_end + 1;
 	}
 
 	/* First argument of searchpath: name */
diff --git a/src/box/index.cc b/src/box/index.cc
index ca4b14cd232aeab48c549fdfc3d4b1311056772c..fbe5afcd69420ba3241352975aa93da9bbd151e7 100644
--- a/src/box/index.cc
+++ b/src/box/index.cc
@@ -220,6 +220,32 @@ Index::bsize() const
 	return 0;
 }
 
+/**
+ * Create a read view for iterator so further index modifications
+ * will not affect the iterator iteration.
+ */
+void
+Index::createReadViewForIterator(struct iterator *iterator)
+{
+	(void) iterator;
+	tnt_raise(ClientError, ER_UNSUPPORTED,
+		  index_type_strs[key_def->type],
+		  "consistent read view");
+}
+
+/**
+ * Destroy a read view of an iterator. Must be called for iterators,
+ * for which createReadViewForIterator was called.
+ */
+void
+Index::destroyReadViewForIterator(struct iterator *iterator)
+{
+	(void) iterator;
+	tnt_raise(ClientError, ER_UNSUPPORTED,
+		  index_type_strs[key_def->type],
+		  "consistent read view");
+}
+
 void
 index_build(Index *index, Index *pk)
 {
diff --git a/src/box/index.h b/src/box/index.h
index 4bea67a895f383f3cfe45f07a78108ad7b3d72fc..11f490dc849120d1eca90c39f97088590774eccb 100644
--- a/src/box/index.h
+++ b/src/box/index.h
@@ -199,6 +199,17 @@ class Index: public Object {
 				  enum iterator_type type,
 				  const char *key, uint32_t part_count) const = 0;
 
+	/**
+	 * Create a read view for iterator so further index modifications
+	 * will not affect the iteration results.
+	 */
+	virtual void createReadViewForIterator(struct iterator *iterator);
+	/**
+	 * Destroy a read view of an iterator. Must be called for iterators,
+	 * for which createReadViewForIterator() was called.
+	 */
+	virtual void destroyReadViewForIterator(struct iterator *iterator);
+
 	inline struct iterator *position() const
 	{
 		if (m_position == NULL)
diff --git a/src/box/iproto.cc b/src/box/iproto.cc
index cb60c0188373f0236fbfecbf55ca5440a0c2f4a2..f60fca845bb6df229027b49e296ef287fdee529a 100644
--- a/src/box/iproto.cc
+++ b/src/box/iproto.cc
@@ -35,6 +35,7 @@
 #include "iproto_port.h"
 #include "main.h"
 #include "fiber.h"
+#include "cbus.h"
 #include "say.h"
 #include "evio.h"
 #include "scoped_guard.h"
@@ -50,109 +51,80 @@
 #include "stat.h"
 #include "lua/call.h"
 
-/* {{{ iproto_task - declaration */
-
-struct iproto_connection;
-
-typedef void (*iproto_task_f)(struct iproto_task *);
+/* {{{ iproto_msg - declaration */
 
 /**
- * A single request from the client. All requests
- * from all clients are queued into a single queue
+ * A single msg from io thread. All requests
+ * from all connections are queued into a single queue
  * and processed in FIFO order.
  */
-struct iproto_task
+struct iproto_msg: public cmsg
 {
 	struct iproto_connection *connection;
-	struct iobuf *iobuf;
-	iproto_task_f process;
+
+	/* --- Box msgs - actual requests for the transaction processor --- */
 	/* Request message code and sync. */
 	struct xrow_header header;
 	/* Box request, if this is a DML */
 	struct request request;
+	/*
+	 * Remember the active iobuf of the connection,
+	 * in which the request is stored. The response
+	 * must be put into the out buffer of this iobuf.
+	 */
+	struct iobuf *iobuf;
+	/**
+	 * How much space the request takes in the
+	 * input buffer (len, header and body - all of it)
+	 * This also works as a reference counter to
+	 * iproto_connection object.
+	 */
 	size_t len;
+	/** End of write position in the output buffer */
+	struct obuf_svp write_end;
+	/**
+	 * Used in "connect" msgs, true if connect trigger failed
+	 * and the connection must be closed.
+	 */
+	bool close_connection;
 };
 
-struct mempool iproto_task_pool;
+static struct mempool iproto_msg_pool;
 
-static struct iproto_task *
-iproto_task_new(struct iproto_connection *con,
-		iproto_task_f process)
+static struct iproto_msg *
+iproto_msg_new(struct iproto_connection *con, struct cmsg_hop *route)
 {
-	struct iproto_task *task =
-		(struct iproto_task *) mempool_alloc(&iproto_task_pool);
-	task->connection = con;
-	task->process = process;
-	return task;
+	struct iproto_msg *msg =
+		(struct iproto_msg *) mempool_alloc(&iproto_msg_pool);
+	cmsg_init(msg, route);
+	msg->connection = con;
+	return msg;
 }
 
+static inline void
+iproto_msg_delete(struct cmsg *msg)
+{
+	mempool_free(&iproto_msg_pool, msg);
+}
 
-static void
-iproto_process_connect(struct iproto_task *task);
-
-static void
-iproto_process_disconnect(struct iproto_task *task);
-
-static void
-iproto_process(struct iproto_task *task);
-
-struct IprotoRequestGuard {
-	struct iproto_task *task;
-	IprotoRequestGuard(struct iproto_task *task_arg):task(task_arg) {}
-	~IprotoRequestGuard()
-	{ if (task) mempool_free(&iproto_task_pool, task); }
-	struct iproto_task *release()
-	{ struct iproto_task *tmp = task; task = NULL; return tmp; }
+struct IprotoMsgGuard {
+	struct iproto_msg *msg;
+	IprotoMsgGuard(struct iproto_msg *msg_arg):msg(msg_arg) {}
+	~IprotoMsgGuard()
+	{ if (msg) iproto_msg_delete(msg); }
+	struct iproto_msg *release()
+	{ struct iproto_msg *tmp = msg; msg = NULL; return tmp; }
 };
 
-/* }}} */
-
-/* {{{ iproto_queue */
-
-struct iproto_task;
+enum { IPROTO_FIBER_POOL_SIZE = 1024 };
 
-enum { IPROTO_REQUEST_QUEUE_SIZE = 2048, };
-
-/**
- * Implementation of an input queue of the box request processor.
- *
- * Event handlers read data, determine request boundaries
- * and enqueue requests. Once all input/output events are
- * processed, an own handler is invoked to deal with the
- * requests in the queue. It leases a fiber from a pool
- * and runs the request in the fiber.
- *
- * @sa iproto_queue_schedule
- */
-struct iproto_queue
-{
-	/** Ring buffer of fixed size */
-	struct iproto_task *queue[IPROTO_REQUEST_QUEUE_SIZE];
-	/**
-	 * Cache of fibers which work on tasks
-	 * in this queue.
-	 */
-	struct rlist fiber_cache;
-	/**
-	 * Used to trigger task processing when
-	 * the queue becomes non-empty.
-	 */
-	struct ev_async watcher;
-	/* Ring buffer position. */
-	int begin, end;
-	/* Ring buffer size. */
-	int size;
-};
+/* }}} */
 
-static inline bool
-iproto_queue_is_empty(struct iproto_queue *i_queue)
-{
-	return i_queue->begin == i_queue->end;
-}
+/* {{{ iproto connection and requests */
 
 /**
- * A single global queue for all tasks in all connections. All
- * tasks are processed concurrently.
+ * A single global queue for all requests in all connections. All
+ * requests from all connections are processed concurrently.
  * Is also used as a queue for just established connections and to
  * execute disconnect triggers. A few notes about these triggers:
  * - they need to be run in a fiber
@@ -161,53 +133,11 @@ iproto_queue_is_empty(struct iproto_queue *i_queue)
  * - on_connect trigger must be processed before any other
  *   request on this connection.
  */
-static struct iproto_queue tx_queue;
-
-static void
-iproto_queue_push(struct iproto_queue *i_queue,
-		  struct iproto_task *task)
-{
-	/* If the queue is full, invoke the handler to work it off. */
-	if (i_queue->end == i_queue->size)
-		ev_invoke(loop(), &i_queue->watcher, EV_CUSTOM);
-	assert(i_queue->end < i_queue->size);
-	/*
-	 * There were some queued tasks, ensure they are
-	 * handled.
-	 */
-	if (iproto_queue_is_empty(i_queue))
-		ev_feed_event(loop(), &tx_queue.watcher, EV_CUSTOM);
-	i_queue->queue[i_queue->end++] = task;
-}
-
-static struct iproto_task *
-iproto_queue_pop(struct iproto_queue *i_queue)
-{
-	if (i_queue->begin == i_queue->end)
-		return NULL;
-	struct iproto_task *task = i_queue->queue[i_queue->begin++];
-	if (i_queue->begin == i_queue->end)
-		i_queue->begin = i_queue->end = 0;
-	return task;
-}
-
-static inline void
-iproto_queue_init(struct iproto_queue *i_queue, ev_async_cb cb)
-{
-	i_queue->size = IPROTO_REQUEST_QUEUE_SIZE;
-	i_queue->begin = i_queue->end = 0;
-	/**
-	 * Initialize an ev_async event which would start
-	 * workers for all outstanding tasks.
-	 */
-	ev_async_init(&i_queue->watcher, cb);
-	i_queue->watcher.data = i_queue;
-	rlist_create(&i_queue->fiber_cache);
-}
-
-/* }}} */
-
-/* {{{ iproto_connection */
+static struct cpipe tx_pipe;
+static struct cpipe net_pipe;
+static struct cbus net_tx_bus;
+/* A pointer to the transaction processor cord. */
+struct cord *tx_cord;
 
 /** Context of a single client connection. */
 struct iproto_connection
@@ -222,49 +152,48 @@ struct iproto_connection
 	struct iobuf *iobuf[2];
 	/*
 	 * Size of readahead which is not parsed yet, i.e.
-	 * size of a piece of task which is not fully read.
-	 * Is always relative to iobuf[0]->in.end. In other words,
-	 * iobuf[0]->in.end - parse_size gives the start of the
-	 * unparsed task. A size rather than a pointer is used
+	 * size of a piece of request which is not fully read.
+	 * Is always relative to iobuf[0]->in.wpos. In other words,
+	 * iobuf[0]->in.wpos - parse_size gives the start of the
+	 * unparsed request. A size rather than a pointer is used
 	 * to be safe in case in->buf is reallocated. Being
-	 * relative to in->end, rather than to in->pos is helpful to
+	 * relative to in->wpos, rather than to in->rpos is helpful to
 	 * make sure ibuf_reserve() or iobuf rotation don't make
 	 * the value meaningless.
 	 */
 	ssize_t parse_size;
-	/** Current write position in the output buffer */
-	struct obuf_svp write_pos;
-	/**
-	 * Function of the task processor to handle
-	 * a single task.
-	 */
 	struct ev_io input;
 	struct ev_io output;
 	/** Logical session. */
 	struct session *session;
 	uint64_t cookie;
 	ev_loop *loop;
-	/* Pre-allocated disconnect task. */
-	struct iproto_task *disconnect;
+	/* Pre-allocated disconnect msg. */
+	struct iproto_msg *disconnect;
 };
 
 static struct mempool iproto_connection_pool;
 
 /**
  * A connection is idle when the client is gone
- * and there are no outstanding tasks in the task queue.
+ * and there are no outstanding msgs in the msg queue.
  * An idle connection can be safely garbage collected.
  * Note: a connection only becomes idle after iproto_connection_close(),
  * which closes the fd.  This is why here the check is for
- * evio_is_active() (false if fd is closed), not ev_is_active()
- * (false if event is not started).
+ * evio_has_fd(), not ev_is_active()  (false if event is not
+ * started).
+ *
+ * ibuf_size() provides an effective reference counter
+ * on connection use in the tx request queue. Any request
+ * in the request queue has a non-zero len, and ibuf_size()
+ * is therefore non-zero as long as there is at least
+ * one request in the tx queue.
  */
 static inline bool
 iproto_connection_is_idle(struct iproto_connection *con)
 {
-	return !evio_is_active(&con->input) &&
-		ibuf_size(&con->iobuf[0]->in) == 0 &&
-		ibuf_size(&con->iobuf[1]->in) == 0;
+	return ibuf_used(&con->iobuf[0]->in) == 0 &&
+		ibuf_used(&con->iobuf[1]->in) == 0;
 }
 
 static void
@@ -274,47 +203,103 @@ static void
 iproto_connection_on_output(ev_loop * /* loop */, struct ev_io *watcher,
 			    int /* revents */);
 
+/** Recycle a connection. Never throws. */
+static inline void
+iproto_connection_delete(struct iproto_connection *con)
+{
+	assert(iproto_connection_is_idle(con));
+	assert(!evio_has_fd(&con->output));
+	assert(!evio_has_fd(&con->input));
+	assert(con->session == NULL);
+	/*
+	 * The output buffers must have been deleted
+	 * in tx thread.
+	 */
+	iobuf_delete_mt(con->iobuf[0]);
+	iobuf_delete_mt(con->iobuf[1]);
+	if (con->disconnect)
+		iproto_msg_delete(con->disconnect);
+	mempool_free(&iproto_connection_pool, con);
+}
+
+static void
+tx_process_msg(struct cmsg *msg);
+
+static void
+net_send_msg(struct cmsg *msg);
+
+/**
+ * Fire on_disconnect triggers in the tx
+ * thread and destroy the session object,
+ * as well as output buffers of the connection.
+ */
+static void
+tx_process_disconnect(struct cmsg *m)
+{
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	struct iproto_connection *con = msg->connection;
+	if (con->session) {
+		if (! rlist_empty(&session_on_disconnect))
+			session_run_on_disconnect_triggers(con->session);
+		session_destroy(con->session);
+		con->session = NULL; /* safety */
+	}
+	/*
+	 * Got to be done in iproto thread since
+	 * that's where the memory is allocated.
+	 */
+	obuf_destroy(&con->iobuf[0]->out);
+	obuf_destroy(&con->iobuf[1]->out);
+}
+
+/**
+ * Cleanup the net thread resources of a connection
+ * and close the connection.
+ */
+static void
+net_finish_disconnect(struct cmsg *m)
+{
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	/* Runs the trigger, which may yield. */
+	iproto_connection_delete(msg->connection);
+	iproto_msg_delete(msg);
+}
+
+static struct cmsg_hop disconnect_route[] = {
+	{ tx_process_disconnect, &net_pipe },
+	{ net_finish_disconnect, NULL },
+};
+
+
+static struct cmsg_hop request_route[] = {
+	{ tx_process_msg, &net_pipe },
+	{ net_send_msg, NULL },
+};
+
 static struct iproto_connection *
 iproto_connection_new(const char *name, int fd, struct sockaddr *addr)
 {
+	(void) name;
 	struct iproto_connection *con = (struct iproto_connection *)
 		mempool_alloc(&iproto_connection_pool);
 	con->input.data = con->output.data = con;
 	con->loop = loop();
 	ev_io_init(&con->input, iproto_connection_on_input, fd, EV_READ);
 	ev_io_init(&con->output, iproto_connection_on_output, fd, EV_WRITE);
-	con->iobuf[0] = iobuf_new(name);
-	con->iobuf[1] = iobuf_new(name);
+	con->iobuf[0] = iobuf_new_mt(&tx_cord->slabc);
+	con->iobuf[1] = iobuf_new_mt(&tx_cord->slabc);
 	con->parse_size = 0;
-	con->write_pos = obuf_create_svp(&con->iobuf[0]->out);
 	con->session = NULL;
 	con->cookie = *(uint64_t *) addr;
 	/* It may be very awkward to allocate at close. */
-	con->disconnect = iproto_task_new(con, iproto_process_disconnect);
+	con->disconnect = iproto_msg_new(con, disconnect_route);
 	return con;
 }
 
-/** Recycle a connection. Never throws. */
-static inline void
-iproto_connection_delete(struct iproto_connection *con)
-{
-	assert(iproto_connection_is_idle(con));
-	assert(!evio_is_active(&con->output));
-	if (con->session) {
-		if (! rlist_empty(&session_on_disconnect))
-			session_run_on_disconnect_triggers(con->session);
-		session_destroy(con->session);
-	}
-	iobuf_delete(con->iobuf[0]);
-	iobuf_delete(con->iobuf[1]);
-	if (con->disconnect)
-		mempool_free(&iproto_task_pool, con->disconnect);
-	mempool_free(&iproto_connection_pool, con);
-}
-
 static inline void
-iproto_connection_shutdown(struct iproto_connection *con)
+iproto_connection_close(struct iproto_connection *con)
 {
+	int fd = con->input.fd;
 	ev_io_stop(con->loop, &con->input);
 	ev_io_stop(con->loop, &con->output);
 	con->input.fd = con->output.fd = -1;
@@ -322,27 +307,20 @@ iproto_connection_shutdown(struct iproto_connection *con)
 	 * Discard unparsed data, to recycle the con
 	 * as soon as all parsed data is processed.
 	 */
-	con->iobuf[0]->in.end -= con->parse_size;
+	con->iobuf[0]->in.wpos -= con->parse_size;
 	/*
 	 * If the con is not idle, it is destroyed
-	 * after the last task is handled. Otherwise,
-	 * queue a separate task to run on_disconnect()
+	 * after the last request is handled. Otherwise,
+	 * queue a separate msg to run on_disconnect()
 	 * trigger and destroy the connection.
 	 * Sic: the check is mandatory to not destroy a connection
 	 * twice.
 	 */
 	if (iproto_connection_is_idle(con)) {
-		struct iproto_task *task = con->disconnect;
+		struct iproto_msg *msg = con->disconnect;
 		con->disconnect = NULL;
-		iproto_queue_push(&tx_queue, task);
+		cpipe_push(&tx_pipe, msg);
 	}
-}
-
-static inline void
-iproto_connection_close(struct iproto_connection *con)
-{
-	int fd = con->input.fd;
-	iproto_connection_shutdown(con);
 	close(fd);
 }
 
@@ -378,8 +356,8 @@ iproto_connection_input_iobuf(struct iproto_connection *con)
 
 	/* The type code is checked in iproto_enqueue_batch() */
 	if (con->parse_size) {
-		const char *pos = oldbuf->in.end - con->parse_size;
-		if (mp_check_uint(pos, oldbuf->in.end) <= 0)
+		const char *pos = oldbuf->in.wpos - con->parse_size;
+		if (mp_check_uint(pos, oldbuf->in.wpos) <= 0)
 			to_read = mp_decode_uint(&pos);
 	}
 
@@ -387,7 +365,7 @@ iproto_connection_input_iobuf(struct iproto_connection *con)
 		return oldbuf;
 
 	/** All requests are processed, reuse the buffer. */
-	if (ibuf_size(&oldbuf->in) == con->parse_size) {
+	if (ibuf_used(&oldbuf->in) == con->parse_size) {
 		ibuf_reserve(&oldbuf->in, to_read);
 		return oldbuf;
 	}
@@ -406,10 +384,10 @@ iproto_connection_input_iobuf(struct iproto_connection *con)
 	 * Discard unparsed data in the old buffer, otherwise it
 	 * won't be recycled when all parsed requests are processed.
 	 */
-	oldbuf->in.end -= con->parse_size;
+	oldbuf->in.wpos -= con->parse_size;
 	/* Move the cached request prefix to the new buffer. */
-	memcpy(newbuf->in.pos, oldbuf->in.end, con->parse_size);
-	newbuf->in.end += con->parse_size;
+	memcpy(newbuf->in.rpos, oldbuf->in.wpos, con->parse_size);
+	newbuf->in.wpos += con->parse_size;
 	/*
 	 * Rotate buffers. Not strictly necessary, but
 	 * helps preserve response order.
@@ -423,54 +401,69 @@ iproto_connection_input_iobuf(struct iproto_connection *con)
 static inline void
 iproto_enqueue_batch(struct iproto_connection *con, struct ibuf *in)
 {
+	bool stop_input = false;
 	while (true) {
-		const char *reqstart = in->end - con->parse_size;
+		const char *reqstart = in->wpos - con->parse_size;
 		const char *pos = reqstart;
 		/* Read request length. */
 		if (mp_typeof(*pos) != MP_UINT) {
 			tnt_raise(ClientError, ER_INVALID_MSGPACK,
 				  "packet length");
 		}
-		if (mp_check_uint(pos, in->end) >= 0)
+		if (mp_check_uint(pos, in->wpos) >= 0)
 			break;
 		uint32_t len = mp_decode_uint(&pos);
 		const char *reqend = pos + len;
-		if (reqend > in->end)
+		if (reqend > in->wpos)
 			break;
-		struct iproto_task *task =
-			iproto_task_new(con, iproto_process);
-		task->iobuf = con->iobuf[0];
-		IprotoRequestGuard guard(task);
-
-		xrow_header_decode(&task->header, &pos, reqend);
-		task->len = pos - reqstart; /* total request length */
+		struct iproto_msg *msg = iproto_msg_new(con, request_route);
+		msg->iobuf = con->iobuf[0];
+		IprotoMsgGuard guard(msg);
 
+		xrow_header_decode(&msg->header, &pos, reqend);
+		assert(pos == reqend);
+		msg->len = reqend - reqstart; /* total request length */
 		/*
 		 * sic: in case of exception con->parse_size
-		 * as well as in->pos must not be advanced, to
-		 * stay in sync.
+		 * must not be advanced to stay in sync with
+		 * in->rpos.
 		 */
-		if (task->header.type >= IPROTO_SELECT &&
-		    task->header.type <= IPROTO_EVAL) {
+		if (msg->header.type >= IPROTO_SELECT &&
+		    msg->header.type <= IPROTO_EVAL) {
 			/* Pre-parse request before putting it into the queue */
-			if (task->header.bodycnt == 0) {
+			if (msg->header.bodycnt == 0) {
 				tnt_raise(ClientError, ER_INVALID_MSGPACK,
 					  "request type");
 			}
-			request_create(&task->request, task->header.type);
-			pos = (const char *) task->header.body[0].iov_base;
-			request_decode(&task->request, pos,
-				       task->header.body[0].iov_len);
+			request_create(&msg->request, msg->header.type);
+			pos = (const char *) msg->header.body[0].iov_base;
+			request_decode(&msg->request, pos,
+				       msg->header.body[0].iov_len);
+		} else if (msg->header.type == IPROTO_SUBSCRIBE ||
+			   msg->header.type == IPROTO_JOIN) {
+			/**
+			 * Don't mess with the file descriptor
+			 * while join is running.
+			 */
+			ev_io_stop(con->loop, &con->output);
+			ev_io_stop(con->loop, &con->input);
+			stop_input = true;
 		}
-		task->request.header = &task->header;
-		iproto_queue_push(&tx_queue, guard.release());
-		/* Request will be discarded in iproto_process_XXX */
+		msg->request.header = &msg->header;
+		cpipe_push_input(&tx_pipe, guard.release());
 
 		/* Request is parsed */
 		con->parse_size -= reqend - reqstart;
-		if (con->parse_size == 0)
+		if (con->parse_size == 0 || stop_input)
 			break;
 	}
+	cpipe_flush_input(&tx_pipe);
+	/*
+	 * Keep reading input, as long as the socket
+	 * supplies data.
+	 */
+	if (!stop_input && !ev_is_active(&con->input))
+		ev_feed_event(con->loop, &con->input, EV_READ);
 }
 
 static void
@@ -492,7 +485,7 @@ iproto_connection_on_input(ev_loop *loop, struct ev_io *watcher,
 
 		struct ibuf *in = &iobuf->in;
 		/* Read input. */
-		int nrd = sio_read(fd, in->end, ibuf_unused(in));
+		int nrd = sio_read(fd, in->wpos, ibuf_unused(in));
 		if (nrd < 0) {                  /* Socket is not ready. */
 			ev_io_start(loop, &con->input);
 			return;
@@ -502,16 +495,10 @@ iproto_connection_on_input(ev_loop *loop, struct ev_io *watcher,
 			return;
 		}
 		/* Update the read position and connection state. */
-		in->end += nrd;
+		in->wpos += nrd;
 		con->parse_size += nrd;
 		/* Enqueue all requests which are fully read up. */
 		iproto_enqueue_batch(con, in);
-		/*
-		 * Keep reading input, as long as the socket
-		 * supplies data.
-		 */
-		if (!ev_is_active(&con->input))
-			ev_feed_event(loop, &con->input, EV_READ);
 	} catch (Exception *e) {
 		e->log();
 		iproto_connection_close(con);
@@ -522,8 +509,7 @@ iproto_connection_on_input(ev_loop *loop, struct ev_io *watcher,
 static inline struct iobuf *
 iproto_connection_output_iobuf(struct iproto_connection *con)
 {
-	if (obuf_size(&con->iobuf[1]->out) &&
-	    obuf_size(&con->iobuf[1]->out) > con->write_pos.size)
+	if (obuf_used(&con->iobuf[1]->out) > 0)
 		return con->iobuf[1];
 	/*
 	 * Don't try to write from a newer buffer if an older one
@@ -531,40 +517,46 @@ iproto_connection_output_iobuf(struct iproto_connection *con)
 	 * the client may end up getting a salad of different
 	 * pieces of replies from both buffers.
 	 */
-	if (ibuf_size(&con->iobuf[1]->in) == 0 &&
-	    obuf_size(&con->iobuf[0]->out) &&
-	    obuf_size(&con->iobuf[0]->out) > con->write_pos.size)
+	if (ibuf_used(&con->iobuf[1]->in) == 0 &&
+	    obuf_used(&con->iobuf[0]->out) > 0)
 		return con->iobuf[0];
 	return NULL;
 }
 
-/** writev() to the socket and handle the output. */
+/** writev() to the socket and handle the result. */
+
 static int
-iproto_flush(struct iobuf *iobuf, int fd, struct obuf_svp *svp)
+iproto_flush(struct iobuf *iobuf, struct iproto_connection *con)
 {
-	/* Begin writing from the saved position. */
-	struct iovec *iov = iobuf->out.iov + svp->pos;
-	int iovcnt = obuf_iovcnt(&iobuf->out) - svp->pos;
-	assert(iovcnt);
-	ssize_t nwr;
-	try {
-		sio_add_to_iov(iov, -svp->iov_len);
-		nwr = sio_writev(fd, iov, iovcnt);
-
-		sio_add_to_iov(iov, svp->iov_len);
-	} catch (SocketError *) {
-		sio_add_to_iov(iov, svp->iov_len);
-		throw;
-	}
+	int fd = con->output.fd;
+	struct obuf_svp *begin = &iobuf->out.wpos;
+	struct obuf_svp *end = &iobuf->out.wend;
+	assert(begin->used < end->used);
+	struct iovec iov[SMALL_OBUF_IOV_MAX+1];
+	struct iovec *src = iobuf->out.iov;
+	int iovcnt = end->pos - begin->pos + 1;
+	/*
+	 * iov[i].iov_len may be concurrently modified in tx thread,
+	 * but only for the last position.
+	 */
+	memcpy(iov, src + begin->pos, iovcnt * sizeof(struct iovec));
+	sio_add_to_iov(iov, -begin->iov_len);
+	/* *Overwrite* iov_len of the last pos as it may be garbage. */
+	iov[iovcnt-1].iov_len = end->iov_len - begin->iov_len * (iovcnt == 1);
 
+	ssize_t nwr = sio_writev(fd, iov, iovcnt);
 	if (nwr > 0) {
-		if (svp->size + nwr == obuf_size(&iobuf->out)) {
-			iobuf_reset(iobuf);
-			*svp = obuf_create_svp(&iobuf->out);
+		if (begin->used + nwr == end->used) {
+			/* Quickly recycle the buffer if it's idle. */
+			if (ibuf_used(&iobuf->in) == 0) {
+				assert(end->used == obuf_size(&iobuf->out));
+				iobuf_reset(iobuf);
+			}
+			*begin = *end;          /* advance write position */
 			return 0;
 		}
-		svp->size += nwr;
-		svp->pos += sio_move_iov(iov, nwr, &svp->iov_len);
+		begin->used += nwr;             /* advance write position */
+		begin->pos += sio_move_iov(iov, nwr, &begin->iov_len);
 	}
 	return -1;
 }
@@ -574,13 +566,11 @@ iproto_connection_on_output(ev_loop *loop, struct ev_io *watcher,
 			    int /* revents */)
 {
 	struct iproto_connection *con = (struct iproto_connection *) watcher->data;
-	int fd = con->output.fd;
-	struct obuf_svp *svp = &con->write_pos;
 
 	try {
 		struct iobuf *iobuf;
 		while ((iobuf = iproto_connection_output_iobuf(con))) {
-			if (iproto_flush(iobuf, fd, svp) < 0) {
+			if (iproto_flush(iobuf, con) < 0) {
 				ev_io_start(loop, &con->output);
 				return;
 			}
@@ -588,98 +578,36 @@ iproto_connection_on_output(ev_loop *loop, struct ev_io *watcher,
 				ev_feed_event(loop, &con->input, EV_READ);
 		}
 		if (ev_is_active(&con->output))
-			ev_io_stop(loop, &con->output);
+			ev_io_stop(con->loop, &con->output);
 	} catch (Exception *e) {
 		e->log();
 		iproto_connection_close(con);
 	}
 }
 
-/* }}} */
-
-/** {{{ tx_queue handlers */
-/**
- * Main function of the fiber invoked to handle all outstanding
- * tasks in a queue.
- */
 static void
-iproto_tx_queue_fiber(va_list ap)
+tx_process_msg(struct cmsg *m)
 {
-	struct iproto_queue *i_queue = va_arg(ap, struct iproto_queue *);
-	struct iproto_task *task;
-restart:
-	while ((task = iproto_queue_pop(i_queue))) {
-		IprotoRequestGuard guard(task);
-		struct session *session = task->connection->session;
-		fiber_set_session(fiber(), session);
-		fiber_set_user(fiber(), &session->credentials);
-		task->process(task);
-	}
-	/** Put the current fiber into a queue fiber cache. */
-	rlist_add_entry(&i_queue->fiber_cache, fiber(), state);
-	fiber_yield();
-	goto restart;
-}
-
-/** Create fibers to handle all outstanding tasks. */
-static void
-iproto_tx_queue_cb(ev_loop * /* loop */, struct ev_async *watcher,
-		   int /* events */)
-{
-	struct iproto_queue *i_queue = (struct iproto_queue *) watcher->data;
-	while (! iproto_queue_is_empty(i_queue)) {
-
-		struct fiber *f;
-		if (! rlist_empty(&i_queue->fiber_cache))
-			f = rlist_shift_entry(&i_queue->fiber_cache,
-					      struct fiber, state);
-		else
-			f = fiber_new("iproto", iproto_tx_queue_fiber);
-		fiber_start(f, i_queue);
-	}
-}
-
-/* }}} */
-
-/* {{{ iproto_process_* functions */
-
-static void
-iproto_process(struct iproto_task *task)
-{
-	struct iobuf *iobuf = task->iobuf;
-	struct obuf *out = &iobuf->out;
-	struct iproto_connection *con = task->connection;
-
-	auto scope_guard = make_scoped_guard([=]{
-		/* Discard request (see iproto_enqueue_batch()) */
-		iobuf->in.pos += task->len;
-
-		if (evio_is_active(&con->output)) {
-			if (! ev_is_active(&con->output))
-				ev_feed_event(con->loop,
-					      &con->output,
-					      EV_WRITE);
-		} else if (iproto_connection_is_idle(con)) {
-			iproto_connection_delete(con);
-		}
-	});
-
-	if (unlikely(! evio_is_active(&con->output)))
-		return;
-
-	task->connection->session->sync = task->header.sync;
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	struct obuf *out = &msg->iobuf->out;
+	struct iproto_connection *con = msg->connection;
+	struct session *session = msg->connection->session;
+	fiber_set_session(fiber(), session);
+	fiber_set_user(fiber(), &session->credentials);
+
+	session->sync = msg->header.sync;
 	try {
-		switch (task->header.type) {
+		switch (msg->header.type) {
 		case IPROTO_SELECT:
 		case IPROTO_INSERT:
 		case IPROTO_REPLACE:
 		case IPROTO_UPDATE:
 		case IPROTO_DELETE:
-			assert(task->request.type == task->header.type);
+			assert(msg->request.type == msg->header.type);
 			struct iproto_port port;
-			iproto_port_init(&port, out, task->header.sync);
+			iproto_port_init(&port, out, msg->header.sync);
 			try {
-				box_process(&task->request, (struct port *) &port);
+				box_process(&msg->request, (struct port *) &port);
 			} catch (Exception *e) {
 				/*
 				 * This only works if there are no
@@ -694,27 +622,27 @@ iproto_process(struct iproto_task *task)
 			}
 			break;
 		case IPROTO_CALL:
-			assert(task->request.type == task->header.type);
-			stat_collect(stat_base, task->request.type, 1);
-			box_lua_call(&task->request, out);
+			assert(msg->request.type == msg->header.type);
+			stat_collect(stat_base, msg->request.type, 1);
+			box_lua_call(&msg->request, out);
 			break;
 		case IPROTO_EVAL:
-			assert(task->request.type == task->header.type);
-			stat_collect(stat_base, task->request.type, 1);
-			box_lua_eval(&task->request, out);
+			assert(msg->request.type == msg->header.type);
+			stat_collect(stat_base, msg->request.type, 1);
+			box_lua_eval(&msg->request, out);
 			break;
 		case IPROTO_AUTH:
 		{
-			assert(task->request.type == task->header.type);
-			const char *user = task->request.key;
+			assert(msg->request.type == msg->header.type);
+			const char *user = msg->request.key;
 			uint32_t len = mp_decode_strl(&user);
-			authenticate(user, len, task->request.tuple,
-				     task->request.tuple_end);
-			iproto_reply_ok(out, task->header.sync);
+			authenticate(user, len, msg->request.tuple,
+				     msg->request.tuple_end);
+			iproto_reply_ok(out, msg->header.sync);
 			break;
 		}
 		case IPROTO_PING:
-			iproto_reply_ok(out, task->header.sync);
+			iproto_reply_ok(out, msg->header.sync);
 			break;
 		case IPROTO_JOIN:
 			/*
@@ -722,28 +650,49 @@ iproto_process(struct iproto_task *task)
 			 * lambda in the beginning of the block
 			 * will re-activate the watchers for us.
 			 */
-			ev_io_stop(con->loop, &con->input);
-			ev_io_stop(con->loop, &con->output);
-			box_process_join(con->input.fd, &task->header);
+			box_process_join(con->input.fd, &msg->header);
 			break;
 		case IPROTO_SUBSCRIBE:
-			ev_io_stop(con->loop, &con->input);
-			ev_io_stop(con->loop, &con->output);
 			/*
 			 * Subscribe never returns - unless there
 			 * is an error/exception. In that case
 			 * the write watcher will be re-activated
 			 * the same way as for JOIN.
 			 */
-			box_process_subscribe(con->input.fd, &task->header);
+			box_process_subscribe(con->input.fd, &msg->header);
 			break;
 		default:
 			tnt_raise(ClientError, ER_UNKNOWN_REQUEST_TYPE,
-				   (uint32_t) task->header.type);
+				   (uint32_t) msg->header.type);
 		}
 	} catch (Exception *e) {
-		iproto_reply_error(out, e, task->header.sync);
+		iproto_reply_error(out, e, msg->header.sync);
+	}
+	msg->write_end = obuf_create_svp(out);
+}
+
+static void
+net_send_msg(struct cmsg *m)
+{
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	struct iproto_connection *con = msg->connection;
+	struct iobuf *iobuf = msg->iobuf;
+	/* Discard request (see iproto_enqueue_batch()) */
+	iobuf->in.rpos += msg->len;
+	iobuf->out.wend = msg->write_end;
+	if ((msg->header.type == IPROTO_SUBSCRIBE ||
+	    msg->header.type == IPROTO_JOIN)) {
+		assert(! ev_is_active(&con->input));
+		ev_io_start(con->loop, &con->input);
+	}
+
+	if (evio_has_fd(&con->output)) {
+		if (! ev_is_active(&con->output))
+			ev_feed_event(con->loop, &con->output, EV_WRITE);
+	} else if (iproto_connection_is_idle(con)) {
+		iproto_connection_close(con);
 	}
+	iproto_msg_delete(msg);
 }
 
 const char *
@@ -765,52 +714,67 @@ iproto_greeting(const char *salt)
  * upon a failure.
  */
 static void
-iproto_process_connect(struct iproto_task *request)
+tx_process_connect(struct cmsg *m)
 {
-	struct iproto_connection *con = request->connection;
-	struct iobuf *iobuf = request->iobuf;
-	int fd = con->input.fd;
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	struct iproto_connection *con = msg->connection;
+	struct obuf *out = &msg->iobuf->out;
 	try {              /* connect. */
-		con->session = session_create(fd, con->cookie);
-		coio_write(&con->input, iproto_greeting(con->session->salt),
-			   IPROTO_GREETING_SIZE);
+		con->session = session_create(con->input.fd, con->cookie);
+		obuf_dup(out, iproto_greeting(con->session->salt),
+			 IPROTO_GREETING_SIZE);
 		if (! rlist_empty(&session_on_connect))
 			session_run_on_connect_triggers(con->session);
-	} catch (SocketError *e) {
-		e->log();
-		iproto_connection_close(con);
-		return;
+		msg->write_end = obuf_create_svp(out);
 	} catch (Exception *e) {
-		iproto_reply_error(&iobuf->out, e, request->header.type);
+		iproto_reply_error(out, e, 0 /* zero sync for connect error */);
+		msg->close_connection = true;
+	}
+}
+
+/**
+ * Send a response to connect to the client or close the
+ * connection in case on_connect trigger failed.
+ */
+static void
+net_send_greeting(struct cmsg *m)
+{
+	struct iproto_msg *msg = (struct iproto_msg *) m;
+	struct iproto_connection *con = msg->connection;
+	if (msg->close_connection) {
+		struct obuf *out = &msg->iobuf->out;
 		try {
-			iproto_flush(iobuf, fd, &con->write_pos);
+			sio_writev(con->output.fd, out->iov,
+				   obuf_iovcnt(out));
 		} catch (Exception *e) {
 			e->log();
 		}
+		assert(iproto_connection_is_idle(con));
 		iproto_connection_close(con);
+		iproto_msg_delete(msg);
 		return;
 	}
+	con->iobuf[0]->out.wend = msg->write_end;
 	/*
 	 * Connect is synchronous, so no one could have been
 	 * messing up with the connection while it was in
 	 * progress.
 	 */
-	assert(evio_is_active(&con->input));
+	assert(evio_has_fd(&con->output));
 	/* Handshake OK, start reading input. */
-	ev_feed_event(con->loop, &con->input, EV_READ);
+	ev_feed_event(con->loop, &con->output, EV_WRITE);
+	iproto_msg_delete(msg);
 }
 
-static void
-iproto_process_disconnect(struct iproto_task *request)
-{
-	/* Runs the trigger, which may yield. */
-	iproto_connection_delete(request->connection);
-}
+static struct cmsg_hop connect_route[] = {
+	{ tx_process_connect, &net_pipe },
+	{ net_send_greeting, NULL },
+};
 
 /** }}} */
 
 /**
- * Create a connection context and start input.
+ * Create a connection and start input.
  */
 static void
 iproto_on_accept(struct evio_service * /* service */, int fd,
@@ -824,28 +788,154 @@ iproto_on_accept(struct evio_service * /* service */, int fd,
 
 	con = iproto_connection_new(name, fd, addr);
 	/*
-	 * Ignore task allocation failure - the queue size is
-	 * fixed so there is a limited number of tasks in
+	 * Ignore msg allocation failure - the queue size is
+	 * fixed so there is a limited number of msgs in
 	 * use, all stored in just a few blocks of the memory pool.
 	 */
-	struct iproto_task *task =
-		iproto_task_new(con, iproto_process_connect);
-	task->iobuf = con->iobuf[0];
-	iproto_queue_push(&tx_queue, task);
+	struct iproto_msg *msg = iproto_msg_new(con, connect_route);
+	msg->iobuf = con->iobuf[0];
+	msg->close_connection = false;
+	cpipe_push(&tx_pipe, msg);
 }
 
-/** Initialize a read-write port. */
-void
-iproto_init(struct evio_service *service)
+static struct evio_service binary; /* iproto binary listener */
+
+/**
+ * The network io thread main function:
+ * begin serving the message bus.
+ */
+static void
+net_cord_f(va_list /* ap */)
 {
-	mempool_create(&iproto_task_pool, &cord()->slabc,
-		       sizeof(struct iproto_task));
-	iproto_queue_init(&tx_queue, iproto_tx_queue_cb);
+	/* Got to be called in every thread using iobuf */
+	iobuf_init();
+	mempool_create(&iproto_msg_pool, &cord()->slabc,
+		       sizeof(struct iproto_msg));
+	cpipe_create(&net_pipe);
 	mempool_create(&iproto_connection_pool, &cord()->slabc,
 		       sizeof(struct iproto_connection));
 
-	evio_service_init(loop(), service, "binary",
+	evio_service_init(loop(), &binary, "binary",
 			  iproto_on_accept, NULL);
+
+	cbus_join(&net_tx_bus, &net_pipe);
+	/*
+	 * Nothing to do in the fiber so far, the service
+	 * will take care of creating events for incoming
+	 * connections.
+	 */
+	fiber_yield();
+}
+
+/** Initialize the iproto subsystem and start network io thread */
+void
+iproto_init()
+{
+	tx_cord = cord();
+
+	cbus_create(&net_tx_bus);
+	cpipe_create(&tx_pipe);
+	static struct cpipe_fiber_pool fiber_pool;
+
+	cpipe_fiber_pool_create(&fiber_pool, "iproto", &tx_pipe,
+				IPROTO_FIBER_POOL_SIZE);
+
+	static struct cord net_cord;
+	if (cord_costart(&net_cord, "iproto", net_cord_f, NULL))
+		panic("failed to initialize iproto thread");
+
+	cbus_join(&net_tx_bus, &tx_pipe);
+}
+
+/**
+ * Since there is no way to "synchronously" change the
+ * state of the io thread, to change the listen port
+ * we need to bounce a couple of messages to and
+ * from this thread.
+ */
+struct iproto_set_listen_msg: public cmsg
+{
+	/**
+	 * If there was an error setting the listen port,
+	 * this will contain the error when the message
+	 * returns to the caller.
+	 */
+	struct diag diag;
+	/**
+	 * The uri to set.
+	 */
+	const char *uri;
+	/**
+	 * The way to tell the caller about the end of
+	 * bind.
+	 */
+	struct cmsg_notify wakeup;
+};
+
+/**
+ * The bind has finished, notify the caller.
+ */
+static void
+iproto_on_bind(void *arg)
+{
+	cpipe_push(&tx_pipe, (struct cmsg_notify *) arg);
+}
+
+static void
+iproto_do_set_listen(struct cmsg *m)
+{
+	struct iproto_set_listen_msg *msg =
+		(struct iproto_set_listen_msg *) m;
+	try {
+		if (evio_service_is_active(&binary))
+			evio_service_stop(&binary);
+
+		if (msg->uri != NULL) {
+			binary.on_bind = iproto_on_bind;
+			binary.on_bind_param = &msg->wakeup;
+			evio_service_start(&binary, msg->uri);
+		} else {
+			iproto_on_bind(&msg->wakeup);
+		}
+	} catch (Exception *e) {
+		diag_move(&fiber()->diag, &msg->diag);
+	}
+}
+
+static void
+iproto_set_listen_msg_init(struct iproto_set_listen_msg *msg,
+			    const char *uri)
+{
+	static cmsg_hop route[] = { { iproto_do_set_listen, NULL }, };
+	cmsg_init(msg, route);
+	msg->uri = uri;
+	diag_create(&msg->diag);
+
+	cmsg_notify_init(&msg->wakeup);
+}
+
+void
+iproto_set_listen(const char *uri)
+{
+	/**
+	 * This is a tricky orchestration for something
+	 * that should be pretty easy at the first glance:
+	 * change the listen uri in the io thread.
+	 *
+	 * To do it, create a message which sets the new
+	 * uri, and another one, which will alert tx
+	 * thread when bind() on the new port is done.
+	 */
+	static struct iproto_set_listen_msg msg;
+	iproto_set_listen_msg_init(&msg, uri);
+
+	cpipe_push(&net_pipe, &msg);
+	/** Wait for the end of bind. */
+	fiber_yield();
+	if (! diag_is_empty(&msg.diag)) {
+		diag_move(&msg.diag, &fiber()->diag);
+		diag_last_error(&fiber()->diag)->raise();
+	}
 }
 
 /* vim: set foldmethod=marker */
diff --git a/src/box/iproto.h b/src/box/iproto.h
index fe7d5db3cf1fe9043548ba18551a3d5a43cb73ec..2bfdb480168534d3124432557c2b59847e896609 100644
--- a/src/box/iproto.h
+++ b/src/box/iproto.h
@@ -29,5 +29,9 @@
  * SUCH DAMAGE.
  */
 void
-iproto_init(struct evio_service *service);
+iproto_init();
+
+void
+iproto_set_listen(const char *uri);
+
 #endif
diff --git a/src/box/iproto_port.cc b/src/box/iproto_port.cc
index a789457df5c0343622efc7d2e2488cd01d348e8b..a966ae9a1111d3fc6bc48b2ad1decf0936e250d0 100644
--- a/src/box/iproto_port.cc
+++ b/src/box/iproto_port.cc
@@ -130,7 +130,7 @@ void
 iproto_reply_select(struct obuf *buf, struct obuf_svp *svp, uint64_t sync,
 			uint32_t count)
 {
-	uint32_t len = obuf_size(buf) - svp->size - 5;
+	uint32_t len = obuf_size(buf) - svp->used - 5;
 
 	struct iproto_header_bin header = iproto_header_bin;
 	header.v_len = mp_bswap_u32(len);
diff --git a/src/box/lua/call.cc b/src/box/lua/call.cc
index 648959f357c6fd5ae553b91864d6c55759d8395e..c43e35b36400baf72eb9c0d7dc60c1a9c2a74f0e 100644
--- a/src/box/lua/call.cc
+++ b/src/box/lua/call.cc
@@ -84,6 +84,7 @@ struct port_lua
 {
 	struct port_vtab *vtab;
 	struct lua_State *L;
+	size_t size; /* for port_lua_add_tuple */
 };
 
 static inline struct port_lua *
@@ -123,30 +124,26 @@ port_lua_table_add_tuple(struct port *port, struct tuple *tuple)
 {
 	lua_State *L = port_lua(port)->L;
 	try {
-		int idx = luaL_getn(L, -1);	/* TODO: can be optimized */
 		lbox_pushtuple(L, tuple);
-		lua_rawseti(L, -2, idx + 1);
-
+		lua_rawseti(L, -2, ++port_lua(port)->size);
 	} catch (...) {
 		tnt_raise(ClientError, ER_PROC_LUA, lua_tostring(L, -1));
 	}
 }
 
 /** Add all tuples to a Lua table. */
-static struct port *
-port_lua_table_create(struct lua_State *L)
+void
+port_lua_table_create(struct port_lua *port, struct lua_State *L)
 {
 	static struct port_vtab port_lua_vtab = {
 		port_lua_table_add_tuple,
 		null_port_eof,
 	};
-	struct port_lua *port = (struct port_lua *)
-			region_alloc(&fiber()->gc, sizeof(struct port_lua));
 	port->vtab = &port_lua_vtab;
 	port->L = L;
+	port->size = 0;
 	/* The destination table to append tuples to. */
 	lua_newtable(L);
-	return (struct port *) port;
 }
 
 /* }}} */
@@ -180,94 +177,62 @@ lbox_process(lua_State *L)
 		return luaL_error(L, "box.process(CALL, ...) is not allowed");
 	}
 	/* Capture all output into a Lua table. */
-	struct port *port_lua = port_lua_table_create(L);
+	struct port_lua port_lua;
 	struct request request;
 	request_create(&request, op);
 	request_decode(&request, req, sz);
-	box_process(&request, port_lua);
-
+	port_lua_table_create(&port_lua, L);
+	box_process(&request, (struct port *) &port_lua);
 	return 1;
 }
 
 void
 lbox_request_create(struct request *request,
 		    struct lua_State *L, enum iproto_type type,
-		    int key, int tuple, int def_tuple)
+		    int key, int tuple, int default_tuple)
 {
 	request_create(request, type);
 	request->space_id = lua_tointeger(L, 1);
+	struct region *gc = &fiber()->gc;
+	struct mpstream stream;
+	mpstream_init(&stream, gc, region_reserve_cb, region_alloc_cb);
+
 	if (key > 0) {
-		struct obuf key_buf;
-		obuf_create(&key_buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
-		luamp_encode_tuple(L, luaL_msgpack_default, &key_buf, key);
-		request->key = obuf_join(&key_buf);
-		request->key_end = request->key + obuf_size(&key_buf);
+		size_t used = region_used(gc);
+		luamp_encode_tuple(L, luaL_msgpack_default, &stream, key);
+		mpstream_flush(&stream);
+		size_t key_len = region_used(gc) - used;
+		request->key = (char *) region_join(gc, key_len);
+		request->key_end = request->key + key_len;
 	}
 	if (tuple > 0) {
-		struct obuf tuple_buf;
-		obuf_create(&tuple_buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
-		luamp_encode_tuple(L, luaL_msgpack_default, &tuple_buf, tuple);
-		request->tuple = obuf_join(&tuple_buf);
-		request->tuple_end = request->tuple + obuf_size(&tuple_buf);
-	}
-	if (def_tuple > 0) {
-		struct obuf tuple_buf;
-		obuf_create(&tuple_buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
-		luamp_encode_tuple(L, luaL_msgpack_default, &tuple_buf,
-				   def_tuple);
-		request->extra_tuple = obuf_join(&tuple_buf);
-		request->extra_tuple_end =
-			request->extra_tuple + obuf_size(&tuple_buf);
-	}
-}
-
-static void
-port_ffi_add_tuple(struct port *port, struct tuple *tuple)
-{
-	struct port_ffi *port_ffi = (struct port_ffi *) port;
-	if (port_ffi->size >= port_ffi->capacity) {
-		uint32_t capacity = (port_ffi->capacity > 0) ?
-				2 * port_ffi->capacity : 1024;
-		struct tuple **ret = (struct tuple **)
-			realloc(port_ffi->ret, sizeof(*ret) * capacity);
-		assert(ret != NULL);
-		port_ffi->ret = ret;
-		port_ffi->capacity = capacity;
+		size_t used = region_used(gc);
+		/*
+		 * region_join() above could have allocated memory and
+		 * invalidated stream write position. Reset the
+		 * stream to avoid overwriting the key.
+		 */
+		mpstream_reset(&stream);
+		luamp_encode_tuple(L, luaL_msgpack_default, &stream, tuple);
+		mpstream_flush(&stream);
+		size_t tuple_len = region_used(gc) - used;
+		request->tuple = (char *) region_join(gc, tuple_len);
+		request->tuple_end = request->tuple + tuple_len;
 	}
-	tuple_ref(tuple);
-	port_ffi->ret[port_ffi->size++] = tuple;
-}
-
-struct port_vtab port_ffi_vtab = {
-	port_ffi_add_tuple,
-	null_port_eof,
-};
-
-void
-port_ffi_create(struct port_ffi *port)
-{
-	memset(port, 0, sizeof(*port));
-	port->vtab = &port_ffi_vtab;
-}
-
-static inline void
-port_ffi_clear(struct port_ffi *port)
-{
-	for (uint32_t i = 0; i < port->size; i++) {
-		tuple_unref(port->ret[i]);
-		port->ret[i] = NULL;
+	if (default_tuple > 0) {
+		size_t used = region_used(gc);
+		mpstream_reset(&stream);
+		luamp_encode_tuple(L, luaL_msgpack_default, &stream,
+				   default_tuple);
+		mpstream_flush(&stream);
+		size_t tuple_len = region_used(gc) - used;
+		request->default_tuple = (char *) region_join(gc, tuple_len);
+		request->default_tuple_end = request->default_tuple + tuple_len;
 	}
-	port->size = 0;
-}
-
-void
-port_ffi_destroy(struct port_ffi *port)
-{
-	free(port->ret);
 }
 
 int
-boxffi_select(struct port_ffi *port, uint32_t space_id, uint32_t index_id,
+boxffi_select(struct port *port, uint32_t space_id, uint32_t index_id,
 	      int iterator, uint32_t offset, uint32_t limit,
 	      const char *key, const char *key_end)
 {
@@ -281,26 +246,36 @@ boxffi_select(struct port_ffi *port, uint32_t space_id, uint32_t index_id,
 	request.key = key;
 	request.key_end = key_end;
 
-	/*
-	 * A single instance of port_ffi object is used
-	 * for all selects, reset it.
-	 */
-	port->size = 0;
 	try {
-		box_process(&request, (struct port *) port);
+		box_process(&request, port);
 		return 0;
 	} catch (Exception *e) {
-		/*
-		 * The tuples will be not blessed and garbage
-		 * collected, unreference them here, to avoid
-		 * a leak.
-		 */
-		port_ffi_clear(port);
 		/* will be hanled by box.error() in Lua */
 		return -1;
 	}
 }
 
+static int
+lbox_select(lua_State *L)
+{
+	if (lua_gettop(L) != 6 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
+		!lua_isnumber(L, 3) || !lua_isnumber(L, 4) || !lua_isnumber(L, 5)) {
+		return luaL_error(L, "Usage index:select(space_id, index_id,"
+			"iterator, offset, limit, key)");
+	}
+
+	struct request request;
+	struct port_lua port;
+	lbox_request_create(&request, L, IPROTO_SELECT, 6, -1, -1);
+	request.index_id = lua_tointeger(L, 2);
+	request.iterator = lua_tointeger(L, 3);
+	request.offset = lua_tointeger(L, 4);
+	request.limit = lua_tointeger(L, 5);
+	port_lua_table_create(&port, L);
+	box_process(&request, (struct port *) &port);
+	return 1;
+}
+
 static int
 lbox_insert(lua_State *L)
 {
@@ -569,28 +544,60 @@ lua_isarray(struct lua_State *L, int i)
 	return index_starts_at_1;
 }
 
+static inline void
+execute_c_call(struct func *func, struct request *request, struct obuf *out)
+{
+	assert(func != NULL && func->def.language == FUNC_LANGUAGE_C);
+	if (func->func == NULL)
+		func_load(func);
+
+	const char *name = request->key;
+	uint32_t name_len = mp_decode_strl(&name);
+	SetuidGuard setuid(name, name_len, PRIV_X, func);
+
+	struct port_buf port_buf;
+	port_buf_create(&port_buf);
+	auto guard = make_scoped_guard([&]{
+		port_buf_destroy(&port_buf);
+	});
+
+	func->func(request, &port_buf.base);
+
+	if (in_txn()) {
+		say_warn("a transaction is active at CALL return");
+		txn_rollback();
+	}
+
+	struct obuf_svp svp = iproto_prepare_select(out);
+	try {
+		for (struct port_buf_entry *entry = port_buf.first;
+		     entry != NULL; entry = entry->next) {
+			tuple_to_obuf(entry->tuple, out);
+		}
+		iproto_reply_select(out, &svp, request->header->sync,
+				    port_buf.size);
+	} catch (Exception *e) {
+		obuf_rollback_to_svp(out, &svp);
+		txn_rollback();
+		/* Let all well-behaved exceptions pass through. */
+		throw;
+	}
+}
+
 /**
  * Invoke a Lua stored procedure from the binary protocol
  * (implementation of 'CALL' command code).
  */
 static inline void
-execute_call(lua_State *L, struct request *request, struct obuf *out)
+execute_lua_call(lua_State *L, struct func *func, struct request *request,
+		 struct obuf *out)
 {
 	const char *name = request->key;
 	uint32_t name_len = mp_decode_strl(&name);
-	int oc = 0; /* how many objects are on stack after box_lua_find */
 
-	struct func *func = func_by_name(name, name_len);
-	/*
-	 * func == NULL means that perhaps the user has a global
-	 * "EXECUTE" privilege, so no specific grant to a function.
-	 */
-	if (func == NULL || func->def.language == FUNC_LANGUAGE_LUA) {
-		/* Try to find a function by name in Lua */
-		oc = box_lua_find(L, name, name + name_len);
-	} else if (func->func == NULL) {
-		func_load(func);
-	}
+	int oc = 0; /* how many objects are on stack after box_lua_find */
+	/* Try to find a function by name in Lua */
+	oc = box_lua_find(L, name, name + name_len);
 	/**
 	 * Check access to the function and optionally change
 	 * execution time user id (set user id). Sic: the order
@@ -602,20 +609,18 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 	/* Push the rest of args (a tuple). */
 	const char *args = request->tuple;
 
-	if (func == NULL || func->def.language == FUNC_LANGUAGE_LUA) {
-		uint32_t arg_count = mp_decode_array(&args);
-		luaL_checkstack(L, arg_count, "call: out of stack");
+	uint32_t arg_count = mp_decode_array(&args);
+	luaL_checkstack(L, arg_count, "call: out of stack");
 
-		for (uint32_t i = 0; i < arg_count; i++) {
-			luamp_decode(L, luaL_msgpack_default, &args);
-		}
-		lua_call(L, arg_count + oc - 1, LUA_MULTRET);
-	} else {
-		struct port_lua port;
-		port_lua_create(&port, L);
+	for (uint32_t i = 0; i < arg_count; i++)
+		luamp_decode(L, luaL_msgpack_default, &args);
+	lua_call(L, arg_count + oc - 1, LUA_MULTRET);
 
-		func->func(request, (struct port *) &port);
+	if (in_txn()) {
+		say_warn("a transaction is active at CALL return");
+		txn_rollback();
 	}
+
 	/**
 	 * Add all elements from Lua stack to iproto.
 	 *
@@ -625,8 +630,9 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 	 * then each return value as a tuple.
 	 *
 	 * If a Lua stack contains at least one scalar, each
-	 * value on the stack is converted to a tuple. A Lua
-	 * is converted to a tuple with multiple fields.
+	 * value on the stack is converted to a tuple. A single
+	 * Lua with scalars is converted to a tuple with multiple
+	 * fields.
 	 *
 	 * If the stack is a Lua table, each member of which is
 	 * not scalar, each member of the table is converted to
@@ -637,6 +643,8 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 
 	uint32_t count = 0;
 	struct obuf_svp svp = iproto_prepare_select(out);
+	struct mpstream stream;
+	mpstream_init(&stream, out, obuf_reserve_cb, obuf_alloc_cb);
 
 	try {
 		/** Check if we deal with a table of tables. */
@@ -646,13 +654,13 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 			 * The table is not empty and consists of tables
 			 * or tuples. Treat each table element as a tuple,
 			 * and push it.
-			 */
+		 */
 			lua_pushnil(L);
 			int has_keys = lua_next(L, 1);
 			if (has_keys  && (lua_isarray(L, lua_gettop(L)) || lua_istuple(L, -1))) {
 				do {
 					luamp_encode_tuple(L, luaL_msgpack_default,
-							   out, -1);
+							   &stream, -1);
 					++count;
 					lua_pop(L, 1);
 				} while (lua_next(L, 1));
@@ -663,15 +671,16 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 		}
 		for (int i = 1; i <= nrets; ++i) {
 			if (lua_isarray(L, i) || lua_istuple(L, i)) {
-				luamp_encode_tuple(L, luaL_msgpack_default, out, i);
+				luamp_encode_tuple(L, luaL_msgpack_default, &stream, i);
 			} else {
-				luamp_encode_array(luaL_msgpack_default, out, 1);
-				luamp_encode(L, luaL_msgpack_default, out, i);
+				luamp_encode_array(luaL_msgpack_default, &stream, 1);
+				luamp_encode(L, luaL_msgpack_default, &stream, i);
 			}
 			++count;
 		}
 
-	done:
+done:
+		mpstream_flush(&stream);
 		iproto_reply_select(out, &svp, request->header->sync, count);
 	} catch (...) {
 		obuf_rollback_to_svp(out, &svp);
@@ -682,15 +691,23 @@ execute_call(lua_State *L, struct request *request, struct obuf *out)
 void
 box_lua_call(struct request *request, struct obuf *out)
 {
+	const char *name = request->key;
+	uint32_t name_len = mp_decode_strl(&name);
+
+	struct func *func = func_by_name(name, name_len);
+	if (func != NULL && func->def.language == FUNC_LANGUAGE_C)
+		return execute_c_call(func, request, out);
+
+	/*
+	 * func == NULL means that perhaps the user has a global
+	 * "EXECUTE" privilege, so no specific grant to a function.
+	 */
+	assert(func == NULL || func->def.language == FUNC_LANGUAGE_LUA);
 	lua_State *L = NULL;
 	try {
 		L = lua_newthread(tarantool_L);
 		LuarefGuard coro_ref(tarantool_L);
-		execute_call(L, request, out);
-		if (in_txn()) {
-			say_warn("a transaction is active at CALL return");
-			txn_rollback();
-		}
+		execute_lua_call(L, func, request, out);
 	} catch (Exception *e) {
 		txn_rollback();
 		/* Let all well-behaved exceptions pass through. */
@@ -727,11 +744,14 @@ execute_eval(lua_State *L, struct request *request, struct obuf *out)
 
 	/* Send results of the called procedure to the client. */
 	struct obuf_svp svp = iproto_prepare_select(out);
+	struct mpstream stream;
+	mpstream_init(&stream, out, obuf_reserve_cb, obuf_alloc_cb);
+	int nrets = lua_gettop(L);
 	try {
-		int nrets = lua_gettop(L);
 		for (int k = 1; k <= nrets; ++k) {
-			luamp_encode(L, luaL_msgpack_default, out, k);
+			luamp_encode(L, luaL_msgpack_default, &stream, k);
 		}
+		mpstream_flush(&stream);
 		iproto_reply_select(out, &svp, request->header->sync, nrets);
 	} catch (...) {
 		obuf_rollback_to_svp(out, &svp);
@@ -778,6 +798,7 @@ static const struct luaL_reg boxlib[] = {
 static const struct luaL_reg boxlib_internal[] = {
 	{"process", lbox_process},
 	{"call_loadproc",  lbox_call_loadproc},
+	{"select", lbox_select},
 	{"insert", lbox_insert},
 	{"replace", lbox_replace},
 	{"update", lbox_update},
@@ -795,6 +816,14 @@ box_lua_init(struct lua_State *L)
 	luaL_register(L, "box.internal", boxlib_internal);
 	lua_pop(L, 1);
 
+#if 0
+	/* Get CTypeID for `struct port *' */
+	int rc = luaL_cdef(L, "struct port;");
+	assert(rc == 0);
+	(void) rc;
+	CTID_STRUCT_PORT_PTR = luaL_ctypeid(L, "struct port *");
+	assert(CTID_CONST_STRUCT_TUPLE_REF != 0);
+#endif
 	box_lua_error_init(L);
 	box_lua_tuple_init(L);
 	box_lua_index_init(L);
diff --git a/src/box/lua/call.h b/src/box/lua/call.h
index f578e6a54c0cb5051161689033c2c5c8f67921f2..35b53cce5660ced6926ea76efdaac6ec6637aa6e 100644
--- a/src/box/lua/call.h
+++ b/src/box/lua/call.h
@@ -45,22 +45,9 @@ void
 box_lua_eval(struct request *request, struct obuf *out);
 
 extern "C" {
-struct port_ffi
-{
-	struct port_vtab *vtab;
-	uint32_t size;
-	uint32_t capacity;
-	struct tuple **ret;
-};
-
-void
-port_ffi_create(struct port_ffi *port);
-
-void
-port_ffi_destroy(struct port_ffi *port);
 
 int
-boxffi_select(struct port_ffi *port, uint32_t space_id, uint32_t index_id,
+boxffi_select(struct port *port, uint32_t space_id, uint32_t index_id,
 	      int iterator, uint32_t offset, uint32_t limit,
 	      const char *key, const char *key_end);
 
diff --git a/src/box/lua/error.cc b/src/box/lua/error.cc
index 95f30013333f0eb2ffe5901855265db69e8bb873..4586b257d3cc0e6539ee9f6e41bc0b78ba47276a 100644
--- a/src/box/lua/error.cc
+++ b/src/box/lua/error.cc
@@ -40,6 +40,14 @@ extern "C" {
 #include "lua/utils.h"
 #include "box/error.h"
 
+int
+lbox_error(lua_State *L)
+{
+	(void) L;
+	diag_last_error(&fiber()->diag)->raise();
+	return 0;
+}
+
 static int
 lbox_error_raise(lua_State *L)
 {
@@ -53,8 +61,8 @@ lbox_error_raise(lua_State *L)
 	int top = lua_gettop(L);
 	if (top <= 1) {
 		/* re-throw saved exceptions (if any) */
-		if (fiber()->exception)
-			fiber()->exception->raise();
+		if (diag_last_error(&fiber()->diag))
+			lbox_error(L);
 		return 0;
 	} else if (top >= 2 && lua_type(L, 2) == LUA_TNUMBER) {
 		code = lua_tointeger(L, 2);
@@ -101,7 +109,9 @@ lbox_error_raise(lua_State *L)
 
 	/* see tnt_raise() */
 	say_debug("ClientError at %s:%i", file, line);
-	throw new ClientError(file, line, reason, code);
+	ClientError *e = new ClientError(file, line, reason, code);
+	diag_add_error(&fiber()->diag, e);
+	throw e;
 	return 0;
 }
 
@@ -111,29 +121,29 @@ lbox_error_last(lua_State *L)
 	if (lua_gettop(L) >= 1)
 		luaL_error(L, "box.error.last(): bad arguments");
 
-	Exception *e = fiber()->exception;
+	Exception *e = diag_last_error(&fiber()->diag);
 
 	if (e == NULL) {
 		lua_pushnil(L);
 	} else {
 		lua_newtable(L);
 
-		lua_pushstring(L, "message");
-		lua_pushstring(L, e->errmsg());
-		lua_settable(L, -3);
-
 		lua_pushstring(L, "type");
-		lua_pushstring(L, e->type());
-		lua_settable(L, -3);
-
-		lua_pushstring(L, "code");
-		lua_pushinteger(L, ClientError::get_errcode(e));
+		lua_pushstring(L, e->type->name);
 		lua_settable(L, -3);
 
-		if (SystemError *se = dynamic_cast<SystemError *>(e)) {
-			lua_pushstring(L, "errno");
-			lua_pushinteger(L, se->errnum());
-			lua_settable(L, -3);
+		type_foreach_method(e->type, method) {
+			if (method_invokable<const char *>(method, e)) {
+				const char *s = method_invoke<const char *>(method, e);
+				lua_pushstring(L, method->name);
+				lua_pushstring(L, s);
+				lua_settable(L, -3);
+			} else if (method_invokable<int>(method, e)) {
+				int code = method_invoke<int>(method, e);
+				lua_pushstring(L, method->name);
+				lua_pushinteger(L, code);
+				lua_settable(L, -3);
+			}
 		}
        }
        return 1;
@@ -145,7 +155,7 @@ lbox_error_clear(lua_State *L)
 	if (lua_gettop(L) >= 1)
 		luaL_error(L, "box.error.clear(): bad arguments");
 
-	Exception::clear(&fiber()->exception);
+	diag_clear(&fiber()->diag);
 	return 0;
 }
 
diff --git a/src/box/lua/error.h b/src/box/lua/error.h
index f0dc5895f9834700a901211f522b3ec62afbd12c..9661f4e147194576579296e65836d81f03ce6fc2 100644
--- a/src/box/lua/error.h
+++ b/src/box/lua/error.h
@@ -34,4 +34,7 @@ struct lua_State;
 void
 box_lua_error_init(struct lua_State *L);
 
+int
+lbox_error(struct lua_State *L);
+
 #endif /* INCLUDES_TARANTOOL_LUA_ERROR_H */
diff --git a/src/box/lua/index.cc b/src/box/lua/index.cc
index 7555537c554a2e2add560b41f6e2d08e7cd6360f..b81fa2a96314d35d86ca3ff5b1bc5f31b0c15e54 100644
--- a/src/box/lua/index.cc
+++ b/src/box/lua/index.cc
@@ -28,17 +28,22 @@
  */
 #include "box/lua/index.h"
 #include "lua/utils.h"
+#include "lua/msgpack.h"
 #include "box/index.h"
 #include "box/space.h"
 #include "box/schema.h"
 #include "box/user_def.h"
 #include "box/tuple.h"
+#include "box/lua/error.h"
 #include "box/lua/tuple.h"
 #include "fiber.h"
+#include "iobuf.h"
 
 /** {{{ box.index Lua library: access to spaces and indexes
  */
 
+static int CTID_STRUCT_ITERATOR_REF = 0;
+
 static inline Index *
 check_index(uint32_t space_id, uint32_t index_id)
 {
@@ -67,21 +72,69 @@ boxffi_index_len(uint32_t space_id, uint32_t index_id)
 	}
 }
 
+static inline int
+lbox_returntuple(lua_State *L, struct tuple *tuple)
+{
+	if (tuple == (struct tuple *) -1) {
+		return lbox_error(L);
+	} else if (tuple == NULL) {
+		lua_pushnil(L);
+		return 1;
+	} else {
+		lbox_pushtuple_noref(L, tuple);
+		return 1;
+	}
+}
+
+static inline struct tuple *
+boxffi_returntuple(struct tuple *tuple)
+{
+	if (tuple == NULL)
+		return NULL;
+	tuple_ref(tuple);
+	return tuple;
+}
+
+static inline char *
+lbox_tokey(lua_State *L, int idx)
+{
+	struct region *gc = &fiber()->gc;
+	size_t used = region_used(gc);
+	struct mpstream stream;
+	mpstream_init(&stream, gc, region_reserve_cb, region_alloc_cb);
+	luamp_encode_tuple(L, luaL_msgpack_default, &stream, idx);
+	mpstream_flush(&stream);
+	size_t key_len = region_used(gc) - used;
+	return (char *) region_join(gc, key_len);
+}
+
 struct tuple *
 boxffi_index_random(uint32_t space_id, uint32_t index_id, uint32_t rnd)
 {
 	try {
 		Index *index = check_index(space_id, index_id);
 		struct tuple *tuple = index->random(rnd);
-		if (tuple == NULL)
-			return NULL;
-		tuple_ref(tuple); /* must not throw in this case */
-		return tuple;
+		return boxffi_returntuple(tuple);
 	}  catch (Exception *) {
 		return (struct tuple *) -1; /* handled by box.error() in Lua */
 	}
 }
 
+static int
+lbox_index_random(lua_State *L)
+{
+	if (lua_gettop(L) != 3 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
+	    !lua_isnumber(L, 3))
+		return luaL_error(L, "Usage index.random(space_id, index_id, rnd)");
+
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	uint32_t rnd = lua_tointeger(L, 3);
+
+	struct tuple *tuple = boxffi_index_random(space_id, index_id, rnd);
+	return lbox_returntuple(L, tuple);
+}
+
 struct tuple *
 boxffi_index_get(uint32_t space_id, uint32_t index_id, const char *key)
 {
@@ -92,15 +145,27 @@ boxffi_index_get(uint32_t space_id, uint32_t index_id, const char *key)
 		uint32_t part_count = key ? mp_decode_array(&key) : 0;
 		primary_key_validate(index->key_def, key, part_count);
 		struct tuple *tuple = index->findByKey(key, part_count);
-		if (tuple == NULL)
-			return NULL;
-		tuple_ref(tuple); /* must not throw in this case */
-		return tuple;
+		return boxffi_returntuple(tuple);
 	}  catch (Exception *) {
 		return (struct tuple *) -1; /* handled by box.error() in Lua */
 	}
 }
 
+static int
+lbox_index_get(lua_State *L)
+{
+	if (lua_gettop(L) != 3 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2))
+		return luaL_error(L, "Usage index.get(space_id, index_id, key)");
+
+	RegionGuard region_guard(&fiber()->gc);
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	const char *key = lbox_tokey(L, 3);
+
+	struct tuple *tuple = boxffi_index_get(space_id, index_id, key);
+	return lbox_returntuple(L, tuple);
+}
+
 struct tuple *
 boxffi_index_min(uint32_t space_id, uint32_t index_id, const char *key)
 {
@@ -115,15 +180,27 @@ boxffi_index_min(uint32_t space_id, uint32_t index_id, const char *key)
 		uint32_t part_count = key ? mp_decode_array(&key) : 0;
 		key_validate(index->key_def, ITER_GE, key, part_count);
 		struct tuple *tuple = index->min(key, part_count);
-		if (tuple == NULL)
-			return NULL;
-		tuple_ref(tuple); /* must not throw in this case */
-		return tuple;
+		return boxffi_returntuple(tuple);
 	}  catch (Exception *) {
 		return (struct tuple *) -1; /* handled by box.error() in Lua */
 	}
 }
 
+static int
+lbox_index_min(lua_State *L)
+{
+	if (lua_gettop(L) != 3 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2))
+		return luaL_error(L, "usage index.min(space_id, index_id, key)");
+
+	RegionGuard region_guard(&fiber()->gc);
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	const char *key = lbox_tokey(L, 3);
+
+	struct tuple *tuple = boxffi_index_min(space_id, index_id, key);
+	return lbox_returntuple(L, tuple);
+}
+
 struct tuple *
 boxffi_index_max(uint32_t space_id, uint32_t index_id, const char *key)
 {
@@ -138,15 +215,27 @@ boxffi_index_max(uint32_t space_id, uint32_t index_id, const char *key)
 		uint32_t part_count = key ? mp_decode_array(&key) : 0;
 		key_validate(index->key_def, ITER_LE, key, part_count);
 		struct tuple *tuple = index->max(key, part_count);
-		if (tuple == NULL)
-			return NULL;
-		tuple_ref(tuple); /* must not throw in this case */
-		return tuple;
+		return boxffi_returntuple(tuple);
 	}  catch (Exception *) {
 		return (struct tuple *) -1; /* handled by box.error() in Lua */
 	}
 }
 
+static int
+lbox_index_max(lua_State *L)
+{
+	if (lua_gettop(L) != 3 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2))
+		return luaL_error(L, "usage index.max(space_id, index_id, key)");
+
+	RegionGuard region_guard(&fiber()->gc);
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	const char *key = lbox_tokey(L, 3);
+
+	struct tuple *tuple = boxffi_index_max(space_id, index_id, key);
+	return lbox_returntuple(L, tuple);
+}
+
 ssize_t
 boxffi_index_count(uint32_t space_id, uint32_t index_id, int type, const char *key)
 {
@@ -160,6 +249,28 @@ boxffi_index_count(uint32_t space_id, uint32_t index_id, int type, const char *k
 		return -1; /* handled by box.error() in Lua */
 	}
 }
+static int
+lbox_index_count(lua_State *L)
+{
+	if (lua_gettop(L) != 4 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
+	    !lua_isnumber(L, 3)) {
+		return luaL_error(L, "usage index.count(space_id, index_id, "
+		       "iterator, key)");
+	}
+
+	RegionGuard region_guard(&fiber()->gc);
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	uint32_t iterator = lua_tointeger(L, 3);
+	const char *key = lbox_tokey(L, 4);
+
+	ssize_t count = boxffi_index_count(space_id, index_id,
+		iterator, key);
+	if (count == -1)
+		return lbox_error(L);
+	lua_pushinteger(L, count);
+	return 1;
+}
 
 static void
 box_index_init_iterator_types(struct lua_State *L, int idx)
@@ -197,11 +308,35 @@ boxffi_index_iterator(uint32_t space_id, uint32_t index_id, int type,
 	} catch (Exception *) {
 		if (it)
 			it->free(it);
-		/* will be hanled by box.error() in Lua */
+		/* will be handled by box.error() in Lua */
 		return NULL;
 	}
 }
 
+static int
+lbox_index_iterator(lua_State *L)
+{
+	if (lua_gettop(L) != 4 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2) ||
+	    !lua_isnumber(L, 3))
+		return luaL_error(L, "usage index.iterator(space_id, index_id, type, key)");
+
+	uint32_t space_id = lua_tointeger(L, 1);
+	uint32_t index_id = lua_tointeger(L, 2);
+	uint32_t iterator = lua_tointeger(L, 3);
+	/* const char *key = lbox_tokey(L, 4); */
+	const char *mpkey = lua_tolstring(L, 4, NULL); /* Key encoded by Lua */
+	struct iterator *it = boxffi_index_iterator(space_id, index_id,
+		iterator, mpkey);
+	if (it == NULL)
+		return lbox_error(L);
+
+	assert(CTID_STRUCT_ITERATOR_REF != 0);
+	struct iterator **ptr = (struct iterator **) luaL_pushcdata(L,
+		CTID_STRUCT_ITERATOR_REF, sizeof(struct iterator *));
+	*ptr = it; /* NULL handled by Lua, gc also set by Lua */
+	return 1;
+}
+
 struct tuple*
 boxffi_iterator_next(struct iterator *itr)
 {
@@ -228,11 +363,36 @@ boxffi_iterator_next(struct iterator *itr)
 	}
 }
 
+static int
+lbox_iterator_next(lua_State *L)
+{
+	/* first argument is key buffer */
+	if (lua_gettop(L) < 1 || lua_type(L, 1) != LUA_TCDATA)
+		return luaL_error(L, "usage: next(state)");
+
+	assert(CTID_STRUCT_ITERATOR_REF != 0);
+	uint32_t ctypeid;
+	void *data = luaL_checkcdata(L, 1, &ctypeid);
+	if (ctypeid != CTID_STRUCT_ITERATOR_REF)
+		return luaL_error(L, "usage: next(state)");
+
+	struct iterator *itr = *(struct iterator **) data;
+	struct tuple *tuple = boxffi_iterator_next(itr);
+	return lbox_returntuple(L, tuple);
+}
+
 /* }}} */
 
 void
 box_lua_index_init(struct lua_State *L)
 {
+	/* Get CTypeIDs */
+	int rc = luaL_cdef(L, "struct iterator;");
+	assert(rc == 0);
+	(void) rc;
+	CTID_STRUCT_ITERATOR_REF = luaL_ctypeid(L, "struct iterator&");
+	assert(CTID_STRUCT_ITERATOR_REF != 0);
+
 	static const struct luaL_reg indexlib [] = {
 		{NULL, NULL}
 	};
@@ -241,4 +401,18 @@ box_lua_index_init(struct lua_State *L)
 	luaL_register_module(L, "box.index", indexlib);
 	box_index_init_iterator_types(L, -2);
 	lua_pop(L, 1);
+
+	static const struct luaL_reg boxlib_internal[] = {
+		{"random", lbox_index_random},
+		{"get",  lbox_index_get},
+		{"min", lbox_index_min},
+		{"max", lbox_index_max},
+		{"count", lbox_index_count},
+		{"iterator", lbox_index_iterator},
+		{"iterator_next", lbox_iterator_next},
+		{NULL, NULL}
+	};
+
+	luaL_register(L, "box.internal", boxlib_internal);
+	lua_pop(L, 1);
 }
diff --git a/src/box/lua/info.cc b/src/box/lua/info.cc
index 484b76e15efa6996610d38f711a255bfc212c3f5..165c487051dd161bac8dbcfd3b7d049f93b0b122 100644
--- a/src/box/lua/info.cc
+++ b/src/box/lua/info.cc
@@ -63,10 +63,10 @@ lbox_info_replication(struct lua_State *L)
 		lua_pushnumber(L, ev_now(loop()) - r->remote.last_row_time);
 		lua_settable(L, -3);
 
-		if (r->remote.reader->exception) {
+		Exception *e = diag_last_error(&r->remote.reader->diag);
+		if (e != NULL) {
 			lua_pushstring(L, "message");
-			lua_pushstring(L,
-				       r->remote.reader->exception->errmsg());
+			lua_pushstring(L, e->errmsg());
 			lua_settable(L, -3);
 		}
 	}
@@ -125,13 +125,6 @@ lbox_info_uptime(struct lua_State *L)
 	return 1;
 }
 
-static int
-lbox_info_snapshot_pid(struct lua_State *L)
-{
-	lua_pushnumber(L, snapshot_pid);
-	return 1;
-}
-
 static int
 lbox_info_pid(struct lua_State *L)
 {
@@ -171,7 +164,6 @@ lbox_info_dynamic_meta [] =
 	{"replication", lbox_info_replication},
 	{"status", lbox_info_status},
 	{"uptime", lbox_info_uptime},
-	{"snapshot_pid", lbox_info_snapshot_pid},
 	{"pid", lbox_info_pid},
 #if 0
 	{"sophia", lbox_info_sophia},
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index a309112173226b02ef1d25977d7d6494adcaeb8b..076d607b49b36efd7b1cdd40d28b24784a0fe18e 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -1,6 +1,7 @@
 -- schema.lua (internal file)
 --
 local ffi = require('ffi')
+local msgpack = require('msgpack')
 local msgpackffi = require('msgpackffi')
 local fun = require('fun')
 local session = box.session
@@ -41,22 +42,35 @@ ffi.cdef[[
     struct tuple *
     boxffi_iterator_next(struct iterator *itr);
 
-    struct port;
-    struct port_ffi
+    struct port
     {
         struct port_vtab *vtab;
-        uint32_t size;
-        uint32_t capacity;
-        struct tuple **ret;
     };
 
+    struct port_buf_entry {
+        struct port_buf_entry *next;
+        struct tuple *tuple;
+    };
+
+    struct port_buf {
+        struct port base;
+        size_t size;
+        struct port_buf_entry *first;
+        struct port_buf_entry *last;
+        struct port_buf_entry first_entry;
+    };
+
+    void
+    port_buf_create(struct port_buf *port_buf);
+
     void
-    port_ffi_create(struct port_ffi *port);
+    port_buf_destroy(struct port_buf *port_buf);
+
     void
-    port_ffi_destroy(struct port_ffi *port);
+    port_buf_transfer(struct port_buf *port_buf);
 
     int
-    boxffi_select(struct port_ffi *port, uint32_t space_id, uint32_t index_id,
+    boxffi_select(struct port_buf *port, uint32_t space_id, uint32_t index_id,
               int iterator, uint32_t offset, uint32_t limit,
               const char *key, const char *key_end);
     void password_prepare(const char *password, int len,
@@ -524,7 +538,7 @@ local iterator_gen = function(param, state)
         information.
     --]]
     if not ffi.istype(iterator_t, state) then
-        error('usage gen(param, state)')
+        error('usage: next(param, state)')
     end
     -- next() modifies state in-place
     local tuple = builtin.boxffi_iterator_next(state)
@@ -537,14 +551,22 @@ local iterator_gen = function(param, state)
     end
 end
 
+local iterator_gen_luac = function(param, state)
+    local tuple = internal.iterator_next(state)
+    if tuple ~= nil then
+        return state, tuple -- new state, value
+    else
+        return nil
+    end
+end
+
 local iterator_cdata_gc = function(iterator)
     return iterator.free(iterator)
 end
 
 -- global struct port instance to use by select()/get()
-local port = ffi.new('struct port_ffi')
-builtin.port_ffi_create(port)
-ffi.gc(port, builtin.port_ffi_destroy)
+local port_buf = ffi.new('struct port_buf')
+local port_buf_entry_t = ffi.typeof('struct port_buf_entry')
 
 -- Helper function for nicer error messages
 -- in some cases when space object is misused
@@ -604,7 +626,7 @@ function box.schema.space.bless(space)
         return error('Attempt to modify a read-only table') end
     index_mt.__index = index_mt
     -- min and max
-    index_mt.min = function(index, key)
+    index_mt.min_ffi = function(index, key)
         local pkey = msgpackffi.encode_tuple(key)
         local tuple = builtin.boxffi_index_min(index.space_id, index.id, pkey)
         if tuple == ffi.cast('void *', -1) then
@@ -615,7 +637,11 @@ function box.schema.space.bless(space)
             return
         end
     end
-    index_mt.max = function(index, key)
+    index_mt.min_luac = function(index, key)
+        key = keify(key)
+        return internal.min(index.space_id, index.id, key);
+    end
+    index_mt.max_ffi = function(index, key)
         local pkey = msgpackffi.encode_tuple(key)
         local tuple = builtin.boxffi_index_max(index.space_id, index.id, pkey)
         if tuple == ffi.cast('void *', -1) then
@@ -626,7 +652,11 @@ function box.schema.space.bless(space)
             return
         end
     end
-    index_mt.random = function(index, rnd)
+    index_mt.max_luac = function(index, key)
+        key = keify(key)
+        return internal.max(index.space_id, index.id, key);
+    end
+    index_mt.random_ffi = function(index, rnd)
         rnd = rnd or math.random()
         local tuple = builtin.boxffi_index_random(index.space_id, index.id, rnd)
         if tuple == ffi.cast('void *', -1) then
@@ -637,8 +667,12 @@ function box.schema.space.bless(space)
             return
         end
     end
+    index_mt.random_luac = function(index, rnd)
+        rnd = rnd or math.random()
+        return internal.random(index.space_id, index.id, rnd);
+    end
     -- iteration
-    index_mt.pairs = function(index, key, opts)
+    index_mt.pairs_ffi = function(index, key, opts)
         local pkey, pkey_end = msgpackffi.encode_tuple(key)
         local itype = check_iterator_type(opts, pkey + 1 >= pkey_end);
 
@@ -648,13 +682,20 @@ function box.schema.space.bless(space)
         if cdata == nil then
             box.error()
         end
-
         return fun.wrap(iterator_gen, keybuf, ffi.gc(cdata, iterator_cdata_gc))
     end
-    index_mt.__pairs = index_mt.pairs -- Lua 5.2 compatibility
-    index_mt.__ipairs = index_mt.pairs -- Lua 5.2 compatibility
+    index_mt.pairs_luac = function(index, key, opts)
+        key = keify(key)
+        local itype = check_iterator_type(opts, #key == 0);
+        local keymp = msgpack.encode(key)
+        local keybuf = ffi.string(keymp, #keymp)
+        local cdata = internal.iterator(index.space_id, index.id, itype, keymp);
+        return fun.wrap(iterator_gen_luac, keybuf,
+            ffi.gc(cdata, iterator_cdata_gc))
+    end
+
     -- index subtree size
-    index_mt.count = function(index, key, opts)
+    index_mt.count_ffi = function(index, key, opts)
         local pkey, pkey_end = msgpackffi.encode_tuple(key)
         local itype = check_iterator_type(opts, pkey + 1 >= pkey_end);
         local count = builtin.boxffi_index_count(index.space_id, index.id,
@@ -664,6 +705,11 @@ function box.schema.space.bless(space)
         end
         return tonumber(count)
     end
+    index_mt.count_luac = function(index, key, opts)
+        key = keify(key)
+        local itype = check_iterator_type(opts, #key == 0);
+        return internal.count(index.space_id, index.id, itype, key);
+    end
 
     local function check_index(space, index_id)
         if space.index[index_id] == nil then
@@ -671,7 +717,7 @@ function box.schema.space.bless(space)
         end
     end
 
-    index_mt.get = function(index, key)
+    index_mt.get_ffi = function(index, key)
         local key, key_end = msgpackffi.encode_tuple(key)
         local tuple = builtin.boxffi_index_get(index.space_id, index.id, key)
         if tuple == ffi.cast('void *', -1) then
@@ -682,14 +728,15 @@ function box.schema.space.bless(space)
             return
         end
     end
+    index_mt.get_luac = function(index, key)
+        key = keify(key)
+        return internal.get(index.space_id, index.id, key)
+    end
 
-    index_mt.select = function(index, key, opts)
+    local function check_select_opts(opts, key_is_nil)
         local offset = 0
         local limit = 4294967295
-
-        local key, key_end = msgpackffi.encode_tuple(key)
-        local iterator = check_iterator_type(opts, key + 1 >= key_end)
-
+        local iterator = check_iterator_type(opts, key_is_nil)
         if opts ~= nil then
             if opts.offset ~= nil then
                 offset = opts.offset
@@ -698,19 +745,40 @@ function box.schema.space.bless(space)
                 limit = opts.limit
             end
         end
+        return iterator, offset, limit
+    end
+
+    index_mt.select_ffi = function(index, key, opts)
+        local key, key_end = msgpackffi.encode_tuple(key)
+        local iterator, offset, limit = check_select_opts(opts, key + 1 >= key_end)
 
-        if builtin.boxffi_select(port, index.space_id,
+        builtin.port_buf_create(port_buf)
+        if builtin.boxffi_select(port_buf, index.space_id,
             index.id, iterator, offset, limit, key, key_end) ~=0 then
+            builtin.port_buf_destroy(port_buf);
             return box.error()
         end
 
         local ret = {}
-        for i=0,port.size - 1,1 do
+        local entry = port_buf.first
+        local i = 1
+        while entry ~= nil do
             -- tuple.bless must never fail
-            ret[i + 1] = box.tuple.bless(port.ret[i])
+            ret[i] = box.tuple.bless(entry.tuple)
+            entry = entry.next
+            i = i + 1
         end
+        builtin.port_buf_transfer(port_buf);
         return ret
     end
+
+    index_mt.select_luac = function(index, key, opts)
+        local key = keify(key)
+        local iterator, offset, limit = check_select_opts(opts, #key == 0)
+        return internal.select(index.space_id, index.id, iterator,
+            offset, limit, key)
+    end
+
     index_mt.update = function(index, key, ops)
         return internal.update(index.space_id, index.id, keify(key), ops);
     end
@@ -733,6 +801,21 @@ function box.schema.space.bless(space)
         end
         return box.schema.index.alter(index.space_id, index.id, options)
     end
+
+    -- true if reading operations may yield
+    local read_yields = space.engine == 'sophia'
+    local read_ops = {'select', 'get', 'min', 'max', 'count', 'random', 'pairs'}
+    for _, op in ipairs(read_ops) do
+        if read_yields then
+            -- use Lua/C implmenetation
+            index_mt[op] = index_mt[op .. "_luac"]
+        else
+            -- use FFI implementation
+            index_mt[op] = index_mt[op .. "_ffi"]
+        end
+    end
+    index_mt.__pairs = index_mt.pairs -- Lua 5.2 compatibility
+    index_mt.__ipairs = index_mt.pairs -- Lua 5.2 compatibility
     --
     local space_mt = {}
     space_mt.len = function(space)
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index e9c22b280bee7756f163e7555d94a017f445797f..6fa40dde687c1a58ec942ebcee86bca2d4a648c2 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -54,8 +54,16 @@ lbox_space_on_replace_trigger(struct trigger *trigger, void *event)
 
 	lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) trigger->data);
 
-	lbox_pushtuple(L, stmt->old_tuple);
-	lbox_pushtuple(L, stmt->new_tuple);
+	if (stmt->old_tuple) {
+		lbox_pushtuple(L, stmt->old_tuple);
+	} else {
+		lua_pushnil(L);
+	}
+	if (stmt->new_tuple) {
+		lbox_pushtuple(L, stmt->new_tuple);
+	} else {
+		lua_pushnil(L);
+	}
 	/* @todo: maybe the space object has to be here */
 	lua_pushstring(L, stmt->space->def.name);
 
diff --git a/src/box/lua/tuple.cc b/src/box/lua/tuple.cc
index 5c021ab9a690a5ddd3594048745267deccb4b71c..323cc18673d264dcc53b8ff870e2aedccdd29bad 100644
--- a/src/box/lua/tuple.cc
+++ b/src/box/lua/tuple.cc
@@ -31,7 +31,6 @@
 #include "box/tuple.h"
 #include "box/tuple_update.h"
 #include "fiber.h"
-#include "iobuf.h"
 #include "lua/utils.h"
 #include "lua/msgpack.h"
 #include "third_party/lua-yaml/lyaml.h"
@@ -98,23 +97,26 @@ lbox_tuple_new(lua_State *L)
 		++argc;
 	}
 
-	RegionGuard region_guard(&fiber()->gc);
-	struct obuf buf;
-	obuf_create(&buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
+	struct region *gc = &fiber()->gc;
+	RegionGuard guard(gc);
+	struct mpstream stream;
+	mpstream_init(&stream, gc, region_reserve_cb, region_alloc_cb);
+
 	if (argc == 1 && (lua_istable(L, 1) || lua_istuple(L, 1))) {
 		/* New format: box.tuple.new({1, 2, 3}) */
-		luamp_encode_tuple(L, luaL_msgpack_default, &buf, 1);
+		luamp_encode_tuple(L, luaL_msgpack_default, &stream, 1);
 	} else {
 		/* Backward-compatible format: box.tuple.new(1, 2, 3). */
-		luamp_encode_array(luaL_msgpack_default, &buf, argc);
+		luamp_encode_array(luaL_msgpack_default, &stream, argc);
 		for (int k = 1; k <= argc; ++k) {
-			luamp_encode(L, luaL_msgpack_default, &buf, k);
+			luamp_encode(L, luaL_msgpack_default, &stream, k);
 		}
 	}
+	mpstream_flush(&stream);
 
-	const char *data = obuf_join(&buf);
-	struct tuple *tuple = tuple_new(tuple_format_ber, data,
-					data + obuf_size(&buf));
+	size_t tuple_len = region_used(gc) - guard.used;
+	const char *data = (char *) region_join(gc, tuple_len);
+	struct tuple *tuple = tuple_new(tuple_format_ber, data, data + tuple_len);
 	lbox_pushtuple(L, tuple);
 	return 1;
 }
@@ -186,11 +188,14 @@ lbox_tuple_slice(struct lua_State *L)
 
 /* A MsgPack extensions handler that supports tuples */
 static mp_type
-luamp_encode_extension_box(struct lua_State *L, int idx, struct obuf *b)
+luamp_encode_extension_box(struct lua_State *L, int idx,
+			   struct mpstream *stream)
 {
 	struct tuple *tuple = lua_istuple(L, idx);
 	if (tuple != NULL) {
-		tuple_to_obuf(tuple, b);
+		char *ptr = mpstream_reserve(stream, tuple->bsize);
+		tuple_to_buf(tuple, ptr);
+		mpstream_advance(stream, tuple->bsize);
 		return MP_ARRAY;
 	}
 
@@ -199,18 +204,12 @@ luamp_encode_extension_box(struct lua_State *L, int idx, struct obuf *b)
 
 void
 luamp_encode_tuple(struct lua_State *L, struct luaL_serializer *cfg,
-		  struct obuf *b, int index)
+		   struct mpstream *stream, int index)
 {
-	if (luamp_encode(L, cfg, b, index) != MP_ARRAY)
+	if (luamp_encode(L, cfg, stream, index) != MP_ARRAY)
 		tnt_raise(ClientError, ER_TUPLE_NOT_ARRAY);
 }
 
-static void *
-tuple_update_region_alloc(void *alloc_ctx, size_t size)
-{
-	return region_alloc((struct region *) alloc_ctx, size);
-}
-
 /**
  * Tuple transforming function.
  *
@@ -267,53 +266,49 @@ lbox_tuple_transform(struct lua_State *L)
 		return 1;
 	}
 
-	RegionGuard region_guard(&fiber()->gc);
-
+	struct region *gc = &fiber()->gc;
+	RegionGuard guard(gc);
+	struct mpstream stream;
+	mpstream_init(&stream, gc, region_reserve_cb, region_alloc_cb);
 	/*
 	 * Prepare UPDATE expression
 	 */
-	struct obuf buf;
-	obuf_create(&buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
-	luamp_encode_array(luaL_msgpack_default, &buf, op_cnt);
+	luamp_encode_array(luaL_msgpack_default, &stream, op_cnt);
 	if (len > 0) {
-		luamp_encode_array(luaL_msgpack_default, &buf, 3);
-		luamp_encode_str(luaL_msgpack_default, &buf, "#", 1);
-		luamp_encode_uint(luaL_msgpack_default, &buf, offset);
-		luamp_encode_uint(luaL_msgpack_default, &buf, len);
+		luamp_encode_array(luaL_msgpack_default, &stream, 3);
+		luamp_encode_str(luaL_msgpack_default, &stream, "#", 1);
+		luamp_encode_uint(luaL_msgpack_default, &stream, offset);
+		luamp_encode_uint(luaL_msgpack_default, &stream, len);
 	}
 
 	for (int i = argc ; i > 3; i--) {
-		luamp_encode_array(luaL_msgpack_default, &buf, 3);
-		luamp_encode_str(luaL_msgpack_default, &buf, "!", 1);
-		luamp_encode_uint(luaL_msgpack_default, &buf, offset);
-		luamp_encode(L, luaL_msgpack_default, &buf, i);
+		luamp_encode_array(luaL_msgpack_default, &stream, 3);
+		luamp_encode_str(luaL_msgpack_default, &stream, "!", 1);
+		luamp_encode_uint(luaL_msgpack_default, &stream, offset);
+		luamp_encode(L, luaL_msgpack_default, &stream, i);
 	}
+	mpstream_flush(&stream);
 
 	/* Execute tuple_update */
-	const char *expr = obuf_join(&buf);
+	size_t expr_len = region_used(gc) - guard.used;
+	const char *expr = (char *) region_join(gc, expr_len);
 	struct tuple *new_tuple = tuple_update(tuple_format_ber,
-					       tuple_update_region_alloc,
-					       &fiber()->gc,
-					       tuple, expr, expr + obuf_size(&buf),
-					       0);
+					       region_alloc_cb,
+					       gc, tuple, expr,
+					       expr + expr_len, 0);
 	lbox_pushtuple(L, new_tuple);
 	return 1;
 }
 
 void
-lbox_pushtuple(struct lua_State *L, struct tuple *tuple)
+lbox_pushtuple_noref(struct lua_State *L, struct tuple *tuple)
 {
-	if (tuple) {
-		assert(CTID_CONST_STRUCT_TUPLE_REF != 0);
-		struct tuple **ptr = (struct tuple **) luaL_pushcdata(L,
-			CTID_CONST_STRUCT_TUPLE_REF, sizeof(struct tuple *));
-		*ptr = tuple;
-		lua_pushcfunction(L, lbox_tuple_gc);
-		luaL_setcdatagc(L, -2);
-		tuple_ref(tuple);
-	} else {
-		return lua_pushnil(L);
-	}
+	assert(CTID_CONST_STRUCT_TUPLE_REF != 0);
+	struct tuple **ptr = (struct tuple **) luaL_pushcdata(L,
+		CTID_CONST_STRUCT_TUPLE_REF, sizeof(struct tuple *));
+	*ptr = tuple;
+	lua_pushcfunction(L, lbox_tuple_gc);
+	luaL_setcdatagc(L, -2);
 }
 
 static const struct luaL_reg lbox_tuple_meta[] = {
diff --git a/src/box/lua/tuple.h b/src/box/lua/tuple.h
index 2e182272e1425a0e16e0e3b6d793c63472863f07..960278d12085d535d061ac78117d8802713664e7 100644
--- a/src/box/lua/tuple.h
+++ b/src/box/lua/tuple.h
@@ -28,6 +28,9 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
+
+#include <box/tuple.h>
+
 struct lua_State;
 struct txn;
 struct tuple;
@@ -36,13 +39,21 @@ struct tuple;
  * Push tuple on lua stack
  */
 void
-lbox_pushtuple(struct lua_State *L, struct tuple *tuple);
+lbox_pushtuple_noref(struct lua_State *L, struct tuple *tuple);
+
+static inline void
+lbox_pushtuple(struct lua_State *L, struct tuple *tuple)
+{
+	assert(tuple != NULL);
+	lbox_pushtuple_noref(L, tuple);
+	tuple_ref(tuple);
+}
 
 struct tuple *lua_istuple(struct lua_State *L, int narg);
 
 void
 luamp_encode_tuple(struct lua_State *L, struct luaL_serializer *cfg,
-		  struct obuf *b, int index);
+		  struct mpstream *stream, int index);
 
 void
 box_lua_tuple_init(struct lua_State *L);
diff --git a/src/box/lua/tuple.lua b/src/box/lua/tuple.lua
index a61e466b47ca53274d7ce42affa33ff626afecc1..7a87974f67f41f159bc32bbd2b203d9f0083d482 100644
--- a/src/box/lua/tuple.lua
+++ b/src/box/lua/tuple.lua
@@ -169,9 +169,8 @@ end
 
 -- Set encode hooks for msgpackffi
 local function tuple_to_msgpack(buf, tuple)
-    buf:reserve(tuple._bsize)
-    builtin.tuple_to_buf(tuple, buf.p)
-    buf.p = buf.p + tuple._bsize
+    local data = buf:alloc(tuple._bsize)
+    builtin.tuple_to_buf(tuple, data)
 end
 
 msgpackffi.on_encode(ffi.typeof('const struct tuple &'), tuple_to_msgpack)
diff --git a/src/box/memtx_engine.cc b/src/box/memtx_engine.cc
index 8647dadd432dec12f366249e0c13b2e262705215..69c77eb26154a9c392c29655a350df5563f122b0 100644
--- a/src/box/memtx_engine.cc
+++ b/src/box/memtx_engine.cc
@@ -27,7 +27,6 @@
  * SUCH DAMAGE.
  */
 #include "memtx_engine.h"
-#include "replication.h"
 #include "tuple.h"
 #include "txn.h"
 #include "index.h"
@@ -36,19 +35,16 @@
 #include "memtx_rtree.h"
 #include "memtx_bitset.h"
 #include "space.h"
-#include "salad/rlist.h"
 #include "request.h"
-#include <stdlib.h>
-#include <string.h>
-#include <sys/wait.h>
 #include "box.h"
 #include "iproto_constants.h"
 #include "xrow.h"
 #include "recovery.h"
+#include "relay.h"
 #include "schema.h"
 #include "main.h"
 #include "coeio_file.h"
-#include "coio.h"
+#include "coeio.h"
 #include "errinj.h"
 #include "scoped_guard.h"
 
@@ -58,6 +54,26 @@ static bool memtx_index_arena_initialized = false;
 static struct slab_arena memtx_index_arena;
 static struct slab_cache memtx_index_arena_slab_cache;
 static struct mempool memtx_index_extent_pool;
+/**
+ * To ensure proper statement-level rollback in case
+ * of out of memory conditions, we maintain a number
+ * of slack memory extents reserved before a statement
+ * is begun. If there isn't enough slack memory,
+ * we don't begin the statement.
+ */
+static int memtx_index_num_reserved_extents;
+static void *memtx_index_reserved_extents;
+
+enum {
+	/**
+	 * This number is calculated based on the
+	 * max (realistic) number of insertions
+	 * a deletion from a B-tree or an R-tree
+	 * can lead to, and, as a result, the max
+	 * number of new block allocations.
+	 */
+	RESERVE_EXTENTS_BEFORE_REPLACE = 16
+};
 
 /**
  * A version of space_replace for a space which has
@@ -158,6 +174,11 @@ memtx_replace_all_keys(struct txn *txn, struct space *space,
 		       struct tuple *old_tuple, struct tuple *new_tuple,
 		       enum dup_replace_mode mode)
 {
+	/*
+	 * Ensure we have enough slack memory to guarantee
+	 * successful statement-level rollback.
+	 */
+	memtx_index_extent_reserve(RESERVE_EXTENTS_BEFORE_REPLACE);
 	uint32_t i = 0;
 	try {
 		/* Update the primary key */
@@ -234,8 +255,7 @@ memtx_build_secondary_keys(struct space *space, void *param)
 
 MemtxEngine::MemtxEngine()
 	:Engine("memtx"),
-	m_checkpoint_id(-1),
-	m_snapshot_pid(0),
+	m_checkpoint(0),
 	m_state(MEMTX_INITIALIZED)
 {
 	flags = ENGINE_CAN_BE_TEMPORARY |
@@ -557,8 +577,8 @@ MemtxEngine::beginJoin()
 }
 
 static void
-snapshot_write_row(struct recovery_state *r, struct xlog *l,
-		   struct xrow_header *row)
+checkpoint_write_row(struct xlog *l, struct xrow_header *row,
+		     uint64_t snap_io_rate_limit)
 {
 	static uint64_t bytes;
 	ev_tstamp elapsed;
@@ -583,9 +603,9 @@ snapshot_write_row(struct recovery_state *r, struct xlog *l,
 	/* TODO: use writev here */
 	for (int i = 0; i < iovcnt; i++) {
 		if (fwrite(iov[i].iov_base, iov[i].iov_len, 1, l->f) != 1) {
-			say_error("Can't write row (%zu bytes)",
-				  iov[i].iov_len);
-			panic_syserror("snapshot_write_row");
+			say_syserror("Can't write row (%zu bytes)",
+				     iov[i].iov_len);
+			tnt_raise(SystemError, "fwrite");
 		}
 		bytes += iov[i].iov_len;
 	}
@@ -595,7 +615,7 @@ snapshot_write_row(struct recovery_state *r, struct xlog *l,
 
 	fiber_gc();
 
-	if (r->snap_io_rate_limit != UINT64_MAX) {
+	if (snap_io_rate_limit != UINT64_MAX) {
 		if (last == 0) {
 			/*
 			 * Remember the time of first
@@ -609,10 +629,10 @@ snapshot_write_row(struct recovery_state *r, struct xlog *l,
 		 * filesystem cache, otherwise the limit is
 		 * not really enforced.
 		 */
-		if (bytes > r->snap_io_rate_limit)
+		if (bytes > snap_io_rate_limit)
 			fdatasync(fileno(l->f));
 	}
-	while (bytes > r->snap_io_rate_limit) {
+	while (bytes > snap_io_rate_limit) {
 		ev_now_update(loop);
 		/*
 		 * How much time have passed since
@@ -629,13 +649,13 @@ snapshot_write_row(struct recovery_state *r, struct xlog *l,
 
 		ev_now_update(loop);
 		last = ev_now(loop);
-		bytes -= r->snap_io_rate_limit;
+		bytes -= snap_io_rate_limit;
 	}
 }
 
 static void
-snapshot_write_tuple(struct xlog *l,
-		     uint32_t n, struct tuple *tuple)
+checkpoint_write_tuple(struct xlog *l, uint32_t n, struct tuple *tuple,
+		       uint64_t snap_io_rate_limit)
 {
 	struct request_replace_body body;
 	body.m_body = 0x82; /* map of two elements. */
@@ -653,88 +673,123 @@ snapshot_write_tuple(struct xlog *l,
 	row.body[0].iov_len = sizeof(body);
 	row.body[1].iov_base = tuple->data;
 	row.body[1].iov_len = tuple->bsize;
-	snapshot_write_row(recovery, l, &row);
+	checkpoint_write_row(l, &row, snap_io_rate_limit);
+}
+
+struct checkpoint_entry {
+	struct space *space;
+	struct iterator *iterator;
+	struct rlist link;
+};
+
+struct checkpoint {
+	/**
+	 * List of MemTX spaces to snapshot, with consistent
+	 * read view iterators.
+	 */
+	struct rlist entries;
+	/** The signature of the snapshot file (lsn sum) */
+	int64_t lsn;
+	uint64_t snap_io_rate_limit;
+	struct cord cord;
+	bool waiting_for_snap_thread;
+	struct vclock vclock;
+	struct xdir dir;
+};
+
+static void
+checkpoint_init(struct checkpoint *ckpt, struct recovery_state *recovery,
+		int64_t lsn_arg)
+{
+	ckpt->entries = RLIST_HEAD_INITIALIZER(ckpt->entries);
+	ckpt->waiting_for_snap_thread = false;
+	ckpt->lsn = lsn_arg;
+	vclock_copy(&ckpt->vclock, &recovery->vclock);
+	xdir_create(&ckpt->dir, recovery->snap_dir.dirname, SNAP,
+		    &recovery->server_uuid);
+	ckpt->snap_io_rate_limit = recovery->snap_io_rate_limit;
 }
 
 static void
-snapshot_space(struct space *sp, void *udata)
+checkpoint_destroy(struct checkpoint *ckpt)
+{
+	struct checkpoint_entry *entry;
+	rlist_foreach_entry(entry, &ckpt->entries, link) {
+		Index *pk = space_index(entry->space, 0);
+		pk->destroyReadViewForIterator(entry->iterator);
+		entry->iterator->free(entry->iterator);
+	}
+	ckpt->entries = RLIST_HEAD_INITIALIZER(ckpt->entries);
+	xdir_destroy(&ckpt->dir);
+}
+
+
+static void
+checkpoint_add_space(struct space *sp, void *data)
 {
 	if (space_is_temporary(sp))
 		return;
 	if (!space_is_memtx(sp))
 		return;
-	struct tuple *tuple;
-	struct xlog *l = (struct xlog *)udata;
 	Index *pk = space_index(sp, 0);
-	if (pk == NULL)
+	if (!pk)
 		return;
-	struct iterator *it = pk->position();
-	pk->initIterator(it, ITER_ALL, NULL, 0);
+	struct checkpoint *ckpt = (struct checkpoint *)data;
+	struct checkpoint_entry *entry;
+	entry = (struct checkpoint_entry *)
+		region_alloc(&fiber()->gc, sizeof(*entry));
+	rlist_add_tail_entry(&ckpt->entries, entry, link);
 
-	while ((tuple = it->next(it)))
-		snapshot_write_tuple(l, space_id(sp), tuple);
-}
+	entry->space = sp;
+	entry->iterator = pk->allocIterator();
 
-static void
-snapshot_save(struct recovery_state *r)
+	pk->initIterator(entry->iterator, ITER_ALL, NULL, 0);
+	pk->createReadViewForIterator(entry->iterator);
+};
+
+void
+checkpoint_f(va_list ap)
 {
-	struct xlog *snap = xlog_create(&r->snap_dir, &r->vclock);
-	if (snap == NULL)
-		panic_status(errno, "failed to save snapshot: failed to open file in write mode.");
-	/*
-	 * While saving a snapshot, snapshot name is set to
-	 * <lsn>.snap.inprogress. When done, the snapshot is
-	 * renamed to <lsn>.snap.
-	 */
-	say_info("saving snapshot `%s'", snap->filename);
+	struct checkpoint *ckpt = va_arg(ap, struct checkpoint *);
 
-	space_foreach(snapshot_space, snap);
+	struct xlog *snap = xlog_create(&ckpt->dir, &ckpt->vclock);
 
-	xlog_close(snap);
+	if (snap == NULL)
+		tnt_raise(SystemError, "xlog_open");
 
+	auto guard = make_scoped_guard([=]{ xlog_close(snap); });
+
+	say_info("saving snapshot `%s'", snap->filename);
+	struct checkpoint_entry *entry;
+	rlist_foreach_entry(entry, &ckpt->entries, link) {
+		struct tuple *tuple;
+		struct iterator *it = entry->iterator;
+		for (tuple = it->next(it); tuple; tuple = it->next(it)) {
+			checkpoint_write_tuple(snap, space_id(entry->space),
+					       tuple, ckpt->snap_io_rate_limit);
+		}
+	}
 	say_info("done");
 }
 
 int
 MemtxEngine::beginCheckpoint(int64_t lsn)
 {
-	assert(m_checkpoint_id == -1);
-	assert(m_snapshot_pid == 0);
-	/* flush buffers to avoid multiple output
-	 *
-	 * https://github.com/tarantool/tarantool/issues/366
-	*/
-	fflush(stdout);
-	fflush(stderr);
-	/*
-	 * Due to fork nature, no threads are recreated.
-	 * This is the only consistency guarantee here for a
-	 * multi-threaded engine.
-	 */
-	snapshot_pid = fork();
-	switch (snapshot_pid) {
-	case -1:
-		say_syserror("fork");
+	assert(m_checkpoint == 0);
+
+	m_checkpoint = (struct checkpoint *)
+		region_alloc(&fiber()->gc, sizeof(*m_checkpoint));
+
+	checkpoint_init(m_checkpoint, ::recovery, lsn);
+	space_foreach(checkpoint_add_space, m_checkpoint);
+
+	if (cord_costart(&m_checkpoint->cord, "snapshot",
+			 checkpoint_f, m_checkpoint)) {
 		return -1;
-	case  0: /* dumper */
-		slab_arena_mprotect(&memtx_arena);
-		cord_set_name("snap");
-		title("dumper", "%" PRIu32, getppid());
-		fiber_set_name(fiber(), "dumper");
-		/* make sure we don't double-write parent stdio
-		 * buffers at exit() during panic */
-		close_all_xcpt(1, log_fd);
-		/* do not rename snapshot */
-		snapshot_save(::recovery);
-		exit(EXIT_SUCCESS);
-		return 0;
-	default: /* waiter */
-		break;
 	}
+	m_checkpoint->waiting_for_snap_thread = true;
 
-	m_checkpoint_id = lsn;
-	m_snapshot_pid = snapshot_pid;
-	/* increment snapshot version */
+	/* increment snapshot version; set tuple deletion to delayed mode */
 	tuple_begin_snapshot();
 	return 0;
 }
@@ -742,60 +797,75 @@ MemtxEngine::beginCheckpoint(int64_t lsn)
 int
 MemtxEngine::waitCheckpoint()
 {
-	assert(m_checkpoint_id >= 0);
-	assert(m_snapshot_pid > 0);
-	/* wait for memtx-part snapshot completion */
-	int rc = coio_waitpid(m_snapshot_pid);
-	if (WIFSIGNALED(rc))
-		rc = EINTR;
-	else
-		rc = WEXITSTATUS(rc);
-	/* complete snapshot */
-	tuple_end_snapshot();
-	m_snapshot_pid = snapshot_pid = 0;
-	errno = rc;
+	assert(m_checkpoint);
+	assert(m_checkpoint->waiting_for_snap_thread);
 
-	return rc;
+	int result;
+	try {
+		/* wait for memtx-part snapshot completion */
+		result = cord_cojoin(&m_checkpoint->cord);
+	} catch (Exception *e) {
+		e->log();
+		result = -1;
+		SystemError *se = type_cast(SystemError, e);
+		if (se)
+			errno = se->errnum();
+	}
+
+	m_checkpoint->waiting_for_snap_thread = false;
+	return result;
 }
 
 void
 MemtxEngine::commitCheckpoint()
 {
 	/* beginCheckpoint() must have been done */
-	assert(m_checkpoint_id >= 0);
+	assert(m_checkpoint);
 	/* waitCheckpoint() must have been done. */
-	assert(m_snapshot_pid == 0);
+	assert(!m_checkpoint->waiting_for_snap_thread);
+
+	tuple_end_snapshot();
 
-	struct xdir *dir = &::recovery->snap_dir;
+	struct xdir *dir = &m_checkpoint->dir;
 	/* rename snapshot on completion */
 	char to[PATH_MAX];
 	snprintf(to, sizeof(to), "%s",
-		 format_filename(dir, m_checkpoint_id, NONE));
-	char *from = format_filename(dir, m_checkpoint_id, INPROGRESS);
+		 format_filename(dir, m_checkpoint->lsn, NONE));
+	char *from = format_filename(dir, m_checkpoint->lsn, INPROGRESS);
 	int rc = coeio_rename(from, to);
 	if (rc != 0)
 		panic("can't rename .snap.inprogress");
 
-	m_checkpoint_id = -1;
+	checkpoint_destroy(m_checkpoint);
+	m_checkpoint = 0;
 }
 
 void
 MemtxEngine::abortCheckpoint()
 {
-	if (m_snapshot_pid > 0) {
-		assert(m_checkpoint_id >= 0);
-		/**
-		 * An error in the other engine's first phase.
-		 */
-		waitCheckpoint();
-	}
-	if (m_checkpoint_id > 0) {
-		/** Remove garbage .inprogress file. */
-		char *filename = format_filename(&::recovery->snap_dir,
-						m_checkpoint_id, INPROGRESS);
-		(void) coeio_unlink(filename);
+	/**
+	 * An error in the other engine's first phase.
+	 */
+	if (m_checkpoint->waiting_for_snap_thread) {
+		try {
+			/* wait for memtx-part snapshot completion */
+			cord_cojoin(&m_checkpoint->cord);
+		} catch (Exception *e) {
+			e->log();
+		}
+		m_checkpoint->waiting_for_snap_thread = false;
 	}
-	m_checkpoint_id = -1;
+
+	tuple_end_snapshot();
+
+	/** Remove garbage .inprogress file. */
+	char *filename = format_filename(&m_checkpoint->dir,
+					 m_checkpoint->lsn,
+					 INPROGRESS);
+	(void) coeio_unlink(filename);
+
+	checkpoint_destroy(m_checkpoint);
+	m_checkpoint = 0;
 }
 
 void
@@ -828,6 +898,9 @@ memtx_index_arena_init()
 	mempool_create(&memtx_index_extent_pool,
 		       &memtx_index_arena_slab_cache,
 		       MEMTX_EXTENT_SIZE);
+	/* Empty reserved list */
+	memtx_index_num_reserved_extents = 0;
+	memtx_index_reserved_extents = 0;
 	/* Done */
 	memtx_index_arena_initialized = true;
 }
@@ -838,7 +911,19 @@ memtx_index_arena_init()
 void *
 memtx_index_extent_alloc()
 {
-	ERROR_INJECT(ERRINJ_INDEX_ALLOC, return 0);
+	if (memtx_index_reserved_extents) {
+		assert(memtx_index_num_reserved_extents > 0);
+		memtx_index_num_reserved_extents--;
+		void *result = memtx_index_reserved_extents;
+		memtx_index_reserved_extents = *(void **)
+			memtx_index_reserved_extents;
+		return result;
+	}
+	ERROR_INJECT(ERRINJ_INDEX_ALLOC,
+		     /* same error as in mempool_alloc */
+		     tnt_raise(OutOfMemory, MEMTX_EXTENT_SIZE,
+			       "mempool", "new slab")
+	);
 	return mempool_alloc(&memtx_index_extent_pool);
 }
 
@@ -850,3 +935,23 @@ memtx_index_extent_free(void *extent)
 {
 	return mempool_free(&memtx_index_extent_pool, extent);
 }
+
+/**
+ * Reserve num extents in pool.
+ * Ensure that next num extent_alloc will succeed w/o an error
+ */
+void
+memtx_index_extent_reserve(int num)
+{
+	ERROR_INJECT(ERRINJ_INDEX_ALLOC,
+	/* same error as in mempool_alloc */
+		     tnt_raise(OutOfMemory, MEMTX_EXTENT_SIZE,
+			       "mempool", "new slab")
+	);
+	while (memtx_index_num_reserved_extents < num) {
+		void *ext = mempool_alloc(&memtx_index_extent_pool);
+		*(void **)ext = memtx_index_reserved_extents;
+		memtx_index_reserved_extents = ext;
+		memtx_index_num_reserved_extents++;
+	}
+}
diff --git a/src/box/memtx_engine.h b/src/box/memtx_engine.h
index b8e771687caa9f45c84f8c062f0b88f965078cd9..7d823881500ab3efa6418a7893a7f88bf1ff3d15 100644
--- a/src/box/memtx_engine.h
+++ b/src/box/memtx_engine.h
@@ -61,11 +61,8 @@ struct MemtxEngine: public Engine {
 	virtual void abortCheckpoint();
 	virtual void initSystemSpace(struct space *space);
 private:
-	/**
-	 * LSN of the snapshot which is in progress.
-	 */
-	int64_t m_checkpoint_id;
-	pid_t m_snapshot_pid;
+	/** Non-zero if there is a checkpoint (snapshot) in * progress. */
+	struct checkpoint *m_checkpoint;
 	enum memtx_recovery_state m_state;
 };
 
@@ -95,4 +92,11 @@ memtx_index_extent_alloc();
 void
 memtx_index_extent_free(void *extent);
 
+/**
+ * Reserve num extents in pool.
+ * Ensure that next num extent_alloc will succeed w/o an error
+ */
+void
+memtx_index_extent_reserve(int num);
+
 #endif /* TARANTOOL_BOX_MEMTX_ENGINE_H_INCLUDED */
diff --git a/src/box/memtx_hash.cc b/src/box/memtx_hash.cc
index 97e88213e6941e5160255879f76e0a1cd270a11b..6f594e98945a86054e329656f8a81445446da686 100644
--- a/src/box/memtx_hash.cc
+++ b/src/box/memtx_hash.cc
@@ -319,7 +319,7 @@ MemtxHash::replace(struct tuple *old_tuple, struct tuple *new_tuple,
 	if (old_tuple) {
 		uint32_t h = tuple_hash(old_tuple, key_def);
 		int res = light_index_delete_value(hash_table, h, old_tuple);
-		assert(res == 0); (void)res;
+		assert(res == 0); (void) res;
 	}
 	return old_tuple;
 }
@@ -378,4 +378,27 @@ MemtxHash::initIterator(struct iterator *ptr, enum iterator_type type,
 			  "Hash index", "requested iterator type");
 	}
 }
+
+/**
+ * Create a read view for iterator so further index modifications
+ * will not affect the iterator iteration.
+ */
+void
+MemtxHash::createReadViewForIterator(struct iterator *iterator)
+{
+	struct hash_iterator *it = (struct hash_iterator *) iterator;
+	light_index_itr_freeze(it->hash_table, &it->hitr);
+}
+
+/**
+ * Destroy a read view of an iterator. Must be called for iterators,
+ * for which createReadViewForIterator was called.
+ */
+void
+MemtxHash::destroyReadViewForIterator(struct iterator *iterator)
+{
+	struct hash_iterator *it = (struct hash_iterator *) iterator;
+	light_index_itr_destroy(it->hash_table, &it->hitr);
+}
+
 /* }}} */
diff --git a/src/box/memtx_hash.h b/src/box/memtx_hash.h
index 448b576a524ad488ee6139037f19415240dc3cf1..6def59301e7599d38f71e40e62400a5b45906fab 100644
--- a/src/box/memtx_hash.h
+++ b/src/box/memtx_hash.h
@@ -50,6 +50,18 @@ class MemtxHash: public Index {
 	virtual void initIterator(struct iterator *iterator,
 				  enum iterator_type type,
 				  const char *key, uint32_t part_count) const;
+
+	/**
+	 * Create a read view for iterator so further index modifications
+	 * will not affect the iterator iteration.
+	 */
+	virtual void createReadViewForIterator(struct iterator *iterator);
+	/**
+	 * Destroy a read view of an iterator. Must be called for iterators,
+	 * for which createReadViewForIterator was called.
+	 */
+	virtual void destroyReadViewForIterator(struct iterator *iterator);
+
 	virtual size_t bsize() const;
 
 protected:
diff --git a/src/box/memtx_rtree.cc b/src/box/memtx_rtree.cc
index 7106aa0f7cf7dae1a83d5fe0a0125962605fb522..4f5e414951d7c3eda5d2467d690e58227d1ce1ee 100644
--- a/src/box/memtx_rtree.cc
+++ b/src/box/memtx_rtree.cc
@@ -34,11 +34,6 @@
 #include "fiber.h"
 #include "small/small.h"
 
-/**
- * Single-linked list of free rtree pages
- */
-static void *rtree_free_pages = 0;
-
 /* {{{ Utilities. *************************************************/
 
 inline void extract_rectangle(struct rtree_rect *rect,
@@ -122,41 +117,6 @@ index_rtree_iterator_next(struct iterator *i)
 
 /* {{{ MemtxRTree  **********************************************************/
 
-static void *
-rtree_page_alloc()
-{
-	ERROR_INJECT(ERRINJ_INDEX_ALLOC, return 0);
-	if (!rtree_free_pages) {
-		/**
-		 * No free pages in list - let's allocate new extent, split it
-		 * into pages and add them to the list.
-		 */
-		char *extent = (char *)memtx_index_extent_alloc();
-		if (!extent) {
-			panic("%s", "Memory allocation failed in rtree");
-			return 0;
-		}
-		assert(MEMTX_EXTENT_SIZE % RTREE_PAGE_SIZE == 0);
-		assert(RTREE_PAGE_SIZE >= sizeof(void *));
-		for (size_t i = 0; i < MEMTX_EXTENT_SIZE; i += RTREE_PAGE_SIZE) {
-			*(void **)(extent + i) = rtree_free_pages;
-			rtree_free_pages = (void *)(extent + i);
-		}
-	}
-	/* Now we surely have a free page in free list */
-	void *res = rtree_free_pages;
-	rtree_free_pages = *(void **)rtree_free_pages;
-	return res;
-}
-
-static void
-rtree_page_free(void *page)
-{
-	/* Just add to free list. */
-	*(void **)page = rtree_free_pages;
-	rtree_free_pages = page;
-}
-
 MemtxRTree::~MemtxRTree()
 {
 	// Iterator has to be destroyed prior to tree
@@ -175,7 +135,8 @@ MemtxRTree::MemtxRTree(struct key_def *key_def)
 	assert(key_def->is_unique == false);
 
 	memtx_index_arena_init();
-	rtree_init(&tree, rtree_page_alloc, rtree_page_free);
+	rtree_init(&tree, MEMTX_EXTENT_SIZE,
+		   memtx_index_extent_alloc, memtx_index_extent_free);
 }
 
 size_t
diff --git a/src/box/memtx_tree.cc b/src/box/memtx_tree.cc
index 2046b6d345c33399f2cd951b3b3359200e2129d1..2e8a232e5317b01e8b1e6385eeb2a8ae52e4f77e 100644
--- a/src/box/memtx_tree.cc
+++ b/src/box/memtx_tree.cc
@@ -399,3 +399,27 @@ MemtxTree::endBuild()
 	build_array_alloc_size = 0;
 }
 
+/**
+ * Create a read view for iterator so further index modifications
+ * will not affect the iterator iteration.
+ */
+void
+MemtxTree::createReadViewForIterator(struct iterator *iterator)
+{
+	struct tree_iterator *it = tree_iterator(iterator);
+	struct bps_tree_index *tree = (struct bps_tree_index *)it->tree;
+	bps_tree_index_itr_freeze(tree, &it->bps_tree_iter);
+}
+
+/**
+ * Destroy a read view of an iterator. Must be called for iterators,
+ * for which createReadViewForIterator was called.
+ */
+void
+MemtxTree::destroyReadViewForIterator(struct iterator *iterator)
+{
+	struct tree_iterator *it = tree_iterator(iterator);
+	struct bps_tree_index *tree = (struct bps_tree_index *)it->tree;
+	bps_tree_index_itr_destroy(tree, &it->bps_tree_iter);
+}
+
diff --git a/src/box/memtx_tree.h b/src/box/memtx_tree.h
index 00019cc7b19c5af677f60ecf969c55ef259544bd..d4ffa12f6287133fdc9cab13450f3f2d02b1a89c 100644
--- a/src/box/memtx_tree.h
+++ b/src/box/memtx_tree.h
@@ -74,6 +74,17 @@ class MemtxTree: public Index {
 				  enum iterator_type type,
 				  const char *key, uint32_t part_count) const;
 
+	/**
+	 * Create a read view for iterator so further index modifications
+	 * will not affect the iterator iteration.
+	 */
+	virtual void createReadViewForIterator(struct iterator *iterator);
+	/**
+	 * Destroy a read view of an iterator. Must be called for iterators,
+	 * for which createReadViewForIterator was called.
+	 */
+	virtual void destroyReadViewForIterator(struct iterator *iterator);
+
 // protected:
 	struct bps_tree_index tree;
 	struct tuple **build_array;
diff --git a/src/box/port.cc b/src/box/port.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5598e59b1c879e78f6d0622f144106c39d043b95
--- /dev/null
+++ b/src/box/port.cc
@@ -0,0 +1,138 @@
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "port.h"
+#include "tuple.h"
+#include <lib/small/slab_cache.h>
+#include <lib/small/mempool.h>
+#include <fiber.h>
+
+void
+null_port_eof(struct port *port __attribute__((unused)))
+{
+}
+
+static void
+null_port_add_tuple(struct port *port __attribute__((unused)),
+		    struct tuple *tuple __attribute__((unused)))
+{
+}
+
+static struct port_vtab null_port_vtab = {
+	null_port_add_tuple,
+	null_port_eof,
+};
+
+struct port null_port = {
+	/* .vtab = */ &null_port_vtab,
+};
+
+static struct mempool port_buf_entry_pool;
+
+static void
+port_buf_add_tuple(struct port *port, struct tuple *tuple)
+{
+	struct port_buf *port_buf = (struct port_buf *) port;
+	struct port_buf_entry *e;
+	if (port_buf->size == 0) {
+		tuple_ref(tuple); /* throws */
+		e = &port_buf->first_entry;
+		port_buf->first = port_buf->last = e;
+	} else {
+		e = (struct port_buf_entry *)
+			mempool_alloc(&port_buf_entry_pool); /* throws */
+		try {
+			tuple_ref(tuple); /* throws */
+		} catch (Exception *) {
+			mempool_free(&port_buf_entry_pool, e);
+			throw;
+		}
+		port_buf->last->next = e;
+		port_buf->last = e;
+	}
+	e->tuple = tuple;
+	e->next = NULL;
+	++port_buf->size;
+}
+
+static struct port_vtab port_buf_vtab = {
+	port_buf_add_tuple,
+	null_port_eof,
+};
+
+void
+port_buf_create(struct port_buf *port_buf)
+{
+	port_buf->base.vtab = &port_buf_vtab;
+	port_buf->size = 0;
+	port_buf->first = NULL;
+	port_buf->last = NULL;
+}
+
+void
+port_buf_destroy(struct port_buf *port_buf)
+{
+	struct port_buf_entry *e = port_buf->first;
+	if (e == NULL)
+		return;
+	tuple_unref(e->tuple);
+	e = e->next;
+	while (e != NULL) {
+		struct port_buf_entry *cur = e;
+		e = e->next;
+		tuple_unref(cur->tuple);
+		mempool_free(&port_buf_entry_pool, cur);
+	}
+}
+
+void
+port_buf_transfer(struct port_buf *port_buf)
+{
+	struct port_buf_entry *e = port_buf->first;
+	if (e == NULL)
+		return;
+	e = e->next;
+	while (e != NULL) {
+		struct port_buf_entry *cur = e;
+		e = e->next;
+		mempool_free(&port_buf_entry_pool, cur);
+	}
+}
+
+void
+port_init(void)
+{
+	mempool_create(&port_buf_entry_pool, &cord()->slabc,
+		       sizeof(struct port_buf_entry));
+}
+
+void
+port_free(void)
+{
+	mempool_destroy(&port_buf_entry_pool);
+}
diff --git a/src/box/port.h b/src/box/port.h
index e839114876127ea8ea9796e112d7e741c884d4ca..9bfa361c47a75e1bd98cdfa8724b438d1ea94055 100644
--- a/src/box/port.h
+++ b/src/box/port.h
@@ -29,6 +29,7 @@
  * SUCH DAMAGE.
  */
 #include "trivia/util.h"
+#include "lib/salad/rlist.h"
 
 #if defined(__cplusplus)
 extern "C" {
@@ -91,6 +92,40 @@ extern struct port null_port;
 void
 null_port_eof(struct port *port __attribute__((unused)));
 
+struct port_buf_entry {
+	struct port_buf_entry *next;
+	struct tuple *tuple;
+};
+
+struct port_buf {
+	struct port base;
+	size_t size;
+	struct port_buf_entry *first;
+	struct port_buf_entry *last;
+	struct port_buf_entry first_entry;
+};
+
+void
+port_buf_create(struct port_buf *port_buf);
+
+/**
+ * Unref all tuples and free allocated memory
+ */
+void
+port_buf_destroy(struct port_buf *port_buf);
+
+/**
+ * Like port_buf_destroy() but doesn't do tuple_unref()
+ */
+void
+port_buf_transfer(struct port_buf *port_buf);
+
+void
+port_init(void);
+
+void
+port_free(void);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/box/recovery.cc b/src/box/recovery.cc
index 6b8f6a51aa05c27cda5872f415ece8aaefc02fe0..9843da6631f01549b3b720b802c9f1c282be5588 100644
--- a/src/box/recovery.cc
+++ b/src/box/recovery.cc
@@ -26,36 +26,23 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
-#define MH_SOURCE 1
 #include "recovery.h"
 
-#include <fcntl.h>
-
-#include "xlog.h"
+#include "scoped_guard.h"
 #include "fiber.h"
-#include "tt_pthread.h"
-#include "fio.h"
-#include "sio.h"
-#include "errinj.h"
 #include "bootstrap.h"
+#include "xlog.h"
+#include "xrow.h"
 
 #include "replica.h"
-#include "fiber.h"
-#include "msgpuck/msgpuck.h"
-#include "xrow.h"
-#include "crc32.h"
-#include "scoped_guard.h"
-#include "box/cluster.h"
-#include "vclock.h"
+#include "cluster.h"
 #include "session.h"
-#include "coio.h"
 
 /*
  * Recovery subsystem
  * ------------------
  *
- * A facade of the recovery subsystem is struct recovery_state,
- * which is a singleton.
+ * A facade of the recovery subsystem is struct recovery_state.
  *
  * Depending on the configuration, start-up parameters, the
  * actual task being performed, the recovery can be
@@ -65,8 +52,6 @@
  * - temporal: whether or not the instance is just booting
  *   from a snapshot, is in 'local hot standby mode', or
  *   is already accepting requests
- * - topological: whether or not it is a master instance
- *   or a replica
  * - task based: whether it's a master process,
  *   snapshot saving process or a replication relay.
  *
@@ -97,8 +82,6 @@
  * M - master mode, recording in-memory state changes in the WAL
  * R - replica mode, receiving changes from the master and
  * recording them in the WAL
- * S - snapshot mode, writing entire in-memory state to a compact
- * snapshot file.
  *
  * The following state transitions are possible/supported:
  *
@@ -108,16 +91,12 @@
  * HS -> M          # recovery_finalize()
  * M -> R           # recovery_follow_remote()
  * R -> M           # recovery_stop_remote()
- * M -> S           # snapshot()
- * R -> S           # snapshot()
  */
 
-const char *wal_mode_STRS[] = { "none", "write", "fsync", NULL };
-
 /* {{{ LSN API */
 
-static void
-fill_lsn(struct recovery_state *r, struct xrow_header *row)
+void
+recovery_fill_lsn(struct recovery_state *r, struct xrow_header *row)
 {
 	if (row->server_id == 0) {
 		/* Local request. */
@@ -137,15 +116,18 @@ fill_lsn(struct recovery_state *r, struct xrow_header *row)
 	}
 }
 
+int64_t
+recovery_last_checkpoint(struct recovery_state *r)
+{
+	/* recover last snapshot lsn */
+	struct vclock *vclock = vclockset_last(&r->snap_dir.index);
+	return vclock ? vclock_sum(vclock) : -1;
+}
+
 /* }}} */
 
 /* {{{ Initial recovery */
 
-static int
-wal_writer_start(struct recovery_state *state, int rows_per_wal);
-void
-wal_writer_stop(struct recovery_state *r);
-
 /**
  * Throws an exception in  case of error.
  */
@@ -260,6 +242,18 @@ recovery_exit(struct recovery_state *r)
 	recovery_delete(r);
 }
 
+void
+recovery_atfork(struct recovery_state *r)
+{
+       xlog_atfork(&r->current_wal);
+       /*
+        * Make sure that atexit() handlers in the child do
+        * not try to stop the non-existent thread.
+        * The writer is not used in the child.
+        */
+       r->writer = NULL;
+}
+
 void
 recovery_apply_row(struct recovery_state *r, struct xrow_header *row)
 {
@@ -547,474 +541,3 @@ recovery_stop_local(struct recovery_state *r)
 
 /* }}} */
 
-/* {{{ WAL writer - maintain a Write Ahead Log for every change
- * in the data state.
- */
-
-struct wal_write_request {
-	STAILQ_ENTRY(wal_write_request) wal_fifo_entry;
-	/* Auxiliary. */
-	int64_t res;
-	struct fiber *fiber;
-	struct xrow_header *row;
-};
-
-/* Context of the WAL writer thread. */
-STAILQ_HEAD(wal_fifo, wal_write_request);
-
-struct wal_writer
-{
-	struct wal_fifo input;
-	struct wal_fifo commit;
-	struct cord cord;
-	pthread_mutex_t mutex;
-	pthread_cond_t cond;
-	ev_async write_event;
-	int rows_per_wal;
-	struct fio_batch *batch;
-	bool is_shutdown;
-	bool is_rollback;
-	ev_loop *txn_loop;
-	struct vclock vclock;
-	bool is_started;
-};
-
-static struct wal_writer wal_writer;
-
-/**
- * A pthread_atfork() callback for a child process. Today we only
- * fork the master process to save a snapshot, and in the child
- * the WAL writer thread is not necessary and not present.
- */
-void
-recovery_atfork(struct recovery_state *r)
-{
-	xlog_atfork(&r->current_wal);
-	if (r->writer == NULL)
-		return;
-	if (r->writer->batch) {
-		free(r->writer->batch);
-		r->writer->batch = NULL;
-	}
-	/*
-	 * Make sure that atexit() handlers in the child do
-	 * not try to stop the non-existent thread.
-	 * The writer is not used in the child.
-	 */
-	r->writer = NULL;
-}
-
-/**
- * A commit watcher callback is invoked whenever there
- * are requests in wal_writer->commit. This callback is
- * associated with an internal WAL writer watcher and is
- * invoked in the front-end main event loop.
- *
- * A rollback watcher callback is invoked only when there is
- * a rollback request and commit is empty.
- * We roll back the entire input queue.
- *
- * ev_async, under the hood, is a simple pipe. The WAL
- * writer thread writes to that pipe whenever it's done
- * handling a pack of requests (look for ev_async_send()
- * call in the writer thread loop).
- */
-static void
-wal_schedule_queue(struct wal_fifo *queue)
-{
-	/*
-	 * Can't use STAILQ_FOREACH since fiber_call()
-	 * destroys the list entry.
-	 */
-	struct wal_write_request *req, *tmp;
-	STAILQ_FOREACH_SAFE(req, queue, wal_fifo_entry, tmp)
-		fiber_call(req->fiber);
-}
-
-static void
-wal_schedule(ev_loop * /* loop */, ev_async *watcher, int /* event */)
-{
-	struct wal_writer *writer = (struct wal_writer *) watcher->data;
-	struct wal_fifo commit = STAILQ_HEAD_INITIALIZER(commit);
-	struct wal_fifo rollback = STAILQ_HEAD_INITIALIZER(rollback);
-
-	(void) tt_pthread_mutex_lock(&writer->mutex);
-	STAILQ_CONCAT(&commit, &writer->commit);
-	if (writer->is_rollback) {
-		STAILQ_CONCAT(&rollback, &writer->input);
-		writer->is_rollback = false;
-	}
-	(void) tt_pthread_mutex_unlock(&writer->mutex);
-
-	wal_schedule_queue(&commit);
-	/*
-	 * Perform a cascading abort of all transactions which
-	 * depend on the transaction which failed to get written
-	 * to the write ahead log. Abort transactions
-	 * in reverse order, performing a playback of the
-	 * in-memory database state.
-	 */
-	STAILQ_REVERSE(&rollback, wal_write_request, wal_fifo_entry);
-	wal_schedule_queue(&rollback);
-}
-
-/**
- * Initialize WAL writer context. Even though it's a singleton,
- * encapsulate the details just in case we may use
- * more writers in the future.
- */
-static void
-wal_writer_init(struct wal_writer *writer, struct vclock *vclock,
-		int rows_per_wal)
-{
-	/* I. Initialize the state. */
-	pthread_mutexattr_t errorcheck;
-
-	(void) tt_pthread_mutexattr_init(&errorcheck);
-
-#ifndef NDEBUG
-	(void) tt_pthread_mutexattr_settype(&errorcheck, PTHREAD_MUTEX_ERRORCHECK);
-#endif
-	/* Initialize queue lock mutex. */
-	(void) tt_pthread_mutex_init(&writer->mutex, &errorcheck);
-	(void) tt_pthread_mutexattr_destroy(&errorcheck);
-
-	(void) tt_pthread_cond_init(&writer->cond, NULL);
-
-	STAILQ_INIT(&writer->input);
-	STAILQ_INIT(&writer->commit);
-
-	ev_async_init(&writer->write_event, wal_schedule);
-	writer->write_event.data = writer;
-	writer->txn_loop = loop();
-	writer->rows_per_wal = rows_per_wal;
-
-	writer->batch = fio_batch_alloc(sysconf(_SC_IOV_MAX));
-
-	if (writer->batch == NULL)
-		panic_syserror("fio_batch_alloc");
-
-	/* Create and fill writer->cluster hash */
-	vclock_create(&writer->vclock);
-	vclock_copy(&writer->vclock, vclock);
-	writer->is_started = false;
-}
-
-/** Destroy a WAL writer structure. */
-static void
-wal_writer_destroy(struct wal_writer *writer)
-{
-	(void) tt_pthread_mutex_destroy(&writer->mutex);
-	(void) tt_pthread_cond_destroy(&writer->cond);
-	free(writer->batch);
-}
-
-/** WAL writer thread routine. */
-static void *wal_writer_thread(void *worker_args);
-
-/**
- * Initialize WAL writer, start the thread.
- *
- * @pre   The server has completed recovery from a snapshot
- *        and/or existing WALs. All WALs opened in read-only
- *        mode are closed.
- *
- * @param state			WAL writer meta-data.
- *
- * @return 0 success, -1 on error. On success, recovery->writer
- *         points to a newly created WAL writer.
- */
-static int
-wal_writer_start(struct recovery_state *r, int rows_per_wal)
-{
-	assert(r->writer == NULL);
-	assert(r->current_wal == NULL);
-	assert(rows_per_wal > 1);
-	assert(! wal_writer.is_shutdown);
-	assert(STAILQ_EMPTY(&wal_writer.input));
-	assert(STAILQ_EMPTY(&wal_writer.commit));
-
-	assert(wal_writer.is_started == false);
-	/* I. Initialize the state. */
-	wal_writer_init(&wal_writer, &r->vclock, rows_per_wal);
-	r->writer = &wal_writer;
-
-	ev_async_start(wal_writer.txn_loop, &wal_writer.write_event);
-
-	/* II. Start the thread. */
-
-	if (cord_start(&wal_writer.cord, "wal", wal_writer_thread, r)) {
-		wal_writer_destroy(&wal_writer);
-		r->writer = NULL;
-		return -1;
-	}
-	wal_writer.is_started = true;
-	return 0;
-}
-
-/** Stop and destroy the writer thread (at shutdown). */
-void
-wal_writer_stop(struct recovery_state *r)
-{
-	struct wal_writer *writer = r->writer;
-
-	/* Stop the worker thread. */
-
-	(void) tt_pthread_mutex_lock(&writer->mutex);
-	writer->is_shutdown= true;
-	(void) tt_pthread_cond_signal(&writer->cond);
-	(void) tt_pthread_mutex_unlock(&writer->mutex);
-	if (cord_join(&writer->cord)) {
-		/* We can't recover from this in any reasonable way. */
-		panic_syserror("WAL writer: thread join failed");
-	}
-
-	ev_async_stop(writer->txn_loop, &writer->write_event);
-	wal_writer.is_started = false;
-	wal_writer_destroy(writer);
-
-	r->writer = NULL;
-}
-
-/**
- * Pop a bulk of requests to write to disk to process.
- * Block on the condition only if we have no other work to
- * do. Loop in case of a spurious wakeup.
- */
-void
-wal_writer_pop(struct wal_writer *writer, struct wal_fifo *input)
-{
-	while (! writer->is_shutdown)
-	{
-		if (! writer->is_rollback && ! STAILQ_EMPTY(&writer->input)) {
-			STAILQ_CONCAT(input, &writer->input);
-			break;
-		}
-		(void) tt_pthread_cond_wait(&writer->cond, &writer->mutex);
-	}
-}
-
-/**
- * If there is no current WAL, try to open it, and close the
- * previous WAL. We close the previous WAL only after opening
- * a new one to smoothly move local hot standby and replication
- * over to the next WAL.
- * In case of error, we try to close any open WALs.
- *
- * @post r->current_wal is in a good shape for writes or is NULL.
- * @return 0 in case of success, -1 on error.
- */
-static int
-wal_opt_rotate(struct xlog **wal, struct recovery_state *r,
-	       struct vclock *vclock)
-{
-	struct xlog *l = *wal, *wal_to_close = NULL;
-
-	ERROR_INJECT_RETURN(ERRINJ_WAL_ROTATE);
-
-	if (l != NULL && l->rows >= r->writer->rows_per_wal) {
-		wal_to_close = l;
-		l = NULL;
-	}
-	if (l == NULL) {
-		/*
-		 * Close the file *before* we create the new WAL, to
-		 * make sure local hot standby/replication can see
-		 * EOF in the old WAL before switching to the new
-		 * one.
-		 */
-		if (wal_to_close) {
-			/*
-			 * We can not handle xlog_close()
-			 * failure in any reasonable way.
-			 * A warning is written to the server
-			 * log file.
-			 */
-			xlog_close(wal_to_close);
-			wal_to_close = NULL;
-		}
-		/* Open WAL with '.inprogress' suffix. */
-		l = xlog_create(&r->wal_dir, vclock);
-	}
-	assert(wal_to_close == NULL);
-	*wal = l;
-	return l ? 0 : -1;
-}
-
-static struct wal_write_request *
-wal_fill_batch(struct xlog *wal, struct fio_batch *batch, int rows_per_wal,
-	       struct wal_write_request *req)
-{
-	int max_rows = rows_per_wal - wal->rows;
-	/* Post-condition of successful wal_opt_rotate(). */
-	assert(max_rows > 0);
-	fio_batch_start(batch, max_rows);
-
-	struct iovec iov[XROW_IOVMAX];
-	while (req != NULL && !fio_batch_has_space(batch, nelem(iov))) {
-		int iovcnt = xlog_encode_row(req->row, iov);
-		fio_batch_add(batch, iov, iovcnt);
-		req = STAILQ_NEXT(req, wal_fifo_entry);
-	}
-	return req;
-}
-
-/**
- * fio_batch_write() version with recovery specific
- * error injection.
- */
-static inline int
-wal_fio_batch_write(struct fio_batch *batch, int fd)
-{
-	ERROR_INJECT(ERRINJ_WAL_WRITE, return 0);
-	return fio_batch_write(batch, fd);
-}
-
-static struct wal_write_request *
-wal_write_batch(struct xlog *wal, struct fio_batch *batch,
-		struct wal_write_request *req, struct wal_write_request *end,
-		struct vclock *vclock)
-{
-	int rows_written = wal_fio_batch_write(batch, fileno(wal->f));
-	wal->rows += rows_written;
-	while (req != end && rows_written-- != 0)  {
-		vclock_follow(vclock, req->row->server_id, req->row->lsn);
-		req->res = 0;
-		req = STAILQ_NEXT(req, wal_fifo_entry);
-	}
-	return req;
-}
-
-static void
-wal_write_to_disk(struct recovery_state *r, struct wal_writer *writer,
-		  struct wal_fifo *input, struct wal_fifo *commit,
-		  struct wal_fifo *rollback)
-{
-	struct xlog **wal = &r->current_wal;
-	struct fio_batch *batch = writer->batch;
-
-	struct wal_write_request *req = STAILQ_FIRST(input);
-	struct wal_write_request *write_end = req;
-
-	while (req) {
-		if (wal_opt_rotate(wal, r, &writer->vclock) != 0)
-			break;
-		struct wal_write_request *batch_end;
-		batch_end = wal_fill_batch(*wal, batch, writer->rows_per_wal,
-					   req);
-		write_end = wal_write_batch(*wal, batch, req, batch_end,
-					    &writer->vclock);
-		if (batch_end != write_end)
-			break;
-		req = write_end;
-	}
-	fiber_gc();
-	STAILQ_SPLICE(input, write_end, wal_fifo_entry, rollback);
-	STAILQ_CONCAT(commit, input);
-}
-
-/** WAL writer thread main loop.  */
-static void *
-wal_writer_thread(void *worker_args)
-{
-	struct recovery_state *r = (struct recovery_state *) worker_args;
-	struct wal_writer *writer = r->writer;
-	struct wal_fifo input = STAILQ_HEAD_INITIALIZER(input);
-	struct wal_fifo commit = STAILQ_HEAD_INITIALIZER(commit);
-	struct wal_fifo rollback = STAILQ_HEAD_INITIALIZER(rollback);
-
-	(void) tt_pthread_mutex_lock(&writer->mutex);
-	while (! writer->is_shutdown) {
-		wal_writer_pop(writer, &input);
-		(void) tt_pthread_mutex_unlock(&writer->mutex);
-
-		wal_write_to_disk(r, writer, &input, &commit, &rollback);
-
-		(void) tt_pthread_mutex_lock(&writer->mutex);
-		STAILQ_CONCAT(&writer->commit, &commit);
-		if (! STAILQ_EMPTY(&rollback)) {
-			/*
-			 * Begin rollback: create a rollback queue
-			 * from all requests which were not
-			 * written to disk and all requests in the
-			 * input queue.
-			 */
-			writer->is_rollback = true;
-			STAILQ_CONCAT(&rollback, &writer->input);
-			STAILQ_CONCAT(&writer->input, &rollback);
-		}
-		ev_async_send(writer->txn_loop, &writer->write_event);
-	}
-	(void) tt_pthread_mutex_unlock(&writer->mutex);
-	if (r->current_wal != NULL) {
-		xlog_close(r->current_wal);
-		r->current_wal = NULL;
-	}
-	return NULL;
-}
-
-/**
- * WAL writer main entry point: queue a single request
- * to be written to disk and wait until this task is completed.
- */
-int64_t
-wal_write(struct recovery_state *r, struct xrow_header *row)
-{
-	/*
-	 * Bump current LSN even if wal_mode = NONE, so that
-	 * snapshots still works with WAL turned off.
-	 */
-	fill_lsn(r, row);
-	if (r->wal_mode == WAL_NONE)
-		return vclock_sum(&r->vclock);
-
-	ERROR_INJECT_RETURN(ERRINJ_WAL_IO);
-
-	struct wal_writer *writer = r->writer;
-
-	struct wal_write_request *req = (struct wal_write_request *)
-		region_alloc(&fiber()->gc, sizeof(struct wal_write_request));
-
-	req->fiber = fiber();
-	req->res = -1;
-	req->row = row;
-	row->tm = ev_now(loop());
-
-	(void) tt_pthread_mutex_lock(&writer->mutex);
-
-	bool input_was_empty = STAILQ_EMPTY(&writer->input);
-	STAILQ_INSERT_TAIL(&writer->input, req, wal_fifo_entry);
-
-	if (input_was_empty)
-		(void) tt_pthread_cond_signal(&writer->cond);
-
-	(void) tt_pthread_mutex_unlock(&writer->mutex);
-
-	/**
-	 * It's not safe to spuriously wakeup this fiber
-	 * since in that case it will ignore a possible
-	 * error from WAL writer and not roll back the
-	 * transaction.
-	 */
-	bool cancellable = fiber_set_cancellable(false);
-	fiber_yield(); /* Request was inserted. */
-	fiber_set_cancellable(cancellable);
-	if (req->res == -1)
-		return -1;
-	return vclock_sum(&r->vclock);
-}
-
-/* }}} */
-
-/* {{{ box.snapshot() */
-
-int64_t
-recovery_last_checkpoint(struct recovery_state *r)
-{
-	/* recover last snapshot lsn */
-	struct vclock *vclock = vclockset_last(&r->snap_dir.index);
-	return vclock ? vclock_sum(vclock) : -1;
-}
-
-/* }}} */
-
diff --git a/src/box/recovery.h b/src/box/recovery.h
index 9aae1746b39b3d230102869f77f8fdc222936587..c68f56c88a8c93cb6d4699a38c990c35fce6f938 100644
--- a/src/box/recovery.h
+++ b/src/box/recovery.h
@@ -28,7 +28,6 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
-#include <stdbool.h>
 #include <netinet/in.h>
 #include <sys/socket.h>
 
@@ -38,13 +37,12 @@
 #include "vclock.h"
 #include "tt_uuid.h"
 #include "uri.h"
+#include "wal.h"
 
 #if defined(__cplusplus)
 extern "C" {
 #endif /* defined(__cplusplus) */
 
-struct fiber;
-struct tbuf;
 struct recovery_state;
 
 typedef void (apply_row_f)(struct recovery_state *, void *,
@@ -54,13 +52,8 @@ typedef void (apply_row_f)(struct recovery_state *, void *,
  * LSN makes it to disk.
  */
 
-struct wal_writer;
 struct wal_watcher;
-
-enum wal_mode { WAL_NONE = 0, WAL_WRITE, WAL_FSYNC, WAL_MODE_MAX };
-
-/** String constants for the supported modes. */
-extern const char *wal_mode_STRS[];
+struct wal_writer;
 
 enum { REMOTE_SOURCE_MAXLEN = 1024 }; /* enough to fit URI with passwords */
 
@@ -117,11 +110,16 @@ void
 recovery_exit(struct recovery_state *r);
 
 void
-recovery_atfork(struct recovery_state *r);
+recovery_update_mode(struct recovery_state *r,
+		     enum wal_mode mode);
 
-void recovery_update_mode(struct recovery_state *r, enum wal_mode mode);
-void recovery_update_io_rate_limit(struct recovery_state *r,
-				   double new_limit);
+void
+recovery_update_io_rate_limit(struct recovery_state *r,
+			      double new_limit);
+
+void
+recovery_setup_panic(struct recovery_state *r, bool on_snap_error,
+		     bool on_wal_error);
 
 static inline bool
 recovery_has_data(struct recovery_state *r)
@@ -129,23 +127,29 @@ recovery_has_data(struct recovery_state *r)
 	return vclockset_first(&r->snap_dir.index) != NULL ||
 	       vclockset_first(&r->wal_dir.index) != NULL;
 }
-void recovery_bootstrap(struct recovery_state *r);
+
+void
+recovery_bootstrap(struct recovery_state *r);
+
 void
 recover_xlog(struct recovery_state *r, struct xlog *l);
-void recovery_follow_local(struct recovery_state *r,
-			   const char *name,
-			   ev_tstamp wal_dir_rescan_delay);
-void recovery_stop_local(struct recovery_state *r);
 
-void recovery_finalize(struct recovery_state *r, enum wal_mode mode,
-		       int rows_per_wal);
+void
+recovery_follow_local(struct recovery_state *r, const char *name,
+		      ev_tstamp wal_dir_rescan_delay);
+
+void
+recovery_stop_local(struct recovery_state *r);
 
-int64_t wal_write(struct recovery_state *r, struct xrow_header *packet);
+void
+recovery_finalize(struct recovery_state *r, enum wal_mode mode,
+		  int rows_per_wal);
 
-void recovery_setup_panic(struct recovery_state *r, bool on_snap_error, bool on_wal_error);
-void recovery_apply_row(struct recovery_state *r, struct xrow_header *packet);
+void
+recovery_fill_lsn(struct recovery_state *r, struct xrow_header *row);
 
-struct fio_batch;
+void
+recovery_apply_row(struct recovery_state *r, struct xrow_header *packet);
 
 /**
  * Return LSN of the most recent snapshot or -1 if there is
@@ -154,6 +158,12 @@ struct fio_batch;
 int64_t
 recovery_last_checkpoint(struct recovery_state *r);
 
+/**
+ * Ensure we don't corrupt the current WAL file in the child.
+ */
+void
+recovery_atfork(struct recovery_state *r);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/src/box/replication.cc b/src/box/relay.cc
similarity index 94%
rename from src/box/replication.cc
rename to src/box/relay.cc
index d376f44b963ce266c5d1f7315d82ac7905d592e9..0eb2dd0cd45d7fc80d04187814c2402e24f004c1 100644
--- a/src/box/replication.cc
+++ b/src/box/relay.cc
@@ -26,32 +26,33 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
-#include "replication.h"
+#include "relay.h"
 #include <say.h>
 #include <fiber.h>
+#include "scoped_guard.h"
 
 #include "recovery.h"
 #include "xlog.h"
 #include "iproto_constants.h"
-#include "box/engine.h"
-#include "box/cluster.h"
-#include "box/schema.h"
-#include "box/vclock.h"
-#include "scoped_guard.h"
+#include "engine.h"
+#include "cluster.h"
+#include "schema.h"
+#include "vclock.h"
 #include "xrow.h"
 #include "coeio.h"
 #include "coio.h"
 #include "cfg.h"
 #include "trigger.h"
+#include "errinj.h"
 
 void
-replication_send_row(struct recovery_state *r, void *param,
-                     struct xrow_header *packet);
+relay_send_row(struct recovery_state *r, void *param,
+	       struct xrow_header *packet);
 
 Relay::Relay(int fd_arg, uint64_t sync_arg)
 {
 	r = recovery_new(cfg_gets("snap_dir"), cfg_gets("wal_dir"),
-			 replication_send_row, this);
+			 relay_send_row, this);
 	recovery_setup_panic(r, cfg_geti("panic_on_snap_error"),
 			     cfg_geti("panic_on_wal_error"));
 
@@ -227,12 +228,16 @@ relay_send(Relay *relay, struct xrow_header *packet)
 	struct iovec iov[XROW_IOVMAX];
 	int iovcnt = xrow_to_iovec(packet, iov);
 	coio_writev(&relay->io, iov, iovcnt, 0);
+	ERROR_INJECT(ERRINJ_RELAY,
+	{
+		sleep(1000);
+	});
 }
 
 /** Send a single row to the client. */
 void
-replication_send_row(struct recovery_state *r, void *param,
-                     struct xrow_header *packet)
+relay_send_row(struct recovery_state *r, void *param,
+	       struct xrow_header *packet)
 {
 	Relay *relay = (Relay *) param;
 	assert(iproto_type_is_dml(packet->type));
diff --git a/src/box/replication.h b/src/box/relay.h
similarity index 92%
rename from src/box/replication.h
rename to src/box/relay.h
index 02873228efbeeac92a75584cb3494eb91f0cc28c..371f85a9ed9c84b0113626b3de12ea1473290d76 100644
--- a/src/box/replication.h
+++ b/src/box/relay.h
@@ -1,5 +1,5 @@
-#ifndef TARANTOOL_REPLICATION_H_INCLUDED
-#define TARANTOOL_REPLICATION_H_INCLUDED
+#ifndef TARANTOOL_REPLICATION_RELAY_H_INCLUDED
+#define TARANTOOL_REPLICATION_RELAY_H_INCLUDED
 /*
  * Redistribution and use in source and binary forms, with or
  * without modification, are permitted provided that the following
@@ -60,5 +60,5 @@ replication_subscribe(int fd, struct xrow_header *packet);
 void
 relay_send(Relay *relay, struct xrow_header *packet);
 
-#endif // TARANTOOL_REPLICATION_H_INCLUDED
+#endif /* TARANTOOL_REPLICATION_RELAY_H_INCLUDED */
 
diff --git a/src/box/replica.cc b/src/box/replica.cc
index cfd39d88045d5748c617198af9028c08fb93cfda..16a551c3eafe0d73f1a448d9fbbce6dcfa76bc7f 100644
--- a/src/box/replica.cc
+++ b/src/box/replica.cc
@@ -49,26 +49,26 @@ remote_read_row(struct ev_io *coio, struct iobuf *iobuf,
 	struct ibuf *in = &iobuf->in;
 
 	/* Read fixed header */
-	if (ibuf_size(in) < 1)
+	if (ibuf_used(in) < 1)
 		coio_breadn(coio, in, 1);
 
 	/* Read length */
-	if (mp_typeof(*in->pos) != MP_UINT) {
+	if (mp_typeof(*in->rpos) != MP_UINT) {
 		tnt_raise(ClientError, ER_INVALID_MSGPACK,
 			  "packet length");
 	}
-	ssize_t to_read = mp_check_uint(in->pos, in->end);
+	ssize_t to_read = mp_check_uint(in->rpos, in->wpos);
 	if (to_read > 0)
 		coio_breadn(coio, in, to_read);
 
-	uint32_t len = mp_decode_uint((const char **) &in->pos);
+	uint32_t len = mp_decode_uint((const char **) &in->rpos);
 
 	/* Read header and body */
-	to_read = len - ibuf_size(in);
+	to_read = len - ibuf_used(in);
 	if (to_read > 0)
 		coio_breadn(coio, in, to_read);
 
-	xrow_header_decode(row, (const char **) &in->pos, in->pos + len);
+	xrow_header_decode(row, (const char **) &in->rpos, in->rpos + len);
 }
 
 static void
@@ -134,7 +134,7 @@ replica_bootstrap(struct recovery_state *r)
 
 	struct ev_io coio;
 	coio_init(&coio);
-	struct iobuf *iobuf = iobuf_new(fiber_name(fiber()));
+	struct iobuf *iobuf = iobuf_new();
 	auto coio_guard = make_scoped_guard([&] {
 		iobuf_delete(iobuf);
 		evio_close(loop(), &coio);
@@ -207,7 +207,7 @@ pull_from_remote(va_list ap)
 {
 	struct recovery_state *r = va_arg(ap, struct recovery_state *);
 	struct ev_io coio;
-	struct iobuf *iobuf = iobuf_new(fiber_name(fiber()));
+	struct iobuf *iobuf = iobuf_new();
 	ev_loop *loop = loop();
 
 	coio_init(&coio);
@@ -221,7 +221,7 @@ pull_from_remote(va_list ap)
 		const char *err = NULL;
 		try {
 			struct xrow_header row;
-			if (! evio_is_active(&coio)) {
+			if (! evio_has_fd(&coio)) {
 				remote_set_status(&r->remote, "connecting");
 				err = "can't connect to master";
 				remote_connect(r, &coio, iobuf);
@@ -285,7 +285,7 @@ pull_from_remote(va_list ap)
 		 *
 		 * See: https://github.com/tarantool/tarantool/issues/136
 		*/
-		if (! evio_is_active(&coio))
+		if (! evio_has_fd(&coio))
 			fiber_sleep(RECONNECT_DELAY);
 	}
 }
@@ -325,7 +325,7 @@ recovery_stop_remote(struct recovery_state *r)
 	 * If the remote died from an exception, don't throw it
 	 * up.
 	 */
-	Exception::clear(&f->exception);
+	diag_clear(&f->diag);
 	fiber_join(f);
 	r->remote.status = "off";
 }
diff --git a/src/box/request.cc b/src/box/request.cc
index 3a1fa5ed5466d3b769e389891220fc3af17902ac..b127f63185abad237b99d5ecfda8543b16b05ad6 100644
--- a/src/box/request.cc
+++ b/src/box/request.cc
@@ -134,7 +134,7 @@ execute_upsert(struct request *request, struct port *port)
 	struct tuple *new_tuple =
 		tuple_upsert(space->format, region_alloc_cb,
 			     &fiber()->gc, old_tuple,
-			     request->extra_tuple, request->extra_tuple_end,
+			     request->default_tuple, request->default_tuple_end,
 			     request->tuple, request->tuple_end,
 			     request->index_base);
 	TupleGuard guard(new_tuple);
@@ -319,8 +319,8 @@ request_decode(struct request *request, const char *data, uint32_t len)
 			request->key = value;
 			request->key_end = data;
 		case IPROTO_DEF_TUPLE:
-			request->extra_tuple = value;
-			request->extra_tuple_end = data;
+			request->default_tuple = value;
+			request->default_tuple_end = data;
 		default:
 			break;
 		}
diff --git a/src/box/request.h b/src/box/request.h
index ec0a83e237e6860909ae9ff5e523d12aba94f49c..16d6d81d2a84de8c02b178621a35951708414b90 100644
--- a/src/box/request.h
+++ b/src/box/request.h
@@ -59,8 +59,8 @@ struct request
 	const char *tuple;
 	const char *tuple_end;
 	/** Additional tuple of request, currently used by UPSERT */
-	const char *extra_tuple;
-	const char *extra_tuple_end;
+	const char *default_tuple;
+	const char *default_tuple_end;
 	/** Base field offset for UPDATE, e.g. 0 for C and 1 for Lua. */
 	int index_base;
 };
diff --git a/src/box/sophia_engine.cc b/src/box/sophia_engine.cc
index 6bccf5578505147d0a1a289b0d11bbf6cd50bbe1..4c83d5563f7bd61e618e192e083890e6ff58d92f 100644
--- a/src/box/sophia_engine.cc
+++ b/src/box/sophia_engine.cc
@@ -26,7 +26,6 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
-#include "replication.h"
 #include "sophia_engine.h"
 #include "cfg.h"
 #include "xrow.h"
@@ -36,10 +35,10 @@
 #include "index.h"
 #include "sophia_index.h"
 #include "recovery.h"
+#include "relay.h"
 #include "space.h"
 #include "request.h"
 #include "iproto_constants.h"
-#include "replication.h"
 #include "salad/rlist.h"
 #include <sophia.h>
 #include <stdlib.h>
diff --git a/src/box/tuple.cc b/src/box/tuple.cc
index 58dc03718e3647cd7a1cc2f896f9dd243d7248a8..0589714b0fd4e1016aa50eca73e8dc4d36b0f52d 100644
--- a/src/box/tuple.cc
+++ b/src/box/tuple.cc
@@ -400,15 +400,16 @@ tuple_field_cstr(struct tuple *tuple, uint32_t i)
 
 struct tuple *
 tuple_update(struct tuple_format *format,
-	     void *(*region_alloc)(void *, size_t), void *alloc_ctx,
+	     tuple_update_alloc_func f, void *alloc_ctx,
 	     const struct tuple *old_tuple, const char *expr,
 	     const char *expr_end, int field_base)
 {
 	uint32_t new_size = 0;
-	const char *new_data = tuple_update_execute(region_alloc, alloc_ctx,
-					expr, expr_end, old_tuple->data,
-					old_tuple->data + old_tuple->bsize,
-					&new_size, field_base);
+	const char *new_data =
+		tuple_update_execute(f, alloc_ctx,
+				     expr, expr_end, old_tuple->data,
+				     old_tuple->data + old_tuple->bsize,
+				     &new_size, field_base);
 
 	/* Allocate a new tuple. */
 	assert(mp_typeof(*new_data) == MP_ARRAY);
diff --git a/src/box/tuple.h b/src/box/tuple.h
index a9eeec8f159871403bf0d63118bbcbbfb283f3ce..002eed68c101f1e2ed47261b3398603c18aa6e44 100644
--- a/src/box/tuple.h
+++ b/src/box/tuple.h
@@ -30,6 +30,7 @@
  */
 #include "trivia/util.h"
 #include "key_def.h" /* for enum field_type */
+#include "tuple_update.h"
 
 enum { FORMAT_ID_MAX = UINT16_MAX - 1, FORMAT_ID_NIL = UINT16_MAX };
 enum { FORMAT_REF_MAX = INT32_MAX, TUPLE_REF_MAX = UINT16_MAX };
@@ -474,7 +475,7 @@ tuple_init_field_map(struct tuple_format *format,
 
 struct tuple *
 tuple_update(struct tuple_format *new_format,
-	     void *(*region_alloc)(void *, size_t), void *alloc_ctx,
+	     tuple_update_alloc_func f, void *alloc_ctx,
 	     const struct tuple *old_tuple,
 	     const char *expr, const char *expr_end, int field_base);
 
diff --git a/src/box/tuple_update.cc b/src/box/tuple_update.cc
index d50aa2c15f830d60447edb6475ed1bfcafa346f1..8c633d890d33d35cbcb39186dd15961100042db3 100644
--- a/src/box/tuple_update.cc
+++ b/src/box/tuple_update.cc
@@ -91,7 +91,7 @@
 /** Update internal state */
 struct tuple_update
 {
-	region_alloc_func alloc;
+	tuple_update_alloc_func alloc;
 	void *alloc_ctx;
 	struct rope *rope;
 	struct update_op *ops;
@@ -938,7 +938,7 @@ upsert_do_ops(struct tuple_update *update)
 
 static void
 update_init(struct tuple_update *update,
-	    region_alloc_func alloc, void *alloc_ctx,
+	    tuple_update_alloc_func alloc, void *alloc_ctx,
 	    const char *old_data, const char *old_data_end,
 	    int index_base)
 {
@@ -964,7 +964,7 @@ update_finish(struct tuple_update *update, uint32_t *p_tuple_len)
 }
 
 const char *
-tuple_update_execute(region_alloc_func alloc, void *alloc_ctx,
+tuple_update_execute(tuple_update_alloc_func alloc, void *alloc_ctx,
 		     const char *expr,const char *expr_end,
 		     const char *old_data, const char *old_data_end,
 		     uint32_t *p_tuple_len, int index_base)
@@ -980,7 +980,7 @@ tuple_update_execute(region_alloc_func alloc, void *alloc_ctx,
 }
 
 const char *
-tuple_upsert_execute(region_alloc_func alloc, void *alloc_ctx,
+tuple_upsert_execute(tuple_update_alloc_func alloc, void *alloc_ctx,
 		     const char *expr,const char *expr_end,
 		     const char *old_data, const char *old_data_end,
 		     uint32_t *p_tuple_len, int index_base)
diff --git a/src/box/tuple_update.h b/src/box/tuple_update.h
index e80d2b00a4d56d3852d63c11c1d2519a85c42514..e847096275844fe2a0b25e26a4a1ded14d91f151 100644
--- a/src/box/tuple_update.h
+++ b/src/box/tuple_update.h
@@ -37,16 +37,18 @@ enum {
 	BOX_UPDATE_OP_CNT_MAX = 4000,
 };
 
-typedef void *(*region_alloc_func)(void *, size_t);
+extern "C" {
+typedef void *(*tuple_update_alloc_func)(void *, size_t);
+}
 
 const char *
-tuple_update_execute(region_alloc_func alloc, void *alloc_ctx,
+tuple_update_execute(tuple_update_alloc_func alloc, void *alloc_ctx,
 		     const char *expr,const char *expr_end,
 		     const char *old_data, const char *old_data_end,
 		     uint32_t *p_new_size, int index_base);
 
 const char *
-tuple_upsert_execute(region_alloc_func alloc, void *alloc_ctx,
+tuple_upsert_execute(tuple_update_alloc_func alloc, void *alloc_ctx,
 		     const char *expr,const char *expr_end,
 		     const char *old_data, const char *old_data_end,
 		     uint32_t *p_new_size, int index_base);
diff --git a/src/box/txn.cc b/src/box/txn.cc
index 4a87b2bc37272df405cddeea74802e86f21d36fb..fc03b7bcd0b7583b42cf5f9e33429c0e10d6e8bd 100644
--- a/src/box/txn.cc
+++ b/src/box/txn.cc
@@ -54,6 +54,7 @@ txn_add_redo(struct txn_stmt *stmt, struct request *request)
 	stmt->row = request->header;
 	if (request->header != NULL)
 		return;
+
 	/* Create a redo log row for Lua requests */
 	struct xrow_header *row= (struct xrow_header *)
 		region_alloc0(&fiber()->gc, sizeof(struct xrow_header));
@@ -144,17 +145,28 @@ txn_commit(struct txn *txn)
 	if (txn->engine)
 		txn->engine->prepare(txn);
 
+	struct wal_request *req = (struct wal_request *)
+		region_alloc(&fiber()->gc, sizeof(struct wal_request) +
+			     sizeof(struct xrow_header) * txn->n_stmts);
+	req->n_rows = 0;
+
 	rlist_foreach_entry(stmt, &txn->stmts, next) {
 		if (stmt->row == NULL)
 			continue;
+		/*
+		 * Bump current LSN even if wal_mode = NONE, so that
+		 * snapshots still works with WAL turned off.
+		 */
+		recovery_fill_lsn(recovery, stmt->row);
+		stmt->row->tm = ev_now(loop());
+		req->rows[req->n_rows++] = stmt->row;
+	}
+	if (req->n_rows) {
 		ev_tstamp start = ev_now(loop()), stop;
-		int64_t res = wal_write(recovery, stmt->row);
+		int64_t res = wal_write(recovery, req);
 		stop = ev_now(loop());
-		if (stop - start > too_long_threshold && stmt->row != NULL) {
-			say_warn("too long %s: %.3f sec",
-				 iproto_type_name(stmt->row->type),
-				 stop - start);
-		}
+		if (stop - start > too_long_threshold)
+			say_warn("too long WAL write: %.3f sec", stop - start);
 		if (res < 0)
 			tnt_raise(LoggedError, ER_WAL_IO);
 		txn->signature = res;
diff --git a/src/box/wal.cc b/src/box/wal.cc
new file mode 100644
index 0000000000000000000000000000000000000000..de7be5060f30ef77de36ae0c672726361109eb9d
--- /dev/null
+++ b/src/box/wal.cc
@@ -0,0 +1,464 @@
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "wal.h"
+
+#include "recovery.h"
+
+#include "fiber.h"
+#include "fio.h"
+#include "errinj.h"
+
+#include "xrow.h"
+
+const char *wal_mode_STRS[] = { "none", "write", "fsync", NULL };
+
+/*
+ * WAL writer - maintain a Write Ahead Log for every change
+ * in the data state.
+ */
+struct wal_writer
+{
+	struct cord cord;
+	struct cpipe tx_pipe;
+	struct cpipe wal_pipe;
+	struct cbus tx_wal_bus;
+	int rows_per_wal;
+	struct fio_batch *batch;
+	bool is_shutdown;
+	bool is_rollback;
+	ev_loop *txn_loop;
+	struct vclock vclock;
+};
+
+static void
+wal_flush_input(ev_loop * /* loop */, ev_async *watcher, int /* event */)
+{
+	struct cpipe *pipe = (struct cpipe *) watcher->data;
+
+	cbus_lock(pipe->bus);
+	bool input_was_empty = STAILQ_EMPTY(&pipe->pipe);
+	STAILQ_CONCAT(&pipe->pipe, &pipe->input);
+	cbus_unlock(pipe->bus);
+
+	if (input_was_empty)
+		cbus_signal(pipe->bus);
+	pipe->n_input = 0;
+}
+/**
+ * A commit watcher callback is invoked whenever there
+ * are requests in wal_writer->tx.pipe. This callback is
+ * associated with an internal WAL writer watcher and is
+ * invoked in the front-end main event loop.
+ *
+ * A rollback watcher callback is invoked only when there is
+ * a rollback request and commit is empty.
+ * We roll back the entire input queue.
+ *
+ * ev_async, under the hood, is a simple pipe. The WAL
+ * writer thread writes to that pipe whenever it's done
+ * handling a pack of requests (look for ev_async_send()
+ * call in the writer thread loop).
+ */
+static void
+tx_schedule_queue(struct cmsg_fifo *queue)
+{
+	/*
+	 * Can't use STAILQ_FOREACH since fiber_call()
+	 * destroys the list entry.
+	 */
+	struct cmsg *m, *tmp;
+	STAILQ_FOREACH_SAFE(m, queue, fifo, tmp)
+		fiber_call(((struct wal_request *) m)->fiber);
+}
+
+static void
+tx_fetch_output(ev_loop * /* loop */, ev_async *watcher, int /* event */)
+{
+	struct wal_writer *writer = (struct wal_writer *) watcher->data;
+	struct cmsg_fifo commit = STAILQ_HEAD_INITIALIZER(commit);
+	struct cmsg_fifo rollback = STAILQ_HEAD_INITIALIZER(rollback);
+
+	bool is_rollback;
+	cbus_lock(&writer->tx_wal_bus);
+	STAILQ_CONCAT(&commit, &writer->tx_pipe.pipe);
+	is_rollback = writer->is_rollback;
+	if (is_rollback)
+		STAILQ_CONCAT(&rollback, &writer->wal_pipe.pipe);
+	writer->is_rollback = false;
+	cbus_unlock(&writer->tx_wal_bus);
+	if (is_rollback)
+		STAILQ_CONCAT(&rollback, &writer->wal_pipe.input);
+
+	tx_schedule_queue(&commit);
+	/*
+	 * Perform a cascading abort of all transactions which
+	 * depend on the transaction which failed to get written
+	 * to the write ahead log. Abort transactions
+	 * in reverse order, performing a playback of the
+	 * in-memory database state.
+	 */
+	STAILQ_REVERSE(&rollback, cmsg, fifo);
+	tx_schedule_queue(&rollback);
+}
+
+/**
+ * Initialize WAL writer context. Even though it's a singleton,
+ * encapsulate the details just in case we may use
+ * more writers in the future.
+ */
+static void
+wal_writer_init(struct wal_writer *writer, struct vclock *vclock,
+		int rows_per_wal)
+{
+	cbus_create(&writer->tx_wal_bus);
+
+	cpipe_create(&writer->tx_pipe);
+	cpipe_set_fetch_cb(&writer->tx_pipe, tx_fetch_output, writer);
+
+	writer->rows_per_wal = rows_per_wal;
+
+	writer->batch = fio_batch_alloc(sysconf(_SC_IOV_MAX));
+
+	if (writer->batch == NULL)
+		panic_syserror("fio_batch_alloc");
+
+	/* Create and fill writer->cluster hash */
+	vclock_create(&writer->vclock);
+	vclock_copy(&writer->vclock, vclock);
+}
+
+/** Destroy a WAL writer structure. */
+static void
+wal_writer_destroy(struct wal_writer *writer)
+{
+	cpipe_destroy(&writer->tx_pipe);
+	cbus_destroy(&writer->tx_wal_bus);
+	free(writer->batch);
+}
+
+/** WAL writer thread routine. */
+static void wal_writer_f(va_list ap);
+
+/**
+ * Initialize WAL writer, start the thread.
+ *
+ * @pre   The server has completed recovery from a snapshot
+ *        and/or existing WALs. All WALs opened in read-only
+ *        mode are closed.
+ *
+ * @param state			WAL writer meta-data.
+ *
+ * @return 0 success, -1 on error. On success, recovery->writer
+ *         points to a newly created WAL writer.
+ */
+int
+wal_writer_start(struct recovery_state *r, int rows_per_wal)
+{
+	assert(r->writer == NULL);
+	assert(r->current_wal == NULL);
+	assert(rows_per_wal > 1);
+
+	static struct wal_writer wal_writer;
+
+	struct wal_writer *writer = &wal_writer;
+	r->writer = writer;
+
+	/* I. Initialize the state. */
+	wal_writer_init(writer, &r->vclock, rows_per_wal);
+
+	/* II. Start the thread. */
+
+	if (cord_costart(&writer->cord, "wal", wal_writer_f, r)) {
+		wal_writer_destroy(writer);
+		r->writer = NULL;
+		return -1;
+	}
+	cbus_join(&writer->tx_wal_bus, &writer->tx_pipe);
+	cpipe_set_flush_cb(&writer->wal_pipe, wal_flush_input,
+			   &writer->wal_pipe);
+	return 0;
+}
+
+/** Stop and destroy the writer thread (at shutdown). */
+void
+wal_writer_stop(struct recovery_state *r)
+{
+	struct wal_writer *writer = r->writer;
+
+	/* Stop the worker thread. */
+
+	cbus_lock(&writer->tx_wal_bus);
+	writer->is_shutdown= true;
+	cbus_unlock(&writer->tx_wal_bus);
+	cbus_signal(&writer->tx_wal_bus);
+	if (cord_join(&writer->cord)) {
+		/* We can't recover from this in any reasonable way. */
+		panic_syserror("WAL writer: thread join failed");
+	}
+
+	wal_writer_destroy(writer);
+
+	r->writer = NULL;
+}
+
+/**
+ * If there is no current WAL, try to open it, and close the
+ * previous WAL. We close the previous WAL only after opening
+ * a new one to smoothly move local hot standby and replication
+ * over to the next WAL.
+ * In case of error, we try to close any open WALs.
+ *
+ * @post r->current_wal is in a good shape for writes or is NULL.
+ * @return 0 in case of success, -1 on error.
+ */
+static int
+wal_opt_rotate(struct xlog **wal, struct recovery_state *r,
+	       struct vclock *vclock)
+{
+	struct xlog *l = *wal, *wal_to_close = NULL;
+
+	ERROR_INJECT_RETURN(ERRINJ_WAL_ROTATE);
+
+	if (l != NULL && l->rows >= r->writer->rows_per_wal) {
+		wal_to_close = l;
+		l = NULL;
+	}
+	if (l == NULL) {
+		/*
+		 * Close the file *before* we create the new WAL, to
+		 * make sure local hot standby/replication can see
+		 * EOF in the old WAL before switching to the new
+		 * one.
+		 */
+		if (wal_to_close) {
+			/*
+			 * We can not handle xlog_close()
+			 * failure in any reasonable way.
+			 * A warning is written to the server
+			 * log file.
+			 */
+			xlog_close(wal_to_close);
+			wal_to_close = NULL;
+		}
+		/* Open WAL with '.inprogress' suffix. */
+		l = xlog_create(&r->wal_dir, vclock);
+	}
+	assert(wal_to_close == NULL);
+	*wal = l;
+	return l ? 0 : -1;
+}
+
+static struct wal_request *
+wal_fill_batch(struct xlog *wal, struct fio_batch *batch, int rows_per_wal,
+	       struct wal_request *req)
+{
+	int max_rows = rows_per_wal - wal->rows;
+	/* Post-condition of successful wal_opt_rotate(). */
+	assert(max_rows > 0);
+	fio_batch_start(batch, max_rows);
+
+	while (req != NULL && batch->rows < batch->max_rows) {
+		int iovcnt = 0;
+		struct iovec *iov;
+		struct xrow_header **row = req->rows;
+		for (; row < req->rows + req->n_rows; row++) {
+			iov = fio_batch_book(batch, iovcnt, XROW_IOVMAX);
+			if (iov == NULL) {
+				/*
+				 * No space in the batch for
+				 * this transaction, open a new
+				 * batch for it and hope that it
+				 * is sufficient to hold it.
+				 */
+				return req;
+			}
+			iovcnt += xlog_encode_row(*row, iov);
+		}
+		fio_batch_add(batch, iovcnt);
+		req = (struct wal_request *) STAILQ_NEXT(req, fifo);
+	}
+	return req;
+}
+
+/**
+ * fio_batch_write() version with recovery specific
+ * error injection.
+ */
+static inline int
+wal_fio_batch_write(struct fio_batch *batch, int fd)
+{
+	ERROR_INJECT(ERRINJ_WAL_WRITE, return 0);
+	return fio_batch_write(batch, fd);
+}
+
+static struct wal_request *
+wal_write_batch(struct xlog *wal, struct fio_batch *batch,
+		struct wal_request *req, struct wal_request *end,
+		struct vclock *vclock)
+{
+	int rows_written = wal_fio_batch_write(batch, fileno(wal->f));
+	wal->rows += rows_written;
+	while (req != end && rows_written-- != 0)  {
+		vclock_follow(vclock,
+			      req->rows[req->n_rows - 1]->server_id,
+			      req->rows[req->n_rows - 1]->lsn);
+		req->res = 0;
+		req = (struct wal_request *) STAILQ_NEXT(req, fifo);
+	}
+	return req;
+}
+
+/**
+ * Pop a bulk of requests to write to disk to process.
+ * Block on the condition only if we have no other work to
+ * do. Loop in case of a spurious wakeup.
+ */
+void
+wal_writer_pop(struct wal_writer *writer)
+{
+	while (! writer->is_shutdown)
+	{
+		if (! writer->is_rollback &&
+		    ! STAILQ_EMPTY(&writer->wal_pipe.pipe)) {
+			STAILQ_CONCAT(&writer->wal_pipe.output,
+				      &writer->wal_pipe.pipe);
+			break;
+		}
+		cbus_wait_signal(&writer->tx_wal_bus);
+	}
+}
+
+
+static void
+wal_write_to_disk(struct recovery_state *r, struct wal_writer *writer,
+		  struct cmsg_fifo *input, struct cmsg_fifo *commit,
+		  struct cmsg_fifo *rollback)
+{
+	struct xlog **wal = &r->current_wal;
+	struct fio_batch *batch = writer->batch;
+
+	struct wal_request *req = (struct wal_request *) STAILQ_FIRST(input);
+	struct wal_request *write_end = req;
+
+	while (req) {
+		if (wal_opt_rotate(wal, r, &writer->vclock) != 0)
+			break;
+		struct wal_request *batch_end;
+		batch_end = wal_fill_batch(*wal, batch, writer->rows_per_wal,
+					   req);
+		if (batch_end == req)
+			break;
+		write_end = wal_write_batch(*wal, batch, req, batch_end,
+					    &writer->vclock);
+		if (batch_end != write_end)
+			break;
+		req = write_end;
+	}
+	fiber_gc();
+	STAILQ_SPLICE(input, write_end, fifo, rollback);
+	STAILQ_CONCAT(commit, input);
+}
+
+/** WAL writer thread main loop.  */
+static void
+wal_writer_f(va_list ap)
+{
+	struct recovery_state *r = va_arg(ap, struct recovery_state *);
+	struct wal_writer *writer = r->writer;
+
+	cpipe_create(&writer->wal_pipe);
+	cbus_join(&writer->tx_wal_bus, &writer->wal_pipe);
+
+	struct cmsg_fifo commit = STAILQ_HEAD_INITIALIZER(commit);
+	struct cmsg_fifo rollback = STAILQ_HEAD_INITIALIZER(rollback);
+
+	cbus_lock(&writer->tx_wal_bus);
+	while (! writer->is_shutdown) {
+		wal_writer_pop(writer);
+		cbus_unlock(&writer->tx_wal_bus);
+
+		wal_write_to_disk(r, writer, &writer->wal_pipe.output,
+				  &commit, &rollback);
+
+		cbus_lock(&writer->tx_wal_bus);
+		STAILQ_CONCAT(&writer->tx_pipe.pipe, &commit);
+		if (! STAILQ_EMPTY(&rollback)) {
+			/*
+			 * Begin rollback: create a rollback queue
+			 * from all requests which were not
+			 * written to disk and all requests in the
+			 * input queue.
+			 */
+			writer->is_rollback = true;
+			STAILQ_CONCAT(&rollback, &writer->wal_pipe.pipe);
+			STAILQ_CONCAT(&writer->wal_pipe.pipe, &rollback);
+		}
+		ev_async_send(writer->tx_pipe.consumer,
+			      &writer->tx_pipe.fetch_output);
+	}
+	cbus_unlock(&writer->tx_wal_bus);
+	if (r->current_wal != NULL) {
+		xlog_close(r->current_wal);
+		r->current_wal = NULL;
+	}
+	cpipe_destroy(&writer->wal_pipe);
+}
+
+/**
+ * WAL writer main entry point: queue a single request
+ * to be written to disk and wait until this task is completed.
+ */
+int64_t
+wal_write(struct recovery_state *r, struct wal_request *req)
+{
+	if (r->wal_mode == WAL_NONE)
+		return vclock_sum(&r->vclock);
+
+	ERROR_INJECT_RETURN(ERRINJ_WAL_IO);
+
+	struct wal_writer *writer = r->writer;
+
+	req->fiber = fiber();
+	req->res = -1;
+
+	cpipe_push(&writer->wal_pipe, req);
+	/**
+	 * It's not safe to spuriously wakeup this fiber
+	 * since in that case it will ignore a possible
+	 * error from WAL writer and not roll back the
+	 * transaction.
+	 */
+	bool cancellable = fiber_set_cancellable(false);
+	fiber_yield(); /* Request was inserted. */
+	fiber_set_cancellable(cancellable);
+	if (req->res == -1)
+		return -1;
+	return vclock_sum(&r->vclock);
+}
+
diff --git a/src/box/wal.h b/src/box/wal.h
new file mode 100644
index 0000000000000000000000000000000000000000..42ac8f2c8ffde39375de2aec8ecd1366f3cfd160
--- /dev/null
+++ b/src/box/wal.h
@@ -0,0 +1,60 @@
+#ifndef TARANTOOL_WAL_WRITER_H_INCLUDED
+#define TARANTOOL_WAL_WRITER_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include  <third_party/queue.h>
+#include <stdint.h>
+#include "cbus.h"
+
+struct fiber;
+struct recovery_state;
+
+enum wal_mode { WAL_NONE = 0, WAL_WRITE, WAL_FSYNC, WAL_MODE_MAX };
+
+/** String constants for the supported modes. */
+extern const char *wal_mode_STRS[];
+
+struct wal_request: public cmsg {
+	/* Auxiliary. */
+	int64_t res;
+	struct fiber *fiber;
+	int n_rows;
+	struct xrow_header *rows[];
+};
+
+int64_t
+wal_write(struct recovery_state *r, struct wal_request *req);
+
+int
+wal_writer_start(struct recovery_state *state, int rows_per_wal);
+
+void
+wal_writer_stop(struct recovery_state *r);
+
+#endif /* TARANTOOL_WAL_WRITER_H_INCLUDED */
diff --git a/src/box/xlog.cc b/src/box/xlog.cc
index 2b804114267371b48184f0eb7aaf7379d4dfced4..f89fe88654b189da0be48182f425882e79960eb0 100644
--- a/src/box/xlog.cc
+++ b/src/box/xlog.cc
@@ -54,9 +54,10 @@ static const log_magic_t eof_marker = mp_bswap_u32(0xd510aded); /* host byte ord
 static const char inprogress_suffix[] = ".inprogress";
 static const char v12[] = "0.12\n";
 
+const struct type type_XlogError = make_type("XlogError", &type_Exception);
 XlogError::XlogError(const char *file, unsigned line,
 		     const char *format, ...)
-	:Exception(file, line)
+	:Exception(&type_XlogError, file, line)
 {
 	va_list ap;
 	va_start(ap, format);
@@ -64,10 +65,23 @@ XlogError::XlogError(const char *file, unsigned line,
 	va_end(ap);
 }
 
+XlogError::XlogError(const struct type *type, const char *file, unsigned line,
+		     const char *format, ...)
+	:Exception(type, file, line)
+{
+	va_list ap;
+	va_start(ap, format);
+	vsnprintf(m_errmsg, sizeof(m_errmsg), format, ap);
+	va_end(ap);
+}
+
+const struct type type_XlogGapError =
+	make_type("XlogGapError", &type_XlogError);
+
 XlogGapError::XlogGapError(const char *file, unsigned line,
 			   const struct vclock *from,
 			   const struct vclock *to)
-	:XlogError(file, line, "")
+	:XlogError(&type_XlogGapError, file, line, "")
 {
 	char *s_from = vclock_to_string(from);
 	char *s_to = vclock_to_string(to);
@@ -128,6 +142,7 @@ vclockset_reset(vclockset_t *set)
 void
 xdir_destroy(struct xdir *dir)
 {
+	/** Free vclock objects allocated in xdir_scan(). */
 	vclockset_reset(&dir->index);
 }
 
@@ -434,12 +449,11 @@ row_reader(FILE *f, struct xrow_header *row)
 int
 xlog_encode_row(const struct xrow_header *row, struct iovec *iov)
 {
-	int iovcnt = xrow_header_encode(row, iov + 1) + 1;
-	char *fixheader = (char *) region_alloc(&fiber()->gc,
-						XLOG_FIXHEADER_SIZE);
-	uint32_t len = 0;
+	int iovcnt = xrow_header_encode(row, iov, XLOG_FIXHEADER_SIZE);
+	char *fixheader = (char *) iov[0].iov_base;
+	uint32_t len = iov[0].iov_len - XLOG_FIXHEADER_SIZE;
 	uint32_t crc32p = 0;
-	uint32_t crc32c = 0;
+	uint32_t crc32c = crc32_calc(0, fixheader + XLOG_FIXHEADER_SIZE, len);
 	for (int i = 1; i < iovcnt; i++) {
 		crc32c = crc32_calc(crc32c, (const char *) iov[i].iov_base,
 				    iov[i].iov_len);
@@ -459,8 +473,6 @@ xlog_encode_row(const struct xrow_header *row, struct iovec *iov)
 	if (padding > 0)
 		data = mp_encode_strl(data, padding - 1) + padding - 1;
 	assert(data == fixheader + XLOG_FIXHEADER_SIZE);
-	iov[0].iov_base = fixheader;
-	iov[0].iov_len = XLOG_FIXHEADER_SIZE;
 
 	assert(iovcnt <= XROW_IOVMAX);
 	return iovcnt;
diff --git a/src/box/xlog.h b/src/box/xlog.h
index 8ddea5574212dbf01c4c3799d007188ec1d1ae15..11ee522b2622622fe0dec6dc21990b78ed6bd70c 100644
--- a/src/box/xlog.h
+++ b/src/box/xlog.h
@@ -43,6 +43,9 @@ struct XlogError: public Exception
 {
 	XlogError(const char *file, unsigned line,
 		  const char *format, ...);
+protected:
+	XlogError(const struct type *type, const char *file, unsigned line,
+		  const char *format, ...);
 };
 
 struct XlogGapError: public XlogError
diff --git a/src/box/xrow.cc b/src/box/xrow.cc
index 4200bbb69888a9a4c8682616419809250616009e..f1c112329bf5affc5c904068acf86536d6264c57 100644
--- a/src/box/xrow.cc
+++ b/src/box/xrow.cc
@@ -105,10 +105,13 @@ xrow_decode_uuid(const char **pos, struct tt_uuid *out)
 }
 
 int
-xrow_header_encode(const struct xrow_header *header, struct iovec *out)
+xrow_header_encode(const struct xrow_header *header, struct iovec *out,
+		   size_t fixheader_len)
 {
 	/* allocate memory for sign + header */
-	char *data = (char *) region_alloc(&fiber()->gc, HEADER_LEN_MAX);
+	out->iov_base = region_alloc(&fiber()->gc, HEADER_LEN_MAX +
+				     fixheader_len);
+	char *data = (char *) out->iov_base + fixheader_len;
 
 	/* Header */
 	char *d = data + 1; /* Skip 1 byte for MP_MAP */
@@ -118,7 +121,6 @@ xrow_header_encode(const struct xrow_header *header, struct iovec *out)
 		d = mp_encode_uint(d, header->type);
 		map_size++;
 	}
-/* https://github.com/tarantool/tarantool/issues/881 */
 #if 0
 	if (header->sync) {
 		d = mp_encode_uint(d, IPROTO_SYNC);
@@ -126,6 +128,7 @@ xrow_header_encode(const struct xrow_header *header, struct iovec *out)
 		map_size++;
 	}
 #endif
+
 	if (header->server_id) {
 		d = mp_encode_uint(d, IPROTO_SERVER_ID);
 		d = mp_encode_uint(d, header->server_id);
@@ -146,8 +149,7 @@ xrow_header_encode(const struct xrow_header *header, struct iovec *out)
 
 	assert(d <= data + HEADER_LEN_MAX);
 	mp_encode_map(data, map_size);
-	out->iov_base = data;
-	out->iov_len = (d - data);
+	out->iov_len = d - (char *) out->iov_base;
 	out++;
 
 	memcpy(out, header->body, sizeof(*out) * header->bodycnt);
@@ -165,18 +167,15 @@ int
 xrow_to_iovec(const struct xrow_header *row, struct iovec *out)
 {
 	static const int iov0_len = mp_sizeof_uint(UINT32_MAX);
-	int iovcnt = xrow_header_encode(row, out + 1) + 1;
-	char *fixheader = (char *) region_alloc(&fiber()->gc, iov0_len);
-	uint32_t len = 0;
-	for (int i = 1; i < iovcnt; i++)
+	int iovcnt = xrow_header_encode(row, out, iov0_len);
+	ssize_t len = -iov0_len;
+	for (int i = 0; i < iovcnt; i++)
 		len += out[i].iov_len;
 
 	/* Encode length */
-	char *data = fixheader;
+	char *data = (char *) out[0].iov_base;
 	*(data++) = 0xce; /* MP_UINT32 */
 	*(uint32_t *) data = mp_bswap_u32(len);
-	out[0].iov_base = fixheader;
-	out[0].iov_len = iov0_len;
 
 	assert(iovcnt <= XROW_IOVMAX);
 	return iovcnt;
@@ -222,7 +221,7 @@ xrow_decode_error(struct xrow_header *row)
 {
 	uint32_t code = row->type & (IPROTO_TYPE_ERROR - 1);
 
-	char error[TNT_ERRMSG_MAX] = { 0 };
+	char error[EXCEPTION_ERRMSG_MAX] = { 0 };
 	const char *pos;
 	uint32_t map_size;
 
diff --git a/src/box/xrow.h b/src/box/xrow.h
index 9cf1327779c3f39ba2964b68510cb2fe98d3ab0d..537fa335b9982163121bf47e2042daf3eb9cdc24 100644
--- a/src/box/xrow.h
+++ b/src/box/xrow.h
@@ -39,7 +39,7 @@ extern "C" {
 enum {
 	XROW_HEADER_IOVMAX = 1,
 	XROW_BODY_IOVMAX = 2,
-	XROW_IOVMAX = XROW_HEADER_IOVMAX + XROW_BODY_IOVMAX + 1
+	XROW_IOVMAX = XROW_HEADER_IOVMAX + XROW_BODY_IOVMAX
 };
 
 struct xrow_header {
@@ -66,7 +66,7 @@ xrow_encode_uuid(char *pos, const struct tt_uuid *in);
 
 int
 xrow_header_encode(const struct xrow_header *header,
-		   struct iovec *out);
+		   struct iovec *out, size_t fixheader_len);
 
 int
 xrow_to_iovec(const struct xrow_header *row, struct iovec *out);
diff --git a/src/cbus.cc b/src/cbus.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5615ed703c961ad44cf70e84eb634bf79f8ae847
--- /dev/null
+++ b/src/cbus.cc
@@ -0,0 +1,308 @@
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "cbus.h"
+#include "scoped_guard.h"
+
+static void
+cbus_flush_cb(ev_loop * /* loop */, struct ev_async *watcher,
+	      int /* events */);
+
+static void
+cpipe_fetch_output_cb(ev_loop * /* loop */, struct ev_async *watcher,
+			int /* events */)
+{
+	struct cpipe *pipe = (struct cpipe *) watcher->data;
+	struct cmsg *msg;
+	/* Force an exchange if there is nothing to do. */
+	while (cpipe_peek(pipe)) {
+		while ((msg = cpipe_pop_output(pipe)))
+			cmsg_deliver(msg);
+	}
+}
+
+void
+cpipe_create(struct cpipe *pipe)
+{
+	STAILQ_INIT(&pipe->pipe);
+	STAILQ_INIT(&pipe->input);
+	STAILQ_INIT(&pipe->output);
+
+	pipe->n_input = 0;
+	pipe->max_input = INT_MAX;
+
+	ev_async_init(&pipe->flush_input, cbus_flush_cb);
+	pipe->flush_input.data = pipe;
+
+	ev_async_init(&pipe->fetch_output, cpipe_fetch_output_cb);
+	pipe->fetch_output.data = pipe;
+	pipe->consumer = loop();
+	pipe->producer = NULL; /* set in join() under a mutex */
+	ev_async_start(pipe->consumer, &pipe->fetch_output);
+}
+
+void
+cpipe_destroy(struct cpipe *pipe)
+{
+	assert(loop() == pipe->consumer);
+	ev_async_stop(pipe->consumer, &pipe->fetch_output);
+}
+
+static void
+cpipe_join(struct cpipe *pipe, struct cbus *bus, struct cpipe *peer)
+{
+	assert(loop() == pipe->consumer);
+	pipe->bus = bus;
+	pipe->peer = peer;
+	pipe->producer = peer->consumer;
+}
+
+void
+cbus_create(struct cbus *bus)
+{
+	bus->pipe[0] = bus->pipe[1] = NULL;
+
+	pthread_mutexattr_t errorcheck;
+
+	(void) tt_pthread_mutexattr_init(&errorcheck);
+
+#ifndef NDEBUG
+	(void) tt_pthread_mutexattr_settype(&errorcheck,
+					    PTHREAD_MUTEX_ERRORCHECK);
+#endif
+	/* Initialize queue lock mutex. */
+	(void) tt_pthread_mutex_init(&bus->mutex, &errorcheck);
+	(void) tt_pthread_mutexattr_destroy(&errorcheck);
+
+	(void) tt_pthread_cond_init(&bus->cond, NULL);
+}
+
+void
+cbus_destroy(struct cbus *bus)
+{
+	(void) tt_pthread_mutex_destroy(&bus->mutex);
+	(void) tt_pthread_cond_destroy(&bus->cond);
+}
+
+/**
+ * @pre both consumers initialized their pipes
+ * @post each consumers gets the input end of the opposite pipe
+ */
+struct cpipe *
+cbus_join(struct cbus *bus, struct cpipe *pipe)
+{
+	/*
+	 * We can't let one or the other thread go off and
+	 * produce events/send ev_async callback messages
+	 * until the peer thread has initialized the async
+	 * and started it.
+	 * Use a condition variable to make sure that two
+	 * threads operate in sync.
+	 */
+	cbus_lock(bus);
+	int pipe_idx = bus->pipe[0] != NULL;
+	int peer_idx = !pipe_idx;
+	bus->pipe[pipe_idx] = pipe;
+	while (bus->pipe[peer_idx] == NULL)
+		cbus_wait_signal(bus);
+
+	cpipe_join(pipe, bus, bus->pipe[peer_idx]);
+	cbus_signal(bus);
+	/*
+	 * At this point we've have both pipes initialized
+	 * in bus->pipe array, and our pipe joined.
+	 * But the other pipe may have not been joined
+	 * yet, ensure it's fully initialized before return.
+	 */
+	while (bus->pipe[peer_idx]->producer == NULL) {
+		/* Let the other side wakeup and perform the join. */
+		cbus_wait_signal(bus);
+	}
+	cbus_unlock(bus);
+	/*
+	 * POSIX: pthread_cond_signal() function shall
+	 * have no effect if there are no threads currently
+	 * blocked on cond.
+	 */
+	cbus_signal(bus);
+	return bus->pipe[peer_idx];
+}
+
+static void
+cbus_flush_cb(ev_loop * /* loop */, struct ev_async *watcher,
+	      int /* events */)
+{
+	struct cpipe *pipe = (struct cpipe *) watcher->data;
+	if (pipe->n_input == 0)
+		return;
+	struct cpipe *peer = pipe->peer;
+	assert(pipe->producer == loop());
+	assert(peer->consumer == loop());
+
+	/* Trigger task processing when the queue becomes non-empty. */
+	bool pipe_was_empty;
+	bool peer_output_was_empty = STAILQ_EMPTY(&peer->output);
+
+	cbus_lock(pipe->bus);
+	pipe_was_empty = !ev_async_pending(&pipe->fetch_output);
+	/** Flush input */
+	STAILQ_CONCAT(&pipe->pipe, &pipe->input);
+	/*
+	 * While at it, pop output.
+	 * The consumer of the output of the bound queue is the
+	 * same as the producer of input, so we can safely access it.
+	 * We can safely access queue because it's locked.
+	 */
+	STAILQ_CONCAT(&peer->output, &peer->pipe);
+	cbus_unlock(pipe->bus);
+
+	pipe->n_input = 0;
+	if (pipe_was_empty)
+		ev_async_send(pipe->consumer, &pipe->fetch_output);
+	if (peer_output_was_empty && !STAILQ_EMPTY(&peer->output))
+		ev_feed_event(peer->consumer, &peer->fetch_output, EV_CUSTOM);
+}
+
+struct cmsg *
+cpipe_peek_impl(struct cpipe *pipe)
+{
+	assert(STAILQ_EMPTY(&pipe->output));
+
+	struct cpipe *peer = pipe->peer;
+	assert(pipe->consumer == loop());
+	assert(peer->producer == loop());
+
+	bool peer_pipe_was_empty = false;
+	cbus_lock(pipe->bus);
+	STAILQ_CONCAT(&pipe->output, &pipe->pipe);
+	if (! STAILQ_EMPTY(&peer->input)) {
+		peer_pipe_was_empty = !ev_async_pending(&peer->fetch_output);
+		STAILQ_CONCAT(&peer->pipe, &peer->input);
+	}
+	cbus_unlock(pipe->bus);
+	peer->n_input = 0;
+
+	if (peer_pipe_was_empty)
+		ev_async_send(peer->consumer, &peer->fetch_output);
+	return STAILQ_FIRST(&pipe->output);
+}
+
+
+static void
+cmsg_notify_deliver(struct cmsg *msg)
+{
+	fiber_wakeup(((struct cmsg_notify *) msg)->fiber);
+}
+
+void
+cmsg_notify_init(struct cmsg_notify *msg)
+{
+	static cmsg_hop route[] = { { cmsg_notify_deliver, NULL }, };
+
+	cmsg_init(msg, route);
+	msg->fiber = fiber();
+}
+
+
+/** Return true if there are too many active workers in the pool. */
+static inline bool
+cpipe_fiber_pool_needs_throttling(struct cpipe_fiber_pool *pool)
+{
+	return pool->size > pool->max_size;
+}
+
+/**
+ * Main function of the fiber invoked to handle all outstanding
+ * tasks in a queue.
+ */
+static void
+cpipe_fiber_pool_f(va_list ap)
+{
+	struct cpipe_fiber_pool *pool = va_arg(ap, struct cpipe_fiber_pool *);
+	struct cpipe *pipe = pool->pipe;
+	struct cmsg *msg;
+	pool->size++;
+	auto size_guard = make_scoped_guard([=]{ pool->size--; });
+restart:
+	while ((msg = cpipe_pop_output(pipe)))
+		cmsg_deliver(msg);
+
+	if (pool->cache_size < 2 * pool->max_size) {
+		/** Put the current fiber into a fiber cache. */
+		rlist_add_entry(&pool->fiber_cache, fiber(), state);
+		pool->size--;
+		pool->cache_size++;
+		fiber_yield();
+		pool->cache_size--;
+		pool->size++;
+		goto restart;
+	}
+}
+
+
+/** Create fibers to handle all outstanding tasks. */
+static void
+cpipe_fiber_pool_cb(ev_loop * /* loop */, struct ev_async *watcher,
+		  int /* events */)
+{
+	struct cpipe_fiber_pool *pool = (struct cpipe_fiber_pool *) watcher->data;
+	struct cpipe *pipe = pool->pipe;
+	(void) cpipe_peek(pipe);
+	while (! STAILQ_EMPTY(&pipe->output)) {
+		struct fiber *f;
+		if (! rlist_empty(&pool->fiber_cache)) {
+			f = rlist_shift_entry(&pool->fiber_cache,
+					      struct fiber, state);
+			fiber_call(f);
+		} else if (! cpipe_fiber_pool_needs_throttling(pool)) {
+			f = fiber_new(pool->name, cpipe_fiber_pool_f);
+			fiber_start(f, pool);
+		} else {
+			/**
+			 * No worries that this watcher may not
+			 * get scheduled again - there are enough
+			 * worker fibers already, so just leave.
+			 */
+			break;
+		}
+	}
+}
+
+void
+cpipe_fiber_pool_create(struct cpipe_fiber_pool *pool,
+			const char *name, struct cpipe *pipe,
+			int max_pool_size)
+{
+	rlist_create(&pool->fiber_cache);
+	pool->name = name;
+	pool->pipe = pipe;
+	pool->size = 0;
+	pool->cache_size = 0;
+	pool->max_size = max_pool_size;
+	cpipe_set_fetch_cb(pipe, cpipe_fiber_pool_cb, pool);
+}
diff --git a/src/cbus.h b/src/cbus.h
new file mode 100644
index 0000000000000000000000000000000000000000..02b87bc3a6c6fd17504b07da418a4a9527e6b284
--- /dev/null
+++ b/src/cbus.h
@@ -0,0 +1,472 @@
+#ifndef TARANTOOL_CBUS_H_INCLUDED
+#define TARANTOOL_CBUS_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "fiber.h"
+#include "coio.h"
+
+/** cbus, cmsg - inter-cord bus and messaging */
+
+struct cmsg;
+struct cpipe;
+typedef void (*cmsg_f)(struct cmsg *);
+
+/**
+ * One hop in a message travel route.  A message may need to be
+ * delivered to many destinations before it can be dispensed with.
+ * For example, it may be necessary to return a message to the
+ * sender just to destroy it.
+ *
+ * Message travel route is an array of cmsg_hop entries. The first
+ * entry contains a delivery function at the first destination,
+ * and the next destination. Subsequent entries are alike. The
+ * last entry has a delivery function (most often a message
+ * destructor) and NULL for the next destination.
+ */
+struct cmsg_hop {
+	/** The message delivery function. */
+	cmsg_f f;
+	/**
+	 * The next destination to which the message
+	 * should be routed after its delivered locally.
+	 */
+	struct cpipe *pipe;
+};
+
+/** A message traveling between cords. */
+struct cmsg {
+	/**
+	 * A member of the linked list - fifo of the pipe the
+	 * message is stuck in currently, waiting to get
+	 * delivered.
+	 */
+	STAILQ_ENTRY(cmsg) fifo;
+	/** The message routing path. */
+	struct cmsg_hop *route;
+	/** The current hop the message is at. */
+	struct cmsg_hop *hop;
+};
+
+/** Initialize the message and set its route. */
+static inline void
+cmsg_init(struct cmsg *msg, struct cmsg_hop *route)
+{
+	/**
+	 * The first hop can be done explicitly with cbus_push(),
+	 * msg->hop thus points to the second hop.
+	 */
+	msg->hop = msg->route = route;
+}
+
+STAILQ_HEAD(cmsg_fifo, cmsg);
+
+#define CACHELINE_SIZE 64
+/** A  uni-directional FIFO queue from one cord to another. */
+struct cpipe {
+	/**
+	 * The protected part of the pipe: only accessible
+	 * in a critical section.
+	 * The message flow in the pipe is:
+	 *     input       <-- owned by the consumer thread
+	 *       v
+	 *     pipe        <-- shared, protected by a mutex
+	 *       v
+	 *     output      <-- owned by the producer thread
+	 */
+	struct {
+		struct cmsg_fifo pipe;
+		/** Peer pipe - the other direction of the bus. */
+		struct cpipe *peer;
+		struct cbus *bus;
+	} __attribute__((aligned(CACHELINE_SIZE)));
+	/** Stuff most actively used in the producer thread. */
+	struct {
+		/** Staging area for pushed messages */
+		struct cmsg_fifo input;
+		/** Counters are useful for finer-grained scheduling. */
+		int n_input;
+		/**
+		 * When pushing messages, keep the staged input size under
+		 * this limit (speeds up message delivery and reduces
+		 * latency, while still keeping the bus mutex cold enough).
+		*/
+		int max_input;
+		/**
+		 * Rather than flushing input into the pipe
+		 * whenever a single message or a batch is
+		 * complete, do it once per event loop iteration.
+		 */
+		struct ev_async flush_input;
+		/** The producer thread. */
+		struct ev_loop *producer;
+		/** The consumer thread. */
+		struct ev_loop *consumer;
+	} __attribute__((aligned(CACHELINE_SIZE)));
+	/** Stuff related to the consumer. */
+	struct {
+		/** Staged messages (for pop) */
+		struct cmsg_fifo output;
+		/**
+		 * Used to trigger task processing when
+		 * the pipe becomes non-empty.
+		 */
+		struct ev_async fetch_output;
+	} __attribute__((aligned(CACHELINE_SIZE)));
+};
+
+#undef CACHELINE_SIZE
+
+/**
+ * Initialize a pipe. Must be called by the consumer.
+ */
+void
+cpipe_create(struct cpipe *pipe);
+
+void
+cpipe_destroy(struct cpipe *pipe);
+
+/**
+ * Reset the default fetch output callback with a custom one.
+ */
+static inline void
+cpipe_set_fetch_cb(struct cpipe *pipe, ev_async_cb fetch_output_cb,
+		   void *data)
+{
+	assert(loop() == pipe->consumer);
+	/*
+	 * According to libev documentation, you can set cb at
+	 * virtually any time, modulo threads.
+	 */
+	ev_set_cb(&pipe->fetch_output, fetch_output_cb);
+	pipe->fetch_output.data = data;
+}
+
+/**
+ * Reset the default fetch output callback with a custom one.
+ */
+static inline void
+cpipe_set_flush_cb(struct cpipe *pipe, ev_async_cb flush_input_cb,
+		   void *data)
+{
+	assert(loop() == pipe->producer);
+	/*
+	 * According to libev documentation, you can set cb at
+	 * virtually any time, modulo threads.
+	 */
+	ev_set_cb(&pipe->flush_input, flush_input_cb);
+	pipe->flush_input.data = data;
+}
+
+/**
+ * Pop a single message from the staged output area. If
+ * the output is empty, returns NULL. There may be messages
+ * in the pipe!
+ */
+static inline struct cmsg *
+cpipe_pop_output(struct cpipe *pipe)
+{
+	assert(loop() == pipe->consumer);
+
+	if (STAILQ_EMPTY(&pipe->output))
+		return NULL;
+	struct cmsg *msg = STAILQ_FIRST(&pipe->output);
+	STAILQ_REMOVE_HEAD(&pipe->output, fifo);
+	return msg;
+}
+
+struct cmsg *
+cpipe_peek_impl(struct cpipe *pipe);
+
+/**
+ * Check if the pipe has any messages. Triggers a bus
+ * exchange in a critical section if the pipe is empty.
+ */
+static inline struct cmsg *
+cpipe_peek(struct cpipe *pipe)
+{
+	assert(loop() == pipe->consumer);
+
+	if (STAILQ_EMPTY(&pipe->output))
+		return cpipe_peek_impl(pipe);
+
+	return STAILQ_FIRST(&pipe->output);
+}
+
+/**
+ * Pop a single message. Triggers a bus exchange
+ * if the pipe is empty.
+ */
+static inline struct cmsg *
+cpipe_pop(struct cpipe *pipe)
+{
+	if (cpipe_peek(pipe) == NULL)
+		return NULL;
+	return cpipe_pop_output(pipe);
+}
+
+/**
+ * Set pipe max size of staged push area. The default is infinity.
+ * If staged push cap is set, the pushed messages are flushed
+ * whenever the area has more messages than the cap, and also once
+ * per event loop.
+ * Otherwise, the messages flushed once per event loop iteration.
+ *
+ * @todo: collect bus stats per second and adjust max_input once
+ * a second to keep the mutex cold regardless of the message load,
+ * while still keeping the latency low if there are few
+ * long-to-process messages.
+ */
+static inline void
+cpipe_set_max_input(struct cpipe *pipe, int max_input)
+{
+	assert(loop() == pipe->producer);
+	pipe->max_input = max_input;
+}
+
+/**
+ * Flush all staged messages into the pipe and eventually to the
+ * consumer.
+ */
+static inline void
+cpipe_flush_input(struct cpipe *pipe)
+{
+	assert(loop() == pipe->producer);
+
+	/** Flush may be called with no input. */
+	if (pipe->n_input > 0) {
+		if (pipe->n_input < pipe->max_input) {
+			/*
+			 * Not much input, can deliver all
+			 * messages at the end of the event loop
+			 * iteration.
+			 */
+			ev_feed_event(pipe->producer,
+				      &pipe->flush_input, EV_CUSTOM);
+		} else {
+			/*
+			 * Wow, it's a lot of stuff piled up,
+			 * deliver immediately.
+			 */
+			ev_invoke(pipe->producer,
+				  &pipe->flush_input, EV_CUSTOM);
+		}
+	}
+}
+
+/**
+ * Push a single message to the pipe input. The message is pushed
+ * to a staging area. To be delivered, the input needs to be
+ * flushed with cpipe_flush_input().
+ */
+static inline void
+cpipe_push_input(struct cpipe *pipe, struct cmsg *msg)
+{
+	assert(loop() == pipe->producer);
+
+	STAILQ_INSERT_TAIL(&pipe->input, msg, fifo);
+	pipe->n_input++;
+	if (pipe->n_input >= pipe->max_input)
+		ev_invoke(pipe->producer, &pipe->flush_input, EV_CUSTOM);
+}
+
+/**
+ * Push a single message and ensure it's delivered.
+ * A combo of push_input + flush_input for cases when
+ * it's not known at all whether there'll be other
+ * messages coming up.
+ */
+static inline void
+cpipe_push(struct cpipe *pipe, struct cmsg *msg)
+{
+	cpipe_push_input(pipe, msg);
+	assert(pipe->n_input < pipe->max_input);
+	if (pipe->n_input == 1)
+		ev_feed_event(pipe->producer, &pipe->flush_input, EV_CUSTOM);
+}
+
+/**
+ * Cord interconnect: two pipes one for each message flow
+ * direction.
+ */
+struct cbus {
+	/** Two pipes for two directions between two cords. */
+	struct cpipe *pipe[2];
+	/**
+	 * A single mutex to protect all exchanges around the
+	 * two pipes involved in the bus.
+	 */
+	pthread_mutex_t mutex;
+	/** Condition for synchronized start of the bus. */
+	pthread_cond_t cond;
+};
+
+void
+cbus_create(struct cbus *bus);
+
+void
+cbus_destroy(struct cbus *bus);
+
+/**
+ * Connect the pipes: join cord1 input to the cord2 output,
+ * and cord1 output to cord2 input.
+ * Each cord must invoke this method in its own scope,
+ * and provide its own callback to handle incoming messages
+ * (pop_output_cb).
+ * This call synchronizes two threads, and after this method
+ * returns, cpipe_push/cpipe_pop are safe to use.
+ *
+ * @param  bus the bus
+ * @param  pipe the pipe for which this thread is a consumer
+ *
+ * @retval the pipe for this this thread is a producer
+ *
+ * @example:
+ *	cpipe_create(&in);
+ *	struct cpipe *out = cbus_join(bus, &in);
+ *	cpipe_set_max_input(out, 128);
+ *	cpipe_push(out, msg);
+ */
+struct cpipe *
+cbus_join(struct cbus *bus, struct cpipe *pipe);
+
+/**
+ * Lock the bus. Ideally should never be used
+ * outside cbus code.
+ */
+static inline void
+cbus_lock(struct cbus *bus)
+{
+	tt_pthread_mutex_lock(&bus->mutex);
+}
+
+/** Unlock the bus. */
+static inline void
+cbus_unlock(struct cbus *bus)
+{
+	tt_pthread_mutex_unlock(&bus->mutex);
+}
+
+static inline void
+cbus_signal(struct cbus *bus)
+{
+	tt_pthread_cond_signal(&bus->cond);
+}
+
+static inline void
+cbus_wait_signal(struct cbus *bus)
+{
+	tt_pthread_cond_wait(&bus->cond, &bus->mutex);
+}
+
+/**
+ * Dispatch the message to the next hop.
+ */
+static inline void
+cmsg_dispatch(struct cpipe *pipe, struct cmsg *msg)
+{
+	/**
+	 * 'pipe' pointer saved in class constructor works as
+	 * a guard that the message is alive. If a message route
+	 * has the next pipe, then the message mustn't have been
+	 * destroyed on this hop. Otherwise msg->hop->pipe could
+	 * be already pointing to garbage.
+	 */
+	if (pipe) {
+		/*
+		 * Once we pushed the message to the bus,
+		 * we relinquished all write access to it,
+		 * so we must increase the current hop *before*
+		 * push.
+		 */
+		msg->hop++;
+		cpipe_push(pipe, msg);
+	}
+}
+
+struct CmsgDispatchGuard {
+	struct cpipe *pipe;
+	struct cmsg *msg;
+	CmsgDispatchGuard(struct cmsg *msg_arg)
+		:pipe(msg_arg->hop->pipe), msg(msg_arg) {}
+	~CmsgDispatchGuard() { cmsg_dispatch(pipe, msg); }
+};
+
+/**
+ * Deliver the message and dispatch it to the next hop.
+ */
+static inline void
+cmsg_deliver(struct cmsg *msg)
+{
+	/**
+	 * Ensure dispatch happens even if there is an exception,
+	 * otherwise the message may leak.
+	 */
+	CmsgDispatchGuard guard(msg);
+	msg->hop->f(msg);
+}
+
+/**
+ * A helper message to wakeup caller whenever an event
+ * occurs.
+ */
+struct cmsg_notify: public cmsg
+{
+	struct fiber *fiber;
+};
+
+void
+cmsg_notify_init(struct cmsg_notify *msg);
+
+/**
+ * A pool of worker fibers to handle messages,
+ * so that each message is handled in its own fiber.
+ */
+struct cpipe_fiber_pool {
+	const char *name;
+	/** Cache of fibers which work on incoming messages. */
+	struct rlist fiber_cache;
+	/** The number of active fibers working on tasks. */
+	int size;
+	/** The number of sleeping fibers, in the cache */
+	int cache_size;
+	/** The limit on the number of fibers working on tasks. */
+	int max_size;
+	struct cpipe *pipe;
+};
+
+/**
+ * Initialize a fiber pool and connect it to a pipe. Currently
+ * must be done before the pipe is actively used by a bus.
+ */
+void
+cpipe_fiber_pool_create(struct cpipe_fiber_pool *pool,
+			const char *name, struct cpipe *pipe,
+			int max_pool_size);
+
+#endif /* TARANTOOL_CBUS_H_INCLUDED */
diff --git a/src/coeio.cc b/src/coeio.cc
index e2cbc2f12c8f3ca7909f0cb7a04351936dd27020..d25affc54ecf42044e1fa086c6202f19380af122 100644
--- a/src/coeio.cc
+++ b/src/coeio.cc
@@ -363,11 +363,11 @@ cord_cojoin(struct cord *cord)
 {
 	assert(cord() != cord); /* Can't join self. */
 	int rc = coio_call(cord_cojoin_cb, cord);
-	if (rc == 0 && cord->fiber->exception) {
-		Exception::move(&cord->fiber->exception, &fiber()->exception);
+	if (rc == 0 && !diag_is_empty(&cord->fiber->diag)) {
+		diag_move(&cord->fiber->diag, &fiber()->diag);
 		cord_destroy(cord);
 		 /* re-throw exception in this fiber */
-		fiber()->exception->raise();
+		diag_last_error(&fiber()->diag)->raise();
 	}
 	cord_destroy(cord);
 	return rc;
diff --git a/src/coio.cc b/src/coio.cc
index d3c1f2f5f448246aa42d08cc4d5a8071c638cec4..71ed0b111348794cf04d08d089f598003859277f 100644
--- a/src/coio.cc
+++ b/src/coio.cc
@@ -202,12 +202,12 @@ coio_connect_timeout(struct ev_io *coio, struct uri *uri, struct sockaddr *addr,
 	    hints.ai_protocol = 0;
 	    int rc = coio_getaddrinfo(host, service, &hints, &ai, delay);
 	    if (rc != 0) {
-		ai = NULL;
+		    if (errno == ETIMEDOUT)
+			    return -1; /* timeout */
+		    tnt_raise(SocketError, -1, "getaddrinfo");
 	    }
-	}
-	if (ai == NULL)
-		return -1; /* timeout */
 
+	}
 	auto addrinfo_guard = make_scoped_guard([=] {
 		if (!uri->host_hint) freeaddrinfo(ai);
 		else free(ai_local.ai_addr);
@@ -215,7 +215,7 @@ coio_connect_timeout(struct ev_io *coio, struct uri *uri, struct sockaddr *addr,
 	evio_timeout_update(loop(), start, &delay);
 
 	coio_timeout_init(&start, &delay, timeout);
-	assert(! evio_is_active(coio));
+	assert(! evio_has_fd(coio));
 	while (ai) {
 		try {
 			if (coio_connect_addr(coio, ai->ai_addr,
@@ -626,7 +626,7 @@ coio_service_on_accept(struct evio_service *evio_service,
 	struct fiber *f;
 
 	try {
-		iobuf = iobuf_new(iobuf_name);
+		iobuf = iobuf_new();
 		f = fiber_new(fiber_name, service->handler);
 	} catch (Exception *e) {
 		e->log();
diff --git a/src/coio_buf.h b/src/coio_buf.h
index 8adecf53b93eedc8ff0bfc3ebc08d368e33756e5..633dc4f36979d8d74979fe0cdccadb0809f2a417 100644
--- a/src/coio_buf.h
+++ b/src/coio_buf.h
@@ -41,8 +41,8 @@ static inline ssize_t
 coio_bread(struct ev_io *coio, struct ibuf *buf, size_t sz)
 {
 	ibuf_reserve(buf, sz);
-	ssize_t n = coio_read_ahead(coio, buf->end, sz, ibuf_unused(buf));
-	buf->end += n;
+	ssize_t n = coio_read_ahead(coio, buf->wpos, sz, ibuf_unused(buf));
+	buf->wpos += n;
 	return n;
 }
 
@@ -56,9 +56,9 @@ coio_bread_timeout(struct ev_io *coio, struct ibuf *buf, size_t sz,
 		   ev_tstamp timeout)
 {
 	ibuf_reserve(buf, sz);
-	ssize_t n = coio_read_ahead_timeout(coio, buf->end, sz, ibuf_unused(buf),
+	ssize_t n = coio_read_ahead_timeout(coio, buf->wpos, sz, ibuf_unused(buf),
 			                    timeout);
-	buf->end += n;
+	buf->wpos += n;
 	return n;
 }
 
@@ -67,8 +67,8 @@ static inline ssize_t
 coio_breadn(struct ev_io *coio, struct ibuf *buf, size_t sz)
 {
 	ibuf_reserve(buf, sz);
-	ssize_t n = coio_readn_ahead(coio, buf->end, sz, ibuf_unused(buf));
-	buf->end += n;
+	ssize_t n = coio_readn_ahead(coio, buf->wpos, sz, ibuf_unused(buf));
+	buf->wpos += n;
 	return n;
 }
 
@@ -82,9 +82,9 @@ coio_breadn_timeout(struct ev_io *coio, struct ibuf *buf, size_t sz,
 		    ev_tstamp timeout)
 {
 	ibuf_reserve(buf, sz);
-	ssize_t n = coio_readn_ahead_timeout(coio, buf->end, sz, ibuf_unused(buf),
+	ssize_t n = coio_readn_ahead_timeout(coio, buf->wpos, sz, ibuf_unused(buf),
 			                     timeout);
-	buf->end += n;
+	buf->wpos += n;
 	return n;
 }
 
diff --git a/src/errinj.h b/src/errinj.h
index e0b2dec8ee9153a0273430877680d9ea4ce7415e..d35f64179e41f3c8505d92e9ea7f1deeb4a5e6f4 100644
--- a/src/errinj.h
+++ b/src/errinj.h
@@ -45,7 +45,8 @@ struct errinj {
 	_(ERRINJ_WAL_ROTATE, false) \
 	_(ERRINJ_WAL_WRITE, false) \
 	_(ERRINJ_INDEX_ALLOC, false) \
-	_(ERRINJ_TUPLE_ALLOC, false)
+	_(ERRINJ_TUPLE_ALLOC, false) \
+	_(ERRINJ_RELAY, false)
 
 ENUM0(errinj_enum, ERRINJ_LIST);
 extern struct errinj errinjs[];
diff --git a/src/evio.cc b/src/evio.cc
index 53884a803bdf82e4e77d9e0f8f7cec1cd74de4b8..7730775ec5d42aadc803896b36bb8ec45f8b1c0d 100644
--- a/src/evio.cc
+++ b/src/evio.cc
@@ -51,7 +51,7 @@ evio_close(ev_loop *loop, struct ev_io *evio)
 	ev_io_stop(loop, evio);
 	/* Close the socket. */
 	close(evio->fd);
-	/* Make sure evio_is_active() returns a proper value. */
+	/* Make sure evio_has_fd() returns a proper value. */
 	evio->fd = -1;
 }
 
diff --git a/src/evio.h b/src/evio.h
index ff597226833ad58e2b999730fec876ca728e1cf7..2a2bab70966ed901f66cc40b0c8598f1bd8de3d9 100644
--- a/src/evio.h
+++ b/src/evio.h
@@ -139,7 +139,7 @@ evio_service_is_active(struct evio_service *service)
 }
 
 static inline bool
-evio_is_active(struct ev_io *ev)
+evio_has_fd(struct ev_io *ev)
 {
 	return ev->fd >= 0;
 }
diff --git a/src/exception.cc b/src/exception.cc
index 7c7af79865d6729fe1a5e9656945188d2253b8b5..15cf0a50cff2c84f1fbed97b18f6972dfe10ff6c 100644
--- a/src/exception.cc
+++ b/src/exception.cc
@@ -33,85 +33,93 @@
 #include <stdio.h>
 #include <string.h>
 #include <errno.h>
-#include <typeinfo>
 
 /** out_of_memory::size is zero-initialized by the linker. */
 static OutOfMemory out_of_memory(__FILE__, __LINE__,
 				 sizeof(OutOfMemory), "malloc", "exception");
 
-void *
-Exception::operator new(size_t size)
+struct diag *
+diag_get()
 {
-	struct fiber *fiber = fiber();
-
-	if (fiber->exception && fiber->exception->size == 0)
-		fiber->exception = NULL;
-
-	if (fiber->exception) {
-		/* Explicitly call destructor for previous exception */
-		fiber->exception->~Exception();
-		if (fiber->exception->size >= size) {
-			/* Reuse memory allocated for exception */
-			return fiber->exception;
-		}
-		free(fiber->exception);
-	}
-	fiber->exception = (Exception *) malloc(size);
-	if (fiber->exception) {
-		fiber->exception->size = size;
-		return fiber->exception;
-	}
-	fiber->exception = &out_of_memory;
-	throw fiber->exception;
+	return &fiber()->diag;
 }
 
-void
-Exception::operator delete(void * /* ptr */)
+static const struct method exception_methods[] = {
+	make_method(&type_Exception, "message", &Exception::errmsg),
+	make_method(&type_Exception, "file", &Exception::file),
+	make_method(&type_Exception, "line", &Exception::line),
+	make_method(&type_Exception, "log", &Exception::log),
+	METHODS_SENTINEL
+};
+const struct type type_Exception = make_type("Exception", NULL,
+	exception_methods);
+
+void *
+Exception::operator new(size_t size)
 {
-	/* Unsupported */
-	assert(false);
+	void *buf = malloc(size);
+	if (buf != NULL)
+		return buf;
+	diag_add_error(&fiber()->diag, &out_of_memory);
+	throw &out_of_memory;
 }
 
-Exception::Exception(const char *file, unsigned line)
-	: m_file(file), m_line(line)
+void
+Exception::operator delete(void *ptr)
 {
-	m_errmsg[0] = 0;
+	free(ptr);
 }
 
-Exception::Exception(const Exception& e)
-	: Object(), m_file(e.m_file), m_line(e.m_line)
+Exception::~Exception()
 {
-	memcpy(m_errmsg, e.m_errmsg, sizeof(m_errmsg));
+	if (this != &out_of_memory) {
+		assert(m_ref == 0);
+	}
 }
 
-
-const char *
-Exception::type() const
-{
-	const char *name = typeid(*this).name();
-	/** A quick & dirty version of name demangle for class names */
-	char *res = NULL;
-	(void) strtol(name, &res, 10);
-	return res && strlen(res) ? res : name;
+Exception::Exception(const struct type *type_arg, const char *file,
+	unsigned line)
+	:type(type_arg), m_ref(0) {
+	if (file != NULL) {
+		snprintf(m_file, sizeof(m_file), "%s", file);
+		m_line = line;
+	} else {
+		m_file[0] = '\0';
+		m_line = 0;
+	}
+	m_errmsg[0] = '\0';
+	if (this == &out_of_memory) {
+		/* A special workaround for out_of_memory static init */
+		out_of_memory.m_ref = 1;
+		return;
+	}
 }
 
 void
 Exception::log() const
 {
-	_say(S_ERROR, m_file, m_line, m_errmsg, "%s", type());
+	_say(S_ERROR, m_file, m_line, m_errmsg, "%s", type->name);
 }
 
+static const struct method systemerror_methods[] = {
+	make_method(&type_SystemError, "errnum", &SystemError::errnum),
+	METHODS_SENTINEL
+};
+
+const struct type type_SystemError = make_type("SystemError", &type_Exception,
+	systemerror_methods);
 
-SystemError::SystemError(const char *file, unsigned line)
-	: Exception(file, line),
-	  m_errno(errno)
+SystemError::SystemError(const struct type *type,
+			 const char *file, unsigned line)
+	:Exception(type, file, line),
+	m_errno(errno)
 {
 	/* nothing */
 }
 
 SystemError::SystemError(const char *file, unsigned line,
 			 const char *format, ...)
-	:Exception(file, line),
+	: Exception(&type_SystemError, file, line),
 	m_errno(errno)
 {
 	va_list ap;
@@ -142,10 +150,13 @@ SystemError::log() const
 	     m_errmsg);
 }
 
+const struct type type_OutOfMemory =
+	make_type("OutOfMemory", &type_SystemError);
+
 OutOfMemory::OutOfMemory(const char *file, unsigned line,
 			 size_t amount, const char *allocator,
 			 const char *object)
-	:SystemError(file, line)
+	: SystemError(&type_OutOfMemory, file, line)
 {
 	m_errno = ENOMEM;
 	snprintf(m_errmsg, sizeof(m_errmsg),
diff --git a/src/exception.h b/src/exception.h
index c7eec1c14ec925c9ed846cf48b5dc92fabe9524b..9865f5df729a060658cb3fd54240091a0faf3216 100644
--- a/src/exception.h
+++ b/src/exception.h
@@ -30,15 +30,21 @@
  */
 #include "object.h"
 #include <stdarg.h>
+#include <assert.h>
+
+#include "reflection.h"
 #include "say.h"
 
-enum { TNT_ERRMSG_MAX = 512 };
+enum {
+	EXCEPTION_ERRMSG_MAX = 512,
+	EXCEPTION_FILE_MAX = 256
+};
 
+extern const struct type type_Exception;
 class Exception: public Object {
-protected:
-	/** Allocated size. */
-	size_t size;
 public:
+	const struct type *type; /* reflection */
+
 	void *operator new(size_t size);
 	void operator delete(void*);
 	virtual void raise()
@@ -47,51 +53,49 @@ class Exception: public Object {
 		throw this;
 	}
 
-	const char *type() const;
-
 	const char *
 	errmsg() const
 	{
 		return m_errmsg;
 	}
 
-	virtual void log() const;
-	virtual ~Exception() {}
+	const char *file() const {
+		return m_file;
+	}
 
-	static void init(Exception **what)
-	{
-		*what = NULL;
+	int line() const {
+		return m_line;
 	}
-	/** Clear the last error saved in the current fiber's TLS */
-	static inline void clear(Exception **what)
-	{
-		if (*what != NULL && (*what)->size > 0) {
-			(*what)->~Exception();
-			free(*what);
-		}
-		Exception::init(what);
+
+	virtual void log() const;
+	virtual ~Exception();
+
+	void ref() {
+		++m_ref;
 	}
-	/** Move an exception from one thread to another. */
-	static void move(Exception **from, Exception **to)
-	{
-		Exception::clear(to);
-		*to = *from;
-		Exception::init(from);
+
+	void unref() {
+		assert(m_ref > 0);
+		--m_ref;
+		if (m_ref == 0)
+			delete this;
 	}
+
 protected:
-	Exception(const char *file, unsigned line);
-	/* The copy constructor is needed for C++ throw */
-	Exception(const Exception&);
+	Exception(const struct type *type, const char *file, unsigned line);
 
-	/* file name */
-	const char *m_file;
+	/* Ref counter */
+	size_t m_ref;
 	/* line number */
 	unsigned m_line;
+	/* file name */
+	char m_file[EXCEPTION_FILE_MAX];
 
 	/* error description */
-	char m_errmsg[TNT_ERRMSG_MAX];
+	char m_errmsg[EXCEPTION_ERRMSG_MAX];
 };
 
+extern const struct type type_SystemError;
 class SystemError: public Exception {
 public:
 
@@ -111,7 +115,7 @@ class SystemError: public Exception {
 	SystemError(const char *file, unsigned line,
 		    const char *format, ...);
 protected:
-	SystemError(const char *file, unsigned line);
+	SystemError(const struct type *type, const char *file, unsigned line);
 
 	void
 	init(const char *format, ...);
@@ -124,6 +128,7 @@ class SystemError: public Exception {
 	int m_errno;
 };
 
+extern const struct type type_OutOfMemory;
 class OutOfMemory: public SystemError {
 public:
 	OutOfMemory(const char *file, unsigned line,
@@ -131,9 +136,112 @@ class OutOfMemory: public SystemError {
 		    const char *object);
 };
 
+/**
+ * Diagnostics Area - a container for errors and warnings
+ */
+struct diag {
+	/* \cond private */
+	class Exception *last;
+	/* \endcond private */
+};
+
+/**
+ * Remove all errors from the diagnostics area
+ * \param diag diagnostics area
+ */
+static inline void
+diag_clear(struct diag *diag)
+{
+	if (diag->last == NULL)
+		return;
+	diag->last->unref();
+	diag->last = NULL;
+}
+
+/**
+ * Add a new error to the diagnostics area
+ * \param diag diagnostics area
+ * \param e error to add
+ */
+static inline void
+diag_add_error(struct diag *diag, Exception *e)
+{
+	assert(e != NULL);
+	e->ref();
+	diag_clear(diag);
+	diag->last = e;
+}
+
+/**
+ * Return last error
+ * \return last error
+ * \param diag diagnostics area
+ */
+static inline Exception *
+diag_last_error(struct diag *diag)
+{
+	return diag->last;
+}
+
+/**
+ * Move all errors from \a from to \a to.
+ * \param from source
+ * \param to destination
+ * \post diag_is_empty(from)
+ */
+static inline void
+diag_move(struct diag *from, struct diag *to)
+{
+	diag_clear(to);
+	if (from->last == NULL)
+		return;
+	to->last = from->last;
+	from->last = NULL;
+}
+
+/**
+ * Return true if diagnostics area is empty
+ * \param diag diagnostics area to initialize
+ */
+static inline bool
+diag_is_empty(struct diag *diag)
+{
+	return diag->last == NULL;
+}
+
+/**
+ * Create a new diagnostics area
+ * \param diag diagnostics area to initialize
+ */
+static inline void
+diag_create(struct diag *diag)
+{
+	diag->last = NULL;
+}
+
+/**
+ * Destroy diagnostics area
+ * \param diag diagnostics area to clean
+ */
+static inline void
+diag_destroy(struct diag *diag)
+{
+	diag_clear(diag);
+}
+
+/**
+ * A helper for tnt_error to avoid cyclic includes (fiber.h and exception.h)
+ * \cond false
+ * */
+struct diag *
+diag_get();
+/** \endcond */
+
 #define tnt_error(class, ...) ({					\
 	say_debug("%s at %s:%i", #class, __FILE__, __LINE__);		\
-	new class(__FILE__, __LINE__, ##__VA_ARGS__);			\
+	class *e = new class(__FILE__, __LINE__, ##__VA_ARGS__);	\
+	diag_add_error(diag_get(), e);					\
+	e;								\
 })
 
 #define tnt_raise(...) do {						\
diff --git a/src/ffisyms.cc b/src/ffisyms.cc
index 068cc861e60581ecb41f498d0c1f34c15685d004..e2edf632796fe4fa2b31233c38473535927b7897 100644
--- a/src/ffisyms.cc
+++ b/src/ffisyms.cc
@@ -7,6 +7,7 @@
 #include <box/lua/tuple.h>
 #include <box/lua/call.h>
 #include <box/sophia_engine.h>
+#include <box/port.h>
 #include <lua/init.h>
 #include "main.h"
 #include "lua/bsdsocket.h"
@@ -14,6 +15,7 @@
 #include "fiber.h"
 #include "base64.h"
 #include "random.h"
+#include "iobuf.h"
 #include <lib/salad/guava.h>
 
 /*
@@ -42,8 +44,6 @@ void *ffi_symbols[] = {
 	(void *) boxffi_index_iterator,
 	(void *) boxffi_tuple_update,
 	(void *) boxffi_iterator_next,
-	(void *) port_ffi_create,
-	(void *) port_ffi_destroy,
 	(void *) boxffi_select,
 	(void *) password_prepare,
 	(void *) tarantool_error_message,
@@ -66,4 +66,11 @@ void *ffi_symbols[] = {
 	(void *) fiber_time,
 	(void *) fiber_time64,
 	(void *) sophia_schedule,
+	(void *) tarantool_lua_slab_cache,
+	(void *) ibuf_create,
+	(void *) ibuf_destroy,
+	(void *) ibuf_reserve_nothrow_slow,
+	(void *) port_buf_create,
+	(void *) port_buf_destroy,
+	(void *) port_buf_transfer
 };
diff --git a/src/fiber.cc b/src/fiber.cc
index f83ce7ac1ab36240c74e100ed2b950be0eb4edaf..88889dcba2fe03bdcf717de49ccb0a573e22bf64 100644
--- a/src/fiber.cc
+++ b/src/fiber.cc
@@ -36,12 +36,14 @@
 #include "assoc.h"
 #include "memory.h"
 #include "trigger.h"
-#include <typeinfo>
 
 static struct cord main_cord;
 __thread struct cord *cord_ptr = NULL;
 pthread_t main_thread_id;
 
+const struct type type_FiberCancelException =
+	make_type("FiberCancelException", &type_Exception);
+
 static void
 update_last_stack_frame(struct fiber *fiber)
 {
@@ -64,6 +66,7 @@ fiber_call(struct fiber *callee)
 
 	assert(cord->call_stack_depth < FIBER_CALL_STACK);
 	assert(caller);
+	assert(caller != callee);
 
 	cord->call_stack_depth++;
 	cord->fiber = callee;
@@ -220,11 +223,12 @@ fiber_join(struct fiber *fiber)
 	/* The fiber is already dead. */
 	fiber_recycle(fiber);
 
-	Exception::move(&fiber->exception, &fiber()->exception);
-	if (fiber()->exception &&
-	    typeid(*fiber()->exception) != typeid(FiberCancelException)) {
-		fiber()->exception->raise();
-	}
+	/* Move exception to the caller */
+	diag_move(&fiber->diag, &fiber()->diag);
+	Exception *e = diag_last_error(&fiber()->diag);
+	/** Raise exception again, unless it's FiberCancelException */
+	if (e != NULL && type_cast(FiberCancelException, e) == NULL)
+		e->raise();
 	fiber_testcancel();
 }
 /**
@@ -404,7 +408,7 @@ fiber_loop(void *data __attribute__((unused)))
 			 * Make sure a leftover exception does not
 			 * propagate up to the joiner.
 			 */
-			Exception::clear(&fiber->exception);
+			diag_clear(&fiber->diag);
 		} catch (FiberCancelException *e) {
 			say_info("fiber `%s' has been cancelled",
 				 fiber_name(fiber));
@@ -497,6 +501,7 @@ fiber_new(const char *name, void (*f) (va_list))
 	if (++cord->max_fid < 100)
 		cord->max_fid = 101;
 	fiber->fid = cord->max_fid;
+	diag_create(&fiber->diag);
 	fiber_set_name(fiber, name);
 	register_fid(fiber);
 
@@ -524,7 +529,7 @@ fiber_destroy(struct cord *cord, struct fiber *f)
 	rlist_del(&f->state);
 	region_destroy(&f->gc);
 	tarantool_coro_destroy(&f->coro, &cord->slabc);
-	Exception::clear(&f->exception);
+	diag_destroy(&f->diag);
 }
 
 void
@@ -555,7 +560,7 @@ cord_create(struct cord *cord, const char *name)
 	cord->call_stack_depth = 0;
 	cord->sched.fid = 1;
 	fiber_reset(&cord->sched);
-	Exception::init(&cord->sched.exception);
+	diag_create(&cord->sched.diag);
 	region_create(&cord->sched.gc, &cord->slabc);
 	fiber_set_name(&cord->sched, "sched");
 	cord->fiber = &cord->sched;
@@ -578,7 +583,7 @@ cord_destroy(struct cord *cord)
 		mh_i32ptr_delete(cord->fiber_registry);
 	}
 	region_destroy(&cord->sched.gc);
-	Exception::clear(&cord->sched.exception);
+	diag_destroy(&cord->sched.diag);
 	slab_cache_destroy(&cord->slabc);
 	ev_loop_destroy(cord->loop);
 }
@@ -616,11 +621,11 @@ void *cord_thread_func(void *p)
 		 * Clear a possible leftover exception object
 		 * to not confuse the invoker of the thread.
 		 */
-		Exception::clear(&cord->fiber->exception);
+		diag_clear(&cord->fiber->diag);
 	} catch (Exception *) {
 		/*
 		 * The exception is now available to the caller
-		 * via cord->exception.
+		 * via cord->diag.
 		 */
 		res = NULL;
 	}
@@ -653,16 +658,16 @@ cord_join(struct cord *cord)
 	assert(cord() != cord); /* Can't join self. */
 	void *retval = NULL;
 	int res = tt_pthread_join(cord->id, &retval);
-	if (res == 0 && cord->fiber->exception) {
+	if (res == 0 && !diag_is_empty(&cord->fiber->diag)) {
 		/*
 		 * cord_thread_func guarantees that
 		 * cord->exception is only set if the subject cord
 		 * has terminated with an uncaught exception,
 		 * transfer it to the caller.
 		 */
-		Exception::move(&cord->fiber->exception, &fiber()->exception);
+		diag_move(&cord->fiber->diag, &fiber()->diag);
 		cord_destroy(cord);
-		fiber()->exception->raise();
+		diag_last_error(&fiber()->diag)->raise();
 	}
 	cord_destroy(cord);
 	return res;
@@ -702,11 +707,10 @@ cord_costart_thread_func(void *arg)
 		/* The fiber hasn't died right away at start. */
 		ev_run(loop(), 0);
 	}
-	if (f->exception &&
-	    typeid(f->exception) != typeid(FiberCancelException)) {
-		Exception::move(&f->exception, &fiber()->exception);
-		fiber()->exception->raise();
-	}
+	diag_move(&f->diag, &fiber()->diag);
+	Exception *e = diag_last_error(&fiber()->diag);
+	if (e != NULL && type_cast(FiberCancelException, e) == NULL)
+		e->raise();
 
 	return NULL;
 }
diff --git a/src/fiber.h b/src/fiber.h
index a97fde5af9f90c3ccdb22ea5530ee22c58ee4984..658fb0d264c67a2dc5979bcb8902bd957d52e7b6 100644
--- a/src/fiber.h
+++ b/src/fiber.h
@@ -85,10 +85,11 @@ enum {
  * cancelled.
  */
 #if defined(__cplusplus)
+extern const struct type type_FiberCancelException;
 class FiberCancelException: public Exception {
 public:
 	FiberCancelException(const char *file, unsigned line)
-		: Exception(file, line) {
+		: Exception(&type_FiberCancelException, file, line) {
 		/* Nothing */
 	}
 
@@ -158,7 +159,7 @@ struct fiber {
 	/** Fiber local storage */
 	void *fls[FIBER_KEY_MAX];
 	/** Exception which caused this fiber's death. */
-	class Exception *exception;
+	struct diag diag;
 };
 
 enum { FIBER_CALL_STACK = 16 };
diff --git a/src/fio.c b/src/fio.c
index 2a184161f356758fbe4ba1da9983455fce21c2fb..8c2eac65da4f287627303b554eb694e71c295eaa 100644
--- a/src/fio.c
+++ b/src/fio.c
@@ -183,15 +183,14 @@ fio_batch_start(struct fio_batch *batch, long max_rows)
 }
 
 void
-fio_batch_add(struct fio_batch *batch, const struct iovec *iov, int iovcnt)
+fio_batch_add(struct fio_batch *batch, int iovcnt)
 {
-	assert(!fio_batch_has_space(batch, iovcnt));
 	assert(iovcnt > 0);
 	assert(batch->max_rows > 0);
-	for (int i = 0; i < iovcnt; i++) {
-		batch->iov[batch->iovcnt++] = iov[i];
-		batch->bytes += iov[i].iov_len;
-	}
+	int i = batch->iovcnt;
+	batch->iovcnt += iovcnt;
+	for (; i < batch->iovcnt; i++)
+		batch->bytes += batch->iov[i].iov_len;
 	bit_set(batch->rowflag, batch->iovcnt);
 	batch->rows++;
 }
diff --git a/src/fio.h b/src/fio.h
index db7a59362c0118aca3b8cc3bfd6999a7ba662548..a00526af9d33f168f6c50d7633dd930e3f25dd1f 100644
--- a/src/fio.h
+++ b/src/fio.h
@@ -178,19 +178,27 @@ fio_batch_alloc(int max_iov);
 void
 fio_batch_start(struct fio_batch *batch, long max_rows);
 
-static inline bool
-fio_batch_has_space(struct fio_batch *batch, int iovcnt)
-{
-	return batch->iovcnt + iovcnt > batch->max_iov ||
-		batch->rows >= batch->max_rows;
-}
-
 /**
  * Add a row to a batch.
+ * @pre iovcnt is the number of iov elements previously 
+ *      booked with fio_batch_book() and filled with data
  * @pre fio_batch_is_full() == false
  */
 void
-fio_batch_add(struct fio_batch *batch, const struct iovec *iov, int iovcnt);
+fio_batch_add(struct fio_batch *batch, int iovcnt);
+
+/**
+ * Get a pointer to struct iov * in the batch
+ * beyond batch->iovcnt + offset. Ensure
+ * the iov has at least 'count' elements.
+ */
+static inline struct iovec *
+fio_batch_book(struct fio_batch *batch, int offset, int count)
+{
+	if (batch->iovcnt + offset + count <= batch->max_iov)
+		return batch->iov + batch->iovcnt + offset;
+	return 0;
+}
 
 /**
  * Write all rows stacked into the batch.
diff --git a/src/iobuf.cc b/src/iobuf.cc
index eba29561d33a988129474007b08b1189346bbee7..b1aad0c7680a151e0db411beb07c8364f1311720 100644
--- a/src/iobuf.cc
+++ b/src/iobuf.cc
@@ -27,316 +27,71 @@
  * SUCH DAMAGE.
  */
 #include "iobuf.h"
-#include "coio_buf.h"
 #include "fiber.h"
 
 __thread struct mempool iobuf_pool;
+
 /**
  * Network readahead. A signed integer to avoid
  * automatic type coercion to an unsigned type.
+ * We assign it without locks in txn thread and
+ * use in iproto thread -- it's OK that
+ * readahead has a stale value while until the thread
+ * caches have synchronized, after all, it's used
+ * in new connections only.
+ *
+ * Notice that the default is not a strict power of two.
+ * slab metadata takes some space, and we want
+ * allocation steps to be correlated to slab buddy
+ * sizes, so when we ask slab cache for 16320 bytes,
+ * we get a slab of size 16384, not 32768.
  */
-static int iobuf_readahead = 16384;
-
-/* {{{ struct ibuf */
-
-/** Initialize an input buffer. */
-static void
-ibuf_create(struct ibuf *ibuf, struct region *pool)
-{
-	ibuf->pool = pool;
-	ibuf->capacity = 0;
-	ibuf->buf = ibuf->pos = ibuf->end = NULL;
-	/* Don't allocate the buffer yet. */
-}
-
-/** Forget all cached input. */
-static void
-ibuf_reset(struct ibuf *ibuf)
-{
-	ibuf->pos = ibuf->end = ibuf->buf;
-}
-
-/**
- * Ensure the buffer has sufficient capacity
- * to store size bytes.
- */
-void
-ibuf_reserve(struct ibuf *ibuf, size_t size)
-{
-	if (size <= ibuf_unused(ibuf))
-		return;
-	size_t current_size = ibuf_size(ibuf);
-	/*
-	 * Check if we have enough space in the
-	 * current buffer. In this case de-fragment it
-	 * by moving existing data to the beginning.
-	 * Otherwise, get a bigger buffer.
-	 */
-	if (size + current_size <= ibuf->capacity) {
-		memmove(ibuf->buf, ibuf->pos, current_size);
-	} else {
-		/* Use iobuf_readahead as allocation factor. */
-		size_t new_capacity = MAX(ibuf->capacity * 2, iobuf_readahead);
-		while (new_capacity < current_size + size)
-			new_capacity *= 2;
-
-		ibuf->buf = (char *) region_alloc(ibuf->pool, new_capacity);
-		memcpy(ibuf->buf, ibuf->pos, current_size);
-		ibuf->capacity = new_capacity;
-	}
-	ibuf->pos = ibuf->buf;
-	ibuf->end = ibuf->pos + current_size;
-}
-
-/* }}} */
-
-/* {{{ struct obuf */
-
-/**
- * Initialize the next slot in iovec array. The buffer
- * always has at least one empty slot.
- */
-static inline void
-obuf_init_pos(struct obuf *buf, size_t pos)
-{
-	if (pos >= IOBUF_IOV_MAX) {
-		tnt_raise(OutOfMemory, buf->pos, "obuf_init_pos", "iovec");
-	}
-	buf->iov[pos].iov_base = NULL;
-	buf->iov[pos].iov_len = 0;
-	buf->capacity[pos] = 0;
-}
-
-/** Allocate memory for a single iovec buffer. */
-static inline void
-obuf_alloc_pos(struct obuf *buf, size_t pos, size_t size)
-{
-	size_t capacity = pos > 0 ?  buf->capacity[pos-1] * 2 : buf->alloc_factor;
-	while (capacity < size) {
-		capacity *=2;
-	}
-
-	buf->iov[pos].iov_base = region_alloc(buf->pool, capacity);
-	buf->capacity[buf->pos] = capacity;
-	assert(buf->iov[pos].iov_len == 0);
-}
-
-/** Initialize an output buffer instance. Don't allocate memory
- * yet -- it may never be needed.
- */
-void
-obuf_create(struct obuf *buf, struct region *pool, size_t alloc_factor)
-{
-	buf->pool = pool;
-	buf->pos = 0;
-	buf->size = 0;
-	buf->alloc_factor = alloc_factor;
-	obuf_init_pos(buf, buf->pos);
-}
-
-/** Mark an output buffer as empty. */
-static void
-obuf_reset(struct obuf *buf)
-{
-	buf->pos = 0;
-	buf->size = 0;
-	for (struct iovec *iov = buf->iov; iov->iov_len != 0; iov++) {
-		assert(iov < buf->iov + IOBUF_IOV_MAX);
-		iov->iov_len = 0;
-	}
-}
-
-/** Add data to the output buffer. Copies the data. */
-void
-obuf_dup(struct obuf *buf, const void *data, size_t size)
-{
-	struct iovec *iov = &buf->iov[buf->pos];
-	size_t capacity = buf->capacity[buf->pos];
-	/**
-	 * @pre buf->pos points at an array of allocated buffers.
-	 * The array ends with a zero-initialized buffer.
-	 */
-	while (iov->iov_len + size > capacity) {
-		/*
-		 * The data doesn't fit into this buffer.
-		 * It could be because the buffer is not
-		 * allocated, is partially or completely full.
-		 * Copy as much as possible into already
-		 * allocated buffers.
-		 */
-		if (iov->iov_len < capacity) {
-			/*
-			 * This buffer is allocated, but can't
-			 * fit all the data. Copy as much data as
-			 * possible.
-			 */
-			size_t fill = capacity - iov->iov_len;
-			assert(fill < size);
-			memcpy((char *) iov->iov_base + iov->iov_len, data, fill);
-
-			iov->iov_len += fill;
-			buf->size += fill;
-			data = (char *) data + fill;
-			size -= fill;
-			/*
-			 * Check if the remainder can fit
-			 * without allocations.
-			 */
-		} else if (capacity == 0) {
-			/**
-			 * Still some data to copy. We have to get
-			 * a new buffer. Before we allocate
-			 * a buffer for this position, ensure
-			 * there is an unallocated buffer in the
-			 * next one, since it works as an end
-			 * marker for the loop above.
-			 */
-			obuf_init_pos(buf, buf->pos + 1);
-			obuf_alloc_pos(buf, buf->pos, size);
-			break;
-		}
-		assert(capacity == iov->iov_len);
-		buf->pos++;
-		iov = &buf->iov[buf->pos];
-		capacity = buf->capacity[buf->pos];
-	}
-	memcpy((char *) iov->iov_base + iov->iov_len, data, size);
-	iov->iov_len += size;
-	buf->size += size;
-	assert(iov->iov_len <= buf->capacity[buf->pos]);
-}
-
-void
-obuf_ensure_resize(struct obuf *buf, size_t size)
-{
-	struct iovec *iov = &buf->iov[buf->pos];
-	size_t capacity = buf->capacity[buf->pos];
-	if (iov->iov_len > 0) {
-		/* Move to the next buffer. */
-		buf->pos++;
-		iov = &buf->iov[buf->pos];
-		capacity = buf->capacity[buf->pos];
-	}
-	/* Make sure the next buffer can store size.  */
-	if (capacity == 0) {
-		obuf_init_pos(buf, buf->pos + 1);
-		obuf_alloc_pos(buf, buf->pos, size);
-	} else if (size > capacity) {
-		/* Simply realloc. */
-		obuf_alloc_pos(buf, buf->pos, size);
-	}
-}
-
-/** Book a few bytes in the output buffer. */
-struct obuf_svp
-obuf_book(struct obuf *buf, size_t size)
-{
-	obuf_ensure(buf, size);
-
-	struct obuf_svp svp;
-	svp.pos = buf->pos;
-	svp.iov_len = buf->iov[buf->pos].iov_len;
-	svp.size = buf->size;
-
-	obuf_advance(buf, size);
-	return svp;
-}
-
-/** Forget about data in the output buffer beyond the savepoint. */
-void
-obuf_rollback_to_svp(struct obuf *buf, struct obuf_svp *svp)
-{
-	bool is_last_pos = buf->pos == svp->pos;
-
-	buf->pos = svp->pos;
-	buf->iov[buf->pos].iov_len = svp->iov_len;
-	buf->size = svp->size;
-	/**
-	 * We need this check to ensure the following
-	 * loop doesn't run away.
-	 */
-	if (is_last_pos)
-		return;
-	for (struct iovec *iov = buf->iov + buf->pos + 1; iov->iov_len != 0; iov++) {
-		assert(iov < buf->iov + IOBUF_IOV_MAX);
-		iov->iov_len = 0;
-	}
-}
-
-/* struct obuf }}} */
-
-/* {{{ struct iobuf */
+static int iobuf_readahead = 16320;
 
 /**
  * How big is a buffer which needs to be shrunk before it is put
  * back into buffer cache.
  */
-static int iobuf_max_pool_size()
+static int iobuf_max_size()
 {
 	return 18 * iobuf_readahead;
 }
 
-SLIST_HEAD(iobuf_cache, iobuf) iobuf_cache;
-
 /** Create an instance of input/output buffer or take one from cache. */
 struct iobuf *
-iobuf_new(const char *name)
+iobuf_new()
+{
+	return iobuf_new_mt(&cord()->slabc);
+}
+
+struct iobuf *
+iobuf_new_mt(struct slab_cache *slabc_out)
 {
-	/* Does not work in multiple cords yet. */
-	assert(cord_is_main());
 	struct iobuf *iobuf;
-	if (SLIST_EMPTY(&iobuf_cache)) {
-		iobuf = (struct iobuf *) mempool_alloc(&iobuf_pool);
-		region_create(&iobuf->pool, &cord()->slabc);
-		/* Note: do not allocate memory upfront. */
-		ibuf_create(&iobuf->in, &iobuf->pool);
-		obuf_create(&iobuf->out, &iobuf->pool, iobuf_readahead);
-	} else {
-		iobuf = SLIST_FIRST(&iobuf_cache);
-		SLIST_REMOVE_HEAD(&iobuf_cache, next);
-	}
-	/* When releasing the buffer, we trim it to iobuf_max_pool_size. */
-	assert(region_used(&iobuf->pool) <= iobuf_max_pool_size());
-	region_set_name(&iobuf->pool, name);
+	iobuf = (struct iobuf *) mempool_alloc(&iobuf_pool);
+	/* Note: do not allocate memory upfront. */
+	ibuf_create(&iobuf->in, &cord()->slabc, iobuf_readahead);
+	obuf_create(&iobuf->out, slabc_out, iobuf_readahead);
 	return iobuf;
 }
 
-/** Put an instance back to the iobuf_cache. */
+/** Destroy an instance and delete it. */
 void
 iobuf_delete(struct iobuf *iobuf)
 {
-	struct region *pool = &iobuf->pool;
-	if (region_used(pool) < iobuf_max_pool_size()) {
-		ibuf_reset(&iobuf->in);
-		obuf_reset(&iobuf->out);
-	} else {
-		region_free(pool);
-		ibuf_create(&iobuf->in, pool);
-		obuf_create(&iobuf->out, pool, iobuf_readahead);
-	}
-	region_set_name(pool, "iobuf_cache");
-	SLIST_INSERT_HEAD(&iobuf_cache, iobuf, next);
+	ibuf_destroy(&iobuf->in);
+	obuf_destroy(&iobuf->out);
+	mempool_free(&iobuf_pool, iobuf);
 }
 
-/** Send all data in the output buffer and garbage collect. */
-ssize_t
-iobuf_flush(struct iobuf *iobuf, struct ev_io *coio)
+/** Second part of multi-threaded destroy. */
+void
+iobuf_delete_mt(struct iobuf *iobuf)
 {
-	ssize_t total = coio_writev(coio, iobuf->out.iov,
-				    obuf_iovcnt(&iobuf->out),
-				    obuf_size(&iobuf->out));
-	iobuf_reset(iobuf);
-	/*
-	 * If there is some residue in the input buffer, move it
-	 * but only in case if we don't have iobuf_readahead
-	 * bytes available for the next round: it's more efficient
-	 * to move any residue now, when it's likely to be small,
-	 * rather than when we have read a bunch more data, and only
-	 * then discovered we don't have enough space to read a
-	 * full request.
-	 */
-	ibuf_reserve(&iobuf->in, iobuf_readahead);
-	return total;
+	ibuf_destroy(&iobuf->in);
+	/* Destroyed by the caller. */
+	assert(iobuf->out.pos == 0 && iobuf->out.iov[0].iov_base == NULL);
+	mempool_free(&iobuf_pool, iobuf);
 }
 
 void
@@ -346,10 +101,23 @@ iobuf_reset(struct iobuf *iobuf)
 	 * If we happen to have fully processed the input,
 	 * move the pos to the start of the input buffer.
 	 */
-	if (ibuf_size(&iobuf->in) == 0)
-		ibuf_reset(&iobuf->in);
-	/* Cheap to do even if already done. */
-	obuf_reset(&iobuf->out);
+	if (ibuf_used(&iobuf->in) == 0) {
+		if (ibuf_capacity(&iobuf->in) < iobuf_max_size()) {
+			ibuf_reset(&iobuf->in);
+		} else {
+			struct slab_cache *slabc = iobuf->in.slabc;
+			ibuf_destroy(&iobuf->in);
+			ibuf_create(&iobuf->in, slabc, iobuf_readahead);
+		}
+	}
+	if (obuf_capacity(&iobuf->out) < iobuf_max_size()) {
+		/* Cheap to do even if already done. */
+		obuf_reset(&iobuf->out);
+	} else {
+		struct slab_cache *slabc = iobuf->out.slabc;
+		obuf_destroy(&iobuf->out);
+		obuf_create(&iobuf->out, slabc, iobuf_readahead);
+	}
 }
 
 void
@@ -363,5 +131,3 @@ iobuf_set_readahead(int readahead)
 {
 	iobuf_readahead =  readahead;
 }
-
-/* struct iobuf }}} */
diff --git a/src/iobuf.h b/src/iobuf.h
index 2e0e80b710021ffca38a08c6ddebbc3abde51864..671813712b00862def9314666c93edb225daa092 100644
--- a/src/iobuf.h
+++ b/src/iobuf.h
@@ -30,257 +30,45 @@
  */
 #include <sys/uio.h>
 #include <stdbool.h>
-#include "trivia/util.h"
-#include "third_party/queue.h"
-#include "small/region.h"
+#include "small/ibuf.h"
+#include "small/obuf.h"
 
-struct ev_io;
-
-/** {{{ Input buffer.
- *
- * Continuous piece of memory to store input.
- * Allocated in factors of 'iobuf_readahead'.
- * Maintains position of the data "to be processed".
- *
- * Typical use case:
- *
- * struct ibuf *in;
- * coio_bread(coio, in, request_len);
- * if (ibuf_size(in) >= request_len) {
- *	process_request(in->pos, request_len);
- *	in->pos += request_len;
- * }
- */
-struct ibuf
+struct iobuf
 {
-	struct region *pool;
-	char *buf;
-	/** Start of input. */
-	char *pos;
-	/** End of useful input */
-	char *end;
-	/** Buffer size. */
-	size_t capacity;
+	/** Input buffer. */
+	struct ibuf in;
+	/** Output buffer. */
+	struct obuf out;
 };
 
-/** Reserve space for sz bytes in the input buffer. */
-void
-ibuf_reserve(struct ibuf *ibuf, size_t sz);
-
-/** How much data is read and is not parsed yet. */
-static inline size_t
-ibuf_size(struct ibuf *ibuf)
-{
-	assert(ibuf->end >= ibuf->pos);
-	return ibuf->end - ibuf->pos;
-}
-
-/** How much data can we fit beyond buf->end */
-static inline size_t
-ibuf_unused(struct ibuf *ibuf)
-{
-	return ibuf->buf + ibuf->capacity - ibuf->end;
-}
-
-/* Integer value of the position in the buffer - stable
- * in case of realloc.
- */
-static inline size_t
-ibuf_pos(struct ibuf *ibuf)
-{
-	return ibuf->pos - ibuf->buf;
-}
-
-/* }}} */
-
-/* {{{ Output buffer. */
-
-enum { IOBUF_IOV_MAX = 32 };
-
 /**
- * An output buffer is an array of struct iovec vectors
- * for writev().
- * Each buffer is allocated on region allocator.
- * Buffer size grows by a factor of 2. With this growth factor,
- * the number of used buffers is unlikely to ever exceed the
- * hard limit of IOBUF_IOV_MAX. If it does, an exception is
- * raised.
+ * Create an instance of input/output buffer.
+ * @warning not safe to use in case of multi-threaded
+ * access to in and out.
  */
-struct obuf
-{
-	struct region *pool;
-	/* How many bytes are in the buffer. */
-	size_t size;
-	/** Position of the "current" iovec. */
-	size_t pos;
-	/** Allocation factor (allocations are a multiple of this number) */
-	size_t alloc_factor;
-	/** How many bytes are actually allocated for each iovec. */
-	size_t capacity[IOBUF_IOV_MAX];
-	/**
-	 * List of iovec vectors, each vector is at least twice
-	 * as big as the previous one. The vector following the
-	 * last allocated one is always zero-initialized
-	 * (iov_base = NULL, iov_len = 0).
-	 */
-	struct iovec iov[IOBUF_IOV_MAX];
-};
-
-void
-obuf_create(struct obuf *buf, struct region *pool, size_t alloc_factor);
-
-/** How many bytes are in the output buffer. */
-static inline size_t
-obuf_size(struct obuf *obuf)
-{
-	return obuf->size;
-}
-
-/** The size of iov vector in the buffer. */
-static inline int
-obuf_iovcnt(struct obuf *buf)
-{
-	return buf->iov[buf->pos].iov_len > 0 ? buf->pos + 1 : buf->pos;
-}
+struct iobuf *
+iobuf_new();
 
 /**
- * Output buffer savepoint. It's possible to
- * save the current buffer state in a savepoint
- * and roll back to the saved state at any time
- * before iobuf_flush()
+ * Destroy an input/output buffer.
+ * @warning a counterpart of iobuf_new(), only for single threaded
+ * access.
  */
-struct obuf_svp
-{
-	size_t pos;
-	size_t iov_len;
-	size_t size;
-};
-
 void
-obuf_ensure_resize(struct obuf *buf, size_t size);
-
-/**
- * \brief Ensure \a buf to have at least \a size bytes of contiguous memory
- * for write and return a point to this chunk.
- * After write please call obuf_advance(wsize) where wsize <= size to advance
- * a write position.
- * \param buf
- * \param size
- * \return a pointer to contiguous chunk of memory
- */
-static inline char *
-obuf_ensure(struct obuf *buf, size_t size)
-{
-	if (buf->iov[buf->pos].iov_len + size > buf->capacity[buf->pos])
-		obuf_ensure_resize(buf, size);
-	struct iovec *iov = &buf->iov[buf->pos];
-	return (char *) iov->iov_base + iov->iov_len;
-}
+iobuf_delete(struct iobuf *iobuf);
 
 /**
- * \brief Advance write position after using obuf_ensure()
- * \param buf
- * \param size
- * \sa obuf_ensure
+ * Multi-threaded constructor of iobuf - 'out'
+ * may use slab caches of another (consumer) cord.
  */
-static inline void
-obuf_advance(struct obuf *buf, size_t size)
-{
-	buf->iov[buf->pos].iov_len += size;
-	buf->size += size;
-	assert(buf->iov[buf->pos].iov_len <= buf->capacity[buf->pos]);
-}
+struct iobuf *
+iobuf_new_mt(struct slab_cache *slabc_out);
 
 /**
- * Reserve size bytes in the output buffer
- * and return a pointer to the reserved
- * data. Returns a pointer to a continuous piece of
- * memory.
- * Typical use case:
- * struct obuf_svp svp = obuf_book(buf, sizeof(uint32_t));
- * for (...)
- *	obuf_dup(buf, ...);
- * uint32_t total = obuf_size(buf);
- * memcpy(obuf_svp_to_ptr(&svp), &total, sizeof(total);
- * iobuf_flush();
+ * @pre 'out' must be freed if necessary by the consumer cord
  */
-struct obuf_svp
-obuf_book(struct obuf *obuf, size_t size);
-
-/** Append data to the output buffer. */
 void
-obuf_dup(struct obuf *obuf, const void *data, size_t size);
-
-static inline struct obuf_svp
-obuf_create_svp(struct obuf *buf)
-{
-	struct obuf_svp svp;
-	svp.pos = buf->pos;
-	svp.iov_len = buf->iov[buf->pos].iov_len;
-	svp.size = buf->size;
-	return svp;
-}
-
-/** Convert a savepoint position to a pointer in the buffer. */
-static inline void *
-obuf_svp_to_ptr(struct obuf *buf, struct obuf_svp *svp)
-{
-	return (char *) buf->iov[svp->pos].iov_base + svp->iov_len;
-}
-
-/** Forget anything added to output buffer after the savepoint. */
-void
-obuf_rollback_to_svp(struct obuf *buf, struct obuf_svp *svp);
-
-/**
- * \brief Conventional function to join iovec into a solid memory chunk.
- * For iovec of size 1 returns iov->iov_base without allocation extra memory.
- * \param[out] size calculated length of \a iov
- * \return solid memory chunk
- */
-static inline char *
-obuf_join(struct obuf *obuf)
-{
-	size_t iovcnt = obuf_iovcnt(obuf);
-	if (iovcnt == 1)
-		return (char *) obuf->iov[0].iov_base;
-
-	char *data = (char *) region_alloc(obuf->pool, obuf_size(obuf));
-	char *pos = data;
-	for (int i = 0; i < iovcnt; i++) {
-		memcpy(pos, obuf->iov[i].iov_base, obuf->iov[i].iov_len);
-		pos += obuf->iov[i].iov_len;
-	}
-	return data;
-}
-
-/* }}} */
-
-/** {{{  Input/output pair. */
-struct iobuf
-{
-	/** Used for iobuf cache. */
-	SLIST_ENTRY(iobuf) next;
-	/** Input buffer. */
-	struct ibuf in;
-	/** Output buffer. */
-	struct obuf out;
-	struct region pool;
-};
-
-/** Create an instance of input/output buffer. */
-struct iobuf *
-iobuf_new(const char *name);
-
-/** Destroy an input/output buffer. */
-void
-iobuf_delete(struct iobuf *iobuf);
-
-/** Flush output using cooperative I/O and garbage collect.
- * @return number of bytes written
- */
-ssize_t
-iobuf_flush(struct iobuf *iobuf, struct ev_io *coio);
+iobuf_delete_mt(struct iobuf *iobuf);
 
 /**
  * Must be called when we are done sending all output,
@@ -290,19 +78,24 @@ iobuf_flush(struct iobuf *iobuf, struct ev_io *coio);
 void
 iobuf_reset(struct iobuf *iobuf);
 
-/** Return true if there is no input and no output. */
+/** Return true if there is no input and no output and
+ * no one has pinned the buffer - i.e. it's safe to
+ * destroy it.
+ */
 static inline bool
 iobuf_is_idle(struct iobuf *iobuf)
 {
-	return ibuf_size(&iobuf->in) == 0 && obuf_size(&iobuf->out) == 0;
+	return ibuf_used(&iobuf->in) == 0 && obuf_size(&iobuf->out) == 0;
 }
 
+/**
+ * Got to be called in each thread iobuf subsystem is
+ * used in.
+ */
 void
 iobuf_init();
 
 void
 iobuf_set_readahead(int readahead);
 
-/* }}} */
-
 #endif /* TARANTOOL_IOBUF_H_INCLUDED */
diff --git a/src/latch.h b/src/latch.h
new file mode 100644
index 0000000000000000000000000000000000000000..397f88d090550e71562d74a27734c285dda593d2
--- /dev/null
+++ b/src/latch.h
@@ -0,0 +1,158 @@
+#ifndef TARANTOOL_LATCH_H_INCLUDED
+#define TARANTOOL_LATCH_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include "salad/rlist.h"
+#include "fiber.h"
+
+/** Latch of cooperative multitasking environment. */
+
+struct latch
+{
+	/**
+	 * State of latch. 0 - not locked, 1 - locked.
+	 */
+	int locked;
+	/**
+	 * The queue of fibers waiting on a latch.
+	 */
+	struct rlist queue;
+};
+
+/**
+ * latch initializer
+ */
+#define LATCH_INITIALIZER(name) { 0, RLIST_HEAD_INITIALIZER(name.queue) }
+
+/**
+ * Initialize the given latch.
+ *
+ * @param m - latch to be initialized.
+ */
+static inline void
+latch_create(struct latch *l)
+{
+	l->locked = 0;
+	rlist_create(&l->queue);
+}
+
+/**
+ * Destroy the given latch.
+ *
+ * @param m - latch to be destroyed.
+ */
+static inline void
+latch_destroy(struct latch *l)
+{
+	assert(rlist_empty(&l->queue));
+	(void) l;
+}
+
+/**
+ * Lock a latch. If the latch is already locked by another fiber,
+ * waits for timeout.
+ *
+ * @param m - latch to be locked.
+ * @param timeout - maximal time to wait
+ *
+ * @retval 0 - success
+ * @retval 1 - timeout
+ */
+static inline int
+latch_lock_timeout(struct latch *l, ev_tstamp timeout)
+{
+	if (l->locked == 0 && rlist_empty(&l->queue)) {
+		l->locked = 1;
+		return 0;
+	}
+	if (timeout <= 0)
+		return 1;
+
+	rlist_add_tail_entry(&l->queue, fiber(), state);
+	bool was_cancellable = fiber_set_cancellable(false);
+	ev_tstamp start = timeout;
+	int result = 0;
+	while (true) {
+		fiber_yield_timeout(timeout);
+		if (l->locked == 0) {
+			l->locked = 1;
+			break;
+		}
+		timeout -= ev_now(loop()) - start;
+		if (timeout <= 0) {
+			errno = ETIMEDOUT;
+			result = 1;
+			break;
+		}
+	}
+	fiber_set_cancellable(was_cancellable);
+	rlist_del_entry(fiber(), state);
+	return result;
+}
+
+/**
+ * Lock a latch (no timeout). Waits indefinitely until
+ * the current fiber can gain access to the latch.
+ */
+static inline void
+latch_lock(struct latch *l)
+{
+	(void) latch_lock_timeout(l, TIMEOUT_INFINITY);
+}
+
+/**
+ * Try to lock a latch. Return immediately if the latch is locked.
+ * @retval 0 - success
+ * @retval 1 - the latch is locked.
+ */
+static inline int
+latch_trylock(struct latch *l)
+{
+	return latch_lock_timeout(l, 0);
+}
+
+/**
+ * Unlock a latch. The fiber calling this function must
+ * own the latch.
+ */
+static inline void
+latch_unlock(struct latch *l)
+{
+	assert(l->locked);
+	l->locked = 0;
+	if (!rlist_empty(&l->queue)) {
+		struct fiber *f = rlist_first_entry(&l->queue,
+						    struct fiber, state);
+		fiber_wakeup(f);
+	}
+}
+
+#endif /* TARANTOOL_LATCH_H_INCLUDED */
diff --git a/src/lib/salad/rtree.c b/src/lib/salad/rtree.c
index a14f89134ee1ff5555b177baadab3cb0a7e1fcc9..70a43d410c1eeff74ae6bed723f595f3a50eb3d0 100644
--- a/src/lib/salad/rtree.c
+++ b/src/lib/salad/rtree.c
@@ -249,13 +249,23 @@ rtree_always_true(const struct rtree_rect *rt1,
 static struct rtree_page *
 rtree_alloc_page(struct rtree *tree)
 {
-	return (struct rtree_page *)tree->page_alloc();
+	if (tree->free_pages) {
+		struct rtree_page *result =
+			(struct rtree_page *)tree->free_pages;
+		tree->free_pages = *(void **)tree->free_pages;
+		return result;
+	} else {
+		uint32_t unused_id;
+		return (struct rtree_page *)
+			matras_alloc(&tree->mtab, &unused_id);
+	}
 }
 
 static void
 rtree_free_page(struct rtree *tree, struct rtree_page *page)
 {
-	tree->page_free(page);
+	*(void **)page = tree->free_pages;
+	tree->free_pages = (void *)page;
 }
 
 static void
@@ -558,7 +568,7 @@ rtree_page_purge(struct rtree *tree, struct rtree_page *page, int level)
 		for (int i = 0; i < page->n; i++)
 			rtree_page_purge(tree, page->b[i].data.page, level);
 	}
-	tree->page_free(page);
+	rtree_free_page(tree, page);
 }
 
 /*------------------------------------------------------------------------- */
@@ -624,7 +634,8 @@ rtree_iterator_destroy(struct rtree_iterator *itr)
 	struct rtree_neighbor_page *curr, *next;
 	for (curr = itr->page_list; curr != NULL; curr = next) {
 		next = curr->next;
-		itr->tree->page_free(curr);
+		rtree_free_page((struct rtree *) itr->tree,
+				(struct rtree_page *) curr);
 	}
 	itr->page_list = NULL;
 	itr->page_pos = RTREE_NEIGHBORS_IN_PAGE;
@@ -648,7 +659,8 @@ rtree_iterator_allocate_neighbour(struct rtree_iterator *itr)
 {
 	if (itr->page_pos >= RTREE_NEIGHBORS_IN_PAGE) {
 		struct rtree_neighbor_page *new_page =
-			(struct rtree_neighbor_page *)itr->tree->page_alloc();
+			(struct rtree_neighbor_page *)
+			rtree_alloc_page((struct rtree*)itr->tree);
 		new_page->next = itr->page_list;
 		itr->page_list = new_page;
 		itr->page_pos = 0;
@@ -683,6 +695,7 @@ rtree_iterator_free_neighbor(struct rtree_iterator *itr,
 void
 rtree_iterator_init(struct rtree_iterator *itr)
 {
+	itr->tree = 0;
 	itr->neigh_list = NULL;
 	itr->neigh_free_list = NULL;
 	itr->page_list = NULL;
@@ -764,22 +777,24 @@ rtree_iterator_next(struct rtree_iterator *itr)
 /*------------------------------------------------------------------------- */
 
 void
-rtree_init(struct rtree *tree,
-	   rtree_page_alloc_t page_alloc, rtree_page_free_t page_free)
+rtree_init(struct rtree *tree, uint32_t extent_size,
+	   rtree_extent_alloc_t extent_alloc, rtree_extent_free_t extent_free)
 {
 	tree->n_records = 0;
 	tree->height = 0;
 	tree->root = NULL;
 	tree->version = 0;
 	tree->n_pages = 0;
-	tree->page_alloc = page_alloc;
-	tree->page_free = page_free;
+	matras_create(&tree->mtab, extent_size, RTREE_PAGE_SIZE,
+		      extent_alloc, extent_free);
+	tree->free_pages = 0;
 }
 
 void
 rtree_destroy(struct rtree *tree)
 {
 	rtree_purge(tree);
+	matras_destroy(&tree->mtab);
 }
 
 void
@@ -859,6 +874,7 @@ rtree_search(const struct rtree *tree, const struct rtree_rect *rect,
 	     enum spatial_search_op op, struct rtree_iterator *itr)
 {
 	rtree_iterator_reset(itr);
+	assert(itr->tree == 0 || itr->tree == tree);
 	itr->tree = tree;
 	itr->version = tree->version;
 	itr->rect = *rect;
diff --git a/src/lib/salad/rtree.h b/src/lib/salad/rtree.h
index f3e62085009023b2ee4aeb4af23375898295baf8..4d1f4c2e2864810d17ee7a6f99adb82ff0e86f4c 100644
--- a/src/lib/salad/rtree.h
+++ b/src/lib/salad/rtree.h
@@ -30,6 +30,7 @@
  */
 #include <stddef.h>
 #include <stdbool.h>
+#include "small/matras.h"
 
 /**
  * In-memory Guttman's R-tree
@@ -56,6 +57,7 @@ enum {
 	/**
 	 * R-Tree uses linear search for elements on a page,
 	 * so a larger page size can hurt performance.
+	 * must be power of 2
 	 */
 	RTREE_PAGE_SIZE = 1024
 };
@@ -88,8 +90,8 @@ enum spatial_search_op
 };
 
 /* pointers to page allocation and deallocations functions */
-typedef void *(*rtree_page_alloc_t)();
-typedef void (*rtree_page_free_t)(void *);
+typedef void *(*rtree_extent_alloc_t)();
+typedef void (*rtree_extent_free_t)(void *);
 
 /* A point in RTREE_DIMENSION space */
 struct rtree_point
@@ -124,10 +126,10 @@ struct rtree
 	unsigned version;
 	/* Number of allocated (used) pages */
 	unsigned n_pages;
-	/* Function for allocation new pages */
-	rtree_page_alloc_t page_alloc;
-	/* Function for deallocation new pages */
-	rtree_page_free_t page_free;
+	/* Matras for allocating new page */
+	struct matras mtab;
+	/* List of free pages */
+	void *free_pages;
 };
 
 /* Struct for iteration and retrieving rtree values */
@@ -196,12 +198,13 @@ rtree_set2d(struct rtree_rect *rect,
 /**
  * @brief Initialize a tree
  * @param tree - pointer to a tree
- * @param page_alloc - page allocation function
- * @param page_free - page deallocation function
+ * @param extent_size - size of extents allocated by extent_alloc (see next)
+ * @param extent_alloc - extent allocation function
+ * @param extent_free - extent deallocation function
  */
 void
-rtree_init(struct rtree *tree,
-	   rtree_page_alloc_t page_alloc, rtree_page_free_t page_free);
+rtree_init(struct rtree *tree, uint32_t extent_size,
+	   rtree_extent_alloc_t extent_alloc, rtree_extent_free_t extent_free);
 
 /**
  * @brief Destroy a tree
diff --git a/src/lib/small/CMakeLists.txt b/src/lib/small/CMakeLists.txt
index 989e54a97627f8656ee1a49c8fce8f137087206e..502de19de30f57d16a5c8c4d976cf815cd30a835 100644
--- a/src/lib/small/CMakeLists.txt
+++ b/src/lib/small/CMakeLists.txt
@@ -1,3 +1,11 @@
-set(lib_sources slab_cache.c region.c mempool.c slab_arena.c small.c matras.c)
+set(lib_sources
+    slab_cache.c
+    region.c
+    mempool.c
+    slab_arena.c
+    small.c
+    matras.c
+    ibuf.c
+    obuf.c)
 set_source_files_compile_flags(${lib_sources})
 add_library(small ${lib_sources})
diff --git a/src/lib/small/ibuf.c b/src/lib/small/ibuf.c
new file mode 100644
index 0000000000000000000000000000000000000000..e698324fc804bbf2e755d36dbfaa15f1b290a8af
--- /dev/null
+++ b/src/lib/small/ibuf.c
@@ -0,0 +1,104 @@
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "ibuf.h"
+#include <string.h>
+#include "slab_cache.h"
+
+/** Initialize an input buffer. */
+void
+ibuf_create(struct ibuf *ibuf, struct slab_cache *slabc, size_t start_capacity)
+{
+	ibuf->slabc = slabc;
+	ibuf->buf = ibuf->rpos = ibuf->wpos = ibuf->end = NULL;
+	ibuf->start_capacity = start_capacity;
+	/* Don't allocate the buffer yet. */
+}
+
+void
+ibuf_destroy(struct ibuf *ibuf)
+{
+	if (ibuf->buf) {
+		struct slab *slab = slab_from_data(ibuf->buf);
+		slab_put(ibuf->slabc, slab);
+	 }
+}
+
+/** Free memory allocated by this buffer */
+void
+ibuf_reinit(struct ibuf *ibuf)
+{
+	struct slab_cache *slabc = ibuf->slabc;
+	size_t start_capacity = ibuf->start_capacity;
+	ibuf_destroy(ibuf);
+	ibuf_create(ibuf, slabc, start_capacity);
+}
+
+/**
+ * Ensure the buffer has sufficient capacity
+ * to store size bytes, and return pointer to
+ * the beginning.
+ */
+void *
+ibuf_reserve_nothrow_slow(struct ibuf *ibuf, size_t size)
+{
+	assert(ibuf->wpos + size > ibuf->end);
+	size_t used = ibuf_used(ibuf);
+	size_t capacity = ibuf_capacity(ibuf);
+	/*
+	 * Check if we have enough space in the
+	 * current buffer. In this case de-fragment it
+	 * by moving existing data to the beginning.
+	 * Otherwise, get a bigger buffer.
+	 */
+	if (size + used <= capacity) {
+		memmove(ibuf->buf, ibuf->rpos, used);
+	} else {
+		/* Use iobuf_readahead as allocation factor. */
+		size_t new_capacity = capacity * 2;
+		if (new_capacity < ibuf->start_capacity)
+			new_capacity = ibuf->start_capacity;
+
+		while (new_capacity < used + size)
+			new_capacity *= 2;
+
+		struct slab *slab = slab_get(ibuf->slabc, new_capacity);
+		if (slab == NULL)
+			return NULL;
+		char *ptr = (char *) slab_data(slab);
+		memcpy(ptr, ibuf->rpos, used);
+		if (ibuf->buf)
+			slab_put(ibuf->slabc, slab_from_data(ibuf->buf));
+		ibuf->buf = ptr;
+		ibuf->end = ibuf->buf + slab_capacity(slab);
+	}
+	ibuf->rpos = ibuf->buf;
+	ibuf->wpos = ibuf->rpos + used;
+	return ibuf->wpos;
+}
+
diff --git a/src/lib/small/ibuf.h b/src/lib/small/ibuf.h
new file mode 100644
index 0000000000000000000000000000000000000000..f0dc659c1dd117c00791673310e0592381bc782a
--- /dev/null
+++ b/src/lib/small/ibuf.h
@@ -0,0 +1,162 @@
+#ifndef TARANTOOL_SMALL_IBUF_H_INCLUDED
+#define TARANTOOL_SMALL_IBUF_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <stddef.h>
+#include <assert.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+/** @module Input buffer. */
+
+struct slab_cache;
+
+/*
+ * Continuous piece of memory to store input.
+ * Allocated in factors of 'start_capacity'.
+ * Maintains position of the data "to be processed".
+ *
+ * Typical use case:
+ *
+ * struct ibuf *in;
+ * coio_bread(coio, in, request_len);
+ * if (ibuf_size(in) >= request_len) {
+ *	process_request(in->rpos, request_len);
+ *	in->rpos += request_len;
+ * }
+ */
+struct ibuf
+{
+	struct slab_cache *slabc;
+	char *buf;
+	/** Start of input. */
+	char *rpos;
+	/** End of useful input */
+	char *wpos;
+	/** End of buffer. */
+	char *end;
+	size_t start_capacity;
+};
+
+void
+ibuf_create(struct ibuf *ibuf, struct slab_cache *slabc, size_t start_capacity);
+
+void
+ibuf_destroy(struct ibuf *ibuf);
+
+void
+ibuf_reinit(struct ibuf *ibuf);
+
+/** How much data is read and is not parsed yet. */
+static inline size_t
+ibuf_used(struct ibuf *ibuf)
+{
+	assert(ibuf->wpos >= ibuf->rpos);
+	return ibuf->wpos - ibuf->rpos;
+}
+
+/** How much data can we fit beyond buf->wpos */
+static inline size_t
+ibuf_unused(struct ibuf *ibuf)
+{
+	assert(ibuf->wpos <= ibuf->end);
+	return ibuf->end - ibuf->wpos;
+}
+
+/** How much memory is allocated */
+static inline size_t
+ibuf_capacity(struct ibuf *ibuf)
+{
+	return ibuf->end - ibuf->buf;
+}
+
+/**
+ * Integer value of the position in the buffer - stable
+ * in case of realloc.
+ */
+static inline size_t
+ibuf_pos(struct ibuf *ibuf)
+{
+	assert(ibuf->buf <= ibuf->rpos);
+	return ibuf->rpos - ibuf->buf;
+}
+
+/** Forget all cached input. */
+static inline void
+ibuf_reset(struct ibuf *ibuf)
+{
+	ibuf->rpos = ibuf->wpos = ibuf->buf;
+}
+
+void *
+ibuf_reserve_nothrow_slow(struct ibuf *ibuf, size_t size);
+
+static inline void *
+ibuf_reserve_nothrow(struct ibuf *ibuf, size_t size)
+{
+	if (ibuf->wpos + size <= ibuf->end)
+		return ibuf->wpos;
+	return ibuf_reserve_nothrow_slow(ibuf, size);
+}
+
+static inline void *
+ibuf_alloc_nothrow(struct ibuf *ibuf, size_t size)
+{
+	void *ptr;
+	if (ibuf->wpos + size <= ibuf->end)
+		ptr = ibuf->wpos;
+	else {
+		ptr = ibuf_reserve_nothrow_slow(ibuf, size);
+		if (ptr == NULL)
+			return NULL;
+	}
+	ibuf->wpos += size;
+	return ptr;
+}
+
+#if defined(__cplusplus)
+} /* extern "C" */
+
+#include "exception.h"
+
+/** Reserve space for sz bytes in the input buffer. */
+static inline void *
+ibuf_reserve(struct ibuf *ibuf, size_t size)
+{
+	void *ptr = ibuf_reserve_nothrow(ibuf, size);
+	if (ptr == NULL)
+		tnt_raise(OutOfMemory, size, "ibuf", "reserve");
+	return ptr;
+}
+
+#endif /* defined(__cplusplus) */
+
+#endif /* TARANTOOL_SMALL_IBUF_H_INCLUDED */
diff --git a/src/lib/small/matras.h b/src/lib/small/matras.h
index 8c111c15a4aca1aa84b82757639cc510c905b459..40362f14386655b9303e8cdb94e15f8e5380a264 100644
--- a/src/lib/small/matras.h
+++ b/src/lib/small/matras.h
@@ -368,6 +368,6 @@ matras_get(const struct matras *m, matras_id_t id)
 }
 
 #if defined(__cplusplus)
-}; /* extern "C" */
+} /* extern "C" */
 #endif /* defined(__cplusplus) */
 #endif /* INCLUDES_TARANTOOL_SMALL_MATRAS_H */
diff --git a/src/lib/small/mempool.c b/src/lib/small/mempool.c
index 5570c674f29a8bd814e6bc99018ff260bd365aa7..b542e9ffd0927ddde1951bee50eecf0b6d8b3e99 100644
--- a/src/lib/small/mempool.c
+++ b/src/lib/small/mempool.c
@@ -181,7 +181,7 @@ mempool_stats(struct mempool *pool, struct mempool_stats *stats)
 	/* Object size. */
 	stats->objsize = pool->objsize;
 	/* Number of objects. */
-	stats->objcount = pool->slabs.stats.used/pool->objsize;
+	stats->objcount = mempool_count(pool);
 	/* Size of the slab. */
 	stats->slabsize = slab_order_size(pool->cache, pool->slab_order);
 	/* The number of slabs. */
diff --git a/src/lib/small/mempool.h b/src/lib/small/mempool.h
index 5f2737957ecd082b032e9fcf6c9cfdaf2e8abb55..39284377d609408d21ad53ef400518fe2221ebaf 100644
--- a/src/lib/small/mempool.h
+++ b/src/lib/small/mempool.h
@@ -180,6 +180,15 @@ struct mempool_stats
 void
 mempool_stats(struct mempool *mempool, struct mempool_stats *stats);
 
+/**
+ * Number of objects in the pool.
+ */
+static inline size_t
+mempool_count(struct mempool *pool)
+{
+	return pool->slabs.stats.used/pool->objsize;
+}
+
 /** @todo: struct mempool_iterator */
 
 void
diff --git a/src/lib/small/obuf.c b/src/lib/small/obuf.c
new file mode 100644
index 0000000000000000000000000000000000000000..13e9041a63a9945f7d0db90f208bdd43a152b46d
--- /dev/null
+++ b/src/lib/small/obuf.c
@@ -0,0 +1,213 @@
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include "obuf.h"
+#include <string.h>
+
+#include "slab_cache.h"
+
+/** Allocate memory for a single iovec buffer. */
+static inline void *
+obuf_alloc_pos(struct obuf *buf, size_t size)
+{
+	int pos = buf->pos;
+	assert(buf->capacity[pos] == 0 && buf->iov[pos].iov_len == 0);
+	assert(pos < SMALL_OBUF_IOV_MAX);
+	assert(buf->n_iov == pos);
+	/** Initialize the next pos. */
+	buf->iov[pos+1] = buf->iov[pos];
+	buf->capacity[pos+1] = buf->capacity[pos];
+	size_t capacity = buf->start_capacity << pos;
+	while (capacity < size) {
+		capacity = capacity == 0 ? buf->start_capacity: capacity * 2;
+	}
+	struct slab *slab = slab_get(buf->slabc, capacity);
+	if (slab == NULL)
+		return NULL;
+	buf->iov[pos].iov_base = slab_data(slab);
+	buf->capacity[pos] = slab_capacity(slab);
+	buf->n_iov++;
+	return buf->iov[pos].iov_base;
+}
+
+/**
+ * Initialize an output buffer instance. Don't allocate memory
+ * yet -- it may never be needed.
+ */
+void
+obuf_create(struct obuf *buf, struct slab_cache *slabc, size_t start_capacity)
+{
+	buf->slabc = slabc;
+	buf->n_iov = 0;
+	buf->pos = 0;
+	buf->used = 0;
+	buf->start_capacity= start_capacity;
+	buf->iov[0].iov_base = NULL;
+	buf->iov[0].iov_len = 0;
+	buf->capacity[0] = 0;
+	buf->wend = buf->wpos = obuf_create_svp(buf);
+}
+
+
+/** Mark an output buffer as empty. */
+void
+obuf_reset(struct obuf *buf)
+{
+	int iovcnt = obuf_iovcnt(buf);
+	for (int i = 0; i < iovcnt; i++)
+		buf->iov[i].iov_len = 0;
+	buf->pos = 0;
+	buf->used = 0;
+	buf->wend = buf->wpos = obuf_create_svp(buf);
+}
+
+void
+obuf_destroy(struct obuf *buf)
+{
+	for (int i = 0; i < buf->n_iov; i++) {
+		struct slab *slab = slab_from_data(buf->iov[i].iov_base);
+		slab_put(buf->slabc, slab);
+	}
+#ifndef NDEBUG
+	obuf_create(buf, buf->slabc, buf->start_capacity);
+#endif
+}
+
+/** Add data to the output buffer. Copies the data. */
+size_t
+obuf_dup_nothrow(struct obuf *buf, const void *data, size_t size)
+{
+	struct iovec *iov = &buf->iov[buf->pos];
+	size_t capacity = buf->capacity[buf->pos];
+	size_t to_copy = size;
+	/**
+	 * @pre buf->pos points at an array of allocated buffers.
+	 * The array ends with a zero-initialized buffer.
+	 */
+	while (iov->iov_len + to_copy > capacity) {
+		/*
+		 * The data doesn't fit into this buffer.
+		 * It could be because the buffer is not
+		 * allocated, is partially or completely full.
+		 * Copy as much as possible into already
+		 * allocated buffers.
+		 */
+		if (iov->iov_len < capacity) {
+			/*
+			 * This buffer is allocated, but can't
+			 * fit all the data. Copy as much data as
+			 * possible.
+			 */
+			size_t fill = capacity - iov->iov_len;
+			assert(fill < to_copy);
+			memcpy((char *) iov->iov_base + iov->iov_len,
+			       data, fill);
+
+			iov->iov_len += fill;
+			buf->used += fill;
+			data = (char *) data + fill;
+			to_copy -= fill;
+			/*
+			 * Check if the remainder can fit
+			 * without allocations.
+			 */
+		} else if (capacity == 0) {
+			/**
+			 * Still some data to copy. We have to get
+			 * a new buffer. Before we allocate
+			 * a buffer for this position, ensure
+			 * there is an unallocated buffer in the
+			 * next one, since it works as an end
+			 * marker for the loop above.
+			 */
+			if (obuf_alloc_pos(buf, to_copy) == NULL)
+				return size - to_copy;
+			break;
+		}
+		assert(capacity == iov->iov_len);
+		if (buf->pos + 1 >= SMALL_OBUF_IOV_MAX)
+			return size - to_copy;
+		buf->pos++;
+		iov = &buf->iov[buf->pos];
+		capacity = buf->capacity[buf->pos];
+	}
+	memcpy((char *) iov->iov_base + iov->iov_len, data, to_copy);
+	iov->iov_len += to_copy;
+	buf->used += to_copy;
+	assert(iov->iov_len <= buf->capacity[buf->pos]);
+	return size;
+}
+
+void *
+obuf_reserve_slow_nothrow(struct obuf *buf, size_t size)
+{
+	struct iovec *iov = &buf->iov[buf->pos];
+	size_t capacity = buf->capacity[buf->pos];
+	if (iov->iov_len > 0) {
+		/* Move to the next buffer. */
+		if (buf->pos + 1 >= SMALL_OBUF_IOV_MAX)
+			return NULL;
+		buf->pos++;
+		iov = &buf->iov[buf->pos];
+		capacity = buf->capacity[buf->pos];
+	}
+	assert(iov->iov_len == 0);
+	/* Make sure the next buffer can store size. */
+	if (size > capacity) {
+		if (capacity > 0) {
+			/* Simply realloc. */
+			while (capacity < size)
+				capacity = capacity * 2;
+			struct slab *slab = slab_get(buf->slabc, capacity);
+			if (slab == NULL)
+				return NULL;
+			struct slab *old =
+				slab_from_data(buf->iov[buf->pos].iov_base);
+			slab_put(buf->slabc, old);
+			buf->iov[buf->pos].iov_base = slab_data(slab);
+			buf->capacity[buf->pos] = slab_capacity(slab);
+		} else if (obuf_alloc_pos(buf, size) == NULL) {
+			return NULL;
+		}
+	}
+	assert(buf->iov[buf->pos].iov_len + size <= buf->capacity[buf->pos]);
+	return (char*) buf->iov[buf->pos].iov_base + buf->iov[buf->pos].iov_len;
+}
+
+/** Forget about data in the output buffer beyond the savepoint. */
+void
+obuf_rollback_to_svp(struct obuf *buf, struct obuf_svp *svp)
+{
+	int iovcnt = obuf_iovcnt(buf);
+
+	buf->pos = svp->pos;
+	buf->iov[buf->pos].iov_len = svp->iov_len;
+	buf->used = svp->used;
+	for (int i = buf->pos + 1; i < iovcnt; i++)
+		buf->iov[i].iov_len = 0;
+}
diff --git a/src/lib/small/obuf.h b/src/lib/small/obuf.h
new file mode 100644
index 0000000000000000000000000000000000000000..2be841ac5439ae717e57f468ef9d0231b3423f35
--- /dev/null
+++ b/src/lib/small/obuf.h
@@ -0,0 +1,288 @@
+#ifndef TARANTOOL_SMALL_OBUF_H_INCLUDED
+#define TARANTOOL_SMALL_OBUF_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+#include <sys/uio.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <assert.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+enum { SMALL_OBUF_IOV_MAX = 31 };
+
+struct slab_cache;
+
+/**
+ * Output buffer savepoint. It's possible to
+ * save the current buffer state in a savepoint
+ * and roll back to the saved state at any time
+ * before obuf_reset()
+ */
+struct obuf_svp
+{
+	size_t pos;
+	size_t iov_len;
+	size_t used;
+};
+
+/**
+ * An output buffer is a vector of struct iovec
+ * for writev().
+ * Each iovec buffer is allocated using slab allocator.
+ * Buffer size grows by a factor of 2. With this growth factor,
+ * the number of used buffers is unlikely to ever exceed the
+ * hard limit of SMALL_OBUF_IOV_MAX. If it does, an exception is
+ * raised.
+ */
+struct obuf
+{
+	struct slab_cache *slabc;
+	/** Position of the "current" iovec. */
+	int pos;
+	/* The number of allocated iov instances. */
+	int n_iov;
+	/* How many bytes are in the buffer. */
+	size_t used;
+	/**
+	 * iov[0] size (allocations are normally a multiple of this number),
+	 * but can be larger if a large chunk is requested by
+	 * obuf_reserve().
+	 */
+	size_t start_capacity;
+	/** How many bytes are actually allocated for each iovec. */
+	size_t capacity[SMALL_OBUF_IOV_MAX + 1];
+	/**
+	 * List of iovec vectors, each vector is at least twice
+	 * as big as the previous one. The vector following the
+	 * last allocated one is always zero-initialized
+	 * (iov_base = NULL, iov_len = 0).
+	 * Make it the last member to reduce friction around
+	 * wpos/wend in iproto thread - last elements
+	 * of iov are unlikely to be updated often.
+	 */
+	struct iovec iov[SMALL_OBUF_IOV_MAX + 1];
+	/**
+	 * The below two members are used by iproto thread,
+	 * avoid false sharing by cache aligning them.
+	 */
+	struct {
+		/** Current write position in the output buffer */
+		struct obuf_svp wpos;
+		/** End of write position in the output buffer */
+		struct obuf_svp wend;
+	} __attribute__((aligned(64)));
+};
+
+void
+obuf_create(struct obuf *buf, struct slab_cache *slabc, size_t start_capacity);
+
+void
+obuf_destroy(struct obuf *buf);
+
+void
+obuf_reset(struct obuf *buf);
+
+/** How many bytes are in the output buffer. */
+static inline size_t
+obuf_size(struct obuf *obuf)
+{
+	return obuf->used;
+}
+
+static inline size_t
+obuf_used(struct obuf *obuf)
+{
+	return obuf->wend.used - obuf->wpos.used;
+}
+
+/** The size of iov vector in the buffer. */
+static inline int
+obuf_iovcnt(struct obuf *buf)
+{
+	return buf->iov[buf->pos].iov_len > 0 ? buf->pos + 1 : buf->pos;
+}
+
+/**
+ * Slow path of obuf_reserve(), which actually reallocates
+ * memory and moves data if necessary.
+ */
+void *
+obuf_reserve_slow_nothrow(struct obuf *buf, size_t size);
+
+/**
+ * \brief Ensure \a buf to have at least \a size bytes of contiguous memory
+ * for write and return a point to this chunk.
+ * After write please call obuf_advance(wsize) where wsize <= size to advance
+ * a write position.
+ * \param buf
+ * \param size
+ * \return a pointer to contiguous chunk of memory
+ */
+static inline void *
+obuf_reserve_nothrow(struct obuf *buf, size_t size)
+{
+	if (buf->iov[buf->pos].iov_len + size > buf->capacity[buf->pos])
+		return obuf_reserve_slow_nothrow(buf, size);
+	struct iovec *iov = &buf->iov[buf->pos];
+	return (char *) iov->iov_base + iov->iov_len;
+}
+
+/**
+ * \brief Advance write position after using obuf_reserve()
+ * \param buf
+ * \param size
+ * \sa obuf_reserve
+ */
+static inline void *
+obuf_alloc_nothrow(struct obuf *buf, size_t size)
+{
+	struct iovec *iov = &buf->iov[buf->pos];
+	void *ptr;
+	if (iov->iov_len + size <= buf->capacity[buf->pos]) {
+		ptr = (char *) iov->iov_base + iov->iov_len;
+	} else {
+		ptr = obuf_reserve_slow_nothrow(buf, size);
+		if (ptr == NULL)
+			return NULL;
+		iov = &buf->iov[buf->pos];
+		assert(iov->iov_len <= buf->capacity[buf->pos]);
+	}
+	iov->iov_len += size;
+	buf->used += size;
+	return ptr;
+}
+
+/** Append data to the output buffer. */
+size_t
+obuf_dup_nothrow(struct obuf *buf, const void *data, size_t size);
+
+static inline size_t
+obuf_capacity(struct obuf *buf)
+{
+	/** This is an approximation, see obuf_alloc_pos() */
+	return buf->capacity[buf->n_iov] * 2;
+}
+
+static inline struct obuf_svp
+obuf_create_svp(struct obuf *buf)
+{
+	struct obuf_svp svp;
+	svp.pos = buf->pos;
+	svp.iov_len = buf->iov[buf->pos].iov_len;
+	svp.used = buf->used;
+	return svp;
+}
+
+/** Forget anything added to output buffer after the savepoint. */
+void
+obuf_rollback_to_svp(struct obuf *buf, struct obuf_svp *svp);
+
+/** Convert a savepoint position to a pointer in the buffer. */
+static inline void *
+obuf_svp_to_ptr(struct obuf *buf, struct obuf_svp *svp)
+{
+	return (char *) buf->iov[svp->pos].iov_base + svp->iov_len;
+}
+
+#if defined(__cplusplus)
+} /* extern "C" */
+
+#include "exception.h"
+
+static inline void *
+obuf_reserve(struct obuf *buf, size_t size)
+{
+	void *ptr = obuf_reserve_nothrow(buf, size);
+	if (ptr == NULL)
+		tnt_raise(OutOfMemory, size, "obuf", "reserve");
+	return ptr;
+}
+
+static inline void *
+obuf_alloc(struct obuf *buf, size_t size)
+{
+	void *ptr = obuf_alloc_nothrow(buf, size);
+	if (ptr == NULL)
+		tnt_raise(OutOfMemory, size, "obuf", "alloc");
+	return ptr;
+}
+
+static inline void
+obuf_dup(struct obuf *buf, const void *data, size_t size)
+{
+	if (obuf_dup_nothrow(buf, data, size) != size)
+		tnt_raise(OutOfMemory, size, "obuf", "dup");
+}
+
+extern "C" {
+
+static inline void *
+obuf_reserve_cb(void *ctx, size_t *size)
+{
+	struct obuf *buf = (struct obuf *) ctx;
+	void *ptr = obuf_reserve(buf, *size);
+	*size = buf->capacity[buf->pos] - buf->iov[buf->pos].iov_len;
+	return ptr;
+}
+
+static inline void *
+obuf_alloc_cb(void *ctx, size_t size)
+{
+	return obuf_alloc((struct obuf *) ctx, size);
+}
+
+} /* extern "C" */
+
+/**
+ * Reserve size bytes in the output buffer
+ * and return a pointer to the reserved
+ * data. Returns a pointer to a continuous piece of
+ * memory.
+ * Typical use case:
+ * struct obuf_svp svp = obuf_book(buf, sizeof(uint32_t));
+ * for (...)
+ *	obuf_dup(buf, ...);
+ * uint32_t total = obuf_used(buf);
+ * memcpy(obuf_svp_to_ptr(&svp), &total, sizeof(total);
+ */
+static inline struct obuf_svp
+obuf_book(struct obuf *buf, size_t size)
+{
+	obuf_reserve(buf, size);
+	struct obuf_svp svp = obuf_create_svp(buf);
+	obuf_alloc(buf, size);
+	return svp;
+}
+
+#endif /* defined(__cplusplus) */
+
+#endif /* TARANTOOL_SMALL_OBUF_H_INCLUDED */
diff --git a/src/lib/small/region.c b/src/lib/small/region.c
index 84300d48f73d227e56b1d1aa755a7602710fbe29..b8b4921c7763012ec7949c156e9d65029f56275c 100644
--- a/src/lib/small/region.c
+++ b/src/lib/small/region.c
@@ -30,7 +30,7 @@
 #include <sys/types.h> /* ssize_t */
 
 void *
-region_alloc_slow(struct region *region, size_t size)
+region_reserve_slow(struct region *region, size_t size)
 {
 	/* The new slab must have at least this many bytes available. */
 	size_t slab_min_size = size + rslab_sizeof() - slab_sizeof();
@@ -39,14 +39,13 @@ region_alloc_slow(struct region *region, size_t size)
 	slab = (struct rslab *) slab_get(region->cache, slab_min_size);
 	if (slab == NULL)
 		return NULL;
-	slab->used = size;
+	slab->used = 0;
 	/*
 	 * Sic: add the new slab to the beginning of the
 	 * region, even if it is full, otherwise,
 	 * region_truncate() won't work.
 	 */
 	slab_list_add(&region->slabs, &slab->slab, next_in_list);
-	region->slabs.stats.used += size;
 	return rslab_data(slab);
 }
 
@@ -66,11 +65,11 @@ region_free(struct region *region)
  * obtained by calling region_used().
  */
 void
-region_truncate(struct region *region, size_t new_size)
+region_truncate(struct region *region, size_t used)
 {
-	assert(new_size <= region_used(region));
+	ssize_t cut_size = region_used(region) - used;
+	assert(cut_size >= 0);
 
-	ssize_t cut_size = region_used(region) - new_size;
 	while (! rlist_empty(&region->slabs.slabs)) {
 		struct rslab *slab = rlist_first_entry(&region->slabs.slabs,
 						       struct rslab,
@@ -87,6 +86,43 @@ region_truncate(struct region *region, size_t new_size)
 		slab_put(region->cache, &slab->slab);
 	}
 	assert(cut_size == 0);
-	region->slabs.stats.used = new_size;
+	region->slabs.stats.used = used;
 }
 
+void *
+region_join_nothrow(struct region *region, size_t size)
+{
+	if (rlist_empty(&region->slabs.slabs)) {
+		assert(size == 0);
+		return region_alloc_nothrow(region, 0);
+	}
+	struct rslab *slab = rlist_first_entry(&region->slabs.slabs,
+					       struct rslab,
+					       slab.next_in_list);
+
+	if (slab->used >= size) {
+		/* Don't move stuff if it's in a single chunk. */
+		return (char *) rslab_data(slab) + slab->used - size;
+	}
+	/**
+	 * Use region_reserve() to ensure slab->size is not
+	 * changed when the joined region is in the same slab
+	 * as the final chunk.
+	 */
+	char *ptr = region_reserve_nothrow(region, size);
+	size_t offset = size;
+	if (ptr == NULL)
+		return NULL;
+	/*
+	 * Copy data from last chunk to first, i.e. in the reverse order.
+	 */
+	while (offset > 0 && slab->used <= offset) {
+		memcpy(ptr + offset - slab->used, rslab_data(slab), slab->used);
+		offset -= slab->used;
+		slab = rlist_next_entry(slab, slab.next_in_list);
+	}
+	if (offset > 0)
+		memcpy(ptr, rslab_data(slab) + slab->used - offset, offset);
+	region_alloc_nothrow(region, size);
+	return ptr;
+}
diff --git a/src/lib/small/region.h b/src/lib/small/region.h
index f53bead861142edc462f65a264ec3ef8c4e22151..45ecf9410a29a35285c92dab2c863d44f9f30497 100644
--- a/src/lib/small/region.h
+++ b/src/lib/small/region.h
@@ -140,36 +140,37 @@ rslab_unused(struct rslab *slab)
 	return slab->slab.size - rslab_sizeof() - slab->used;
 }
 
-/**
- * Allocate 'size' bytes from a block.
- * @pre block must have enough unused space
- */
+void *
+region_reserve_slow(struct region *region, size_t size);
+
 static inline void *
-rslab_alloc(struct rslab *slab, size_t size)
+region_reserve_nothrow(struct region *region, size_t size)
 {
-	assert(size <= rslab_unused(slab));
-	char *ptr = (char*)rslab_data(slab) + slab->used;
-	slab->used += size;
-	return ptr;
+	if (! rlist_empty(&region->slabs.slabs)) {
+		struct rslab *slab = rlist_first_entry(&region->slabs.slabs,
+						       struct rslab,
+						       slab.next_in_list);
+		if (size <= rslab_unused(slab))
+			return (char *) rslab_data(slab) + slab->used;
+	}
+	return region_reserve_slow(region, size);
 }
 
-void *
-region_alloc_slow(struct region *region, size_t size);
-
 /** Allocate size bytes from a region. */
 static inline void *
 region_alloc_nothrow(struct region *region, size_t size)
 {
-	if (! rlist_empty(&region->slabs.slabs)) {
+	void *ptr = region_reserve_nothrow(region, size);
+	if (ptr != NULL) {
 		struct rslab *slab = rlist_first_entry(&region->slabs.slabs,
 						       struct rslab,
 						       slab.next_in_list);
-		if (size <= rslab_unused(slab)) {
-			region->slabs.stats.used += size;
-			return rslab_alloc(slab, size);
-		}
+		assert(size <= rslab_unused(slab));
+
+		region->slabs.stats.used += size;
+		slab->used += size;
 	}
-	return region_alloc_slow(region, size);
+	return ptr;
 }
 
 /**
@@ -194,6 +195,9 @@ region_used(struct region *region)
 	return region->slabs.stats.used;
 }
 
+/** Return size bytes allocated last as a single chunk. */
+void *
+region_join_nothrow(struct region *region, size_t size);
 
 /** How much memory is held by this region. */
 static inline size_t
@@ -209,8 +213,9 @@ region_free_after(struct region *region, size_t after)
 		region_free(region);
 }
 
+/** Truncate the region to the given size */
 void
-region_truncate(struct region *pool, size_t sz);
+region_truncate(struct region *pool, size_t size);
 
 static inline void
 region_set_name(struct region *region, const char *name)
@@ -237,27 +242,65 @@ region_alloc(struct region *region, size_t size)
 	return ptr;
 }
 
+static inline void *
+region_reserve(struct region *region, size_t size)
+{
+	void *ptr = region_reserve_nothrow(region, size);
+	if (ptr == NULL)
+		tnt_raise(OutOfMemory, size, "region", "new slab");
+	return ptr;
+}
+
+static inline void *
+region_join(struct region *region, size_t size)
+{
+	void *ptr = region_join_nothrow(region, size);
+	if (ptr == NULL)
+		tnt_raise(OutOfMemory, size, "region", "join");
+	return ptr;
+}
+
 static inline void *
 region_alloc0(struct region *region, size_t size)
 {
 	return memset(region_alloc(region, size), 0, size);
 }
 
+static inline void
+region_dup(struct region *region, const void *ptr, size_t size)
+{
+	(void) memcpy(region_alloc(region, size), ptr, size);
+}
+
 extern "C" {
 static inline void *
 region_alloc_cb(void *ctx, size_t size)
 {
 	return region_alloc((struct region *) ctx, size);
 }
+
+static inline void *
+region_reserve_cb(void *ctx, size_t *size)
+{
+	struct region *region = (struct region *) ctx;
+	void *ptr = region_reserve(region, *size);
+	struct rslab *slab = rlist_first_entry(&region->slabs.slabs,
+					       struct rslab,
+					       slab.next_in_list);
+	*size = rslab_unused(slab);
+	return ptr;
+}
+
 } /* extern "C" */
 
 struct RegionGuard {
 	struct region *region;
 	size_t used;
 
-	RegionGuard(struct region *_region)
-		: region(_region),
-		  used(region_used(_region)) {
+	RegionGuard(struct region *region_arg)
+		: region(region_arg),
+		  used(region_used(region_arg))
+        {
 		/* nothing */
 	}
 
diff --git a/src/lib/small/slab_cache.h b/src/lib/small/slab_cache.h
index aa103f3cdd72795e47acaffd7985e24101889af1..399d32fb181f2f3351e44d97eaa32b2347e62fb5 100644
--- a/src/lib/small/slab_cache.h
+++ b/src/lib/small/slab_cache.h
@@ -180,6 +180,12 @@ slab_get_with_order(struct slab_cache *cache, uint8_t order);
 void
 slab_put(struct slab_cache *cache, struct slab *slab);
 
+static inline size_t
+slab_cache_used(struct slab_cache *slabc)
+{
+	return slabc->allocated.stats.used;
+}
+
 struct slab *
 slab_from_ptr(struct slab_cache *cache, void *ptr, uint8_t order);
 
@@ -192,11 +198,23 @@ slab_sizeof()
 
 /** Useful size of a slab. */
 static inline uint32_t
-slab_size(struct slab *slab)
+slab_capacity(struct slab *slab)
 {
 	return slab->size - slab_sizeof();
 }
 
+static inline void *
+slab_data(struct slab *slab)
+{
+	return (char *) slab + slab_sizeof();
+}
+
+static inline struct slab *
+slab_from_data(void *data)
+{
+	return (struct slab *) ((char *) data - slab_sizeof());
+}
+
 void
 slab_cache_check(struct slab_cache *cache);
 
diff --git a/src/lua/bsdsocket.lua b/src/lua/bsdsocket.lua
index 6a9caef5514b4fc6e651c1fa95826ef2ea7bb9d3..31d836bbe0a4dbe1a56436aca01200227b4b618d 100644
--- a/src/lua/bsdsocket.lua
+++ b/src/lua/bsdsocket.lua
@@ -1,7 +1,7 @@
 -- bsdsocket.lua (internal file)
 
 local TIMEOUT_INFINITY      = 500 * 365 * 86400
-local LIMIT_INFINITY = 4294967295
+local LIMIT_INFINITY = 2147483647
 local READAHEAD = 16380
 
 local ffi = require('ffi')
@@ -10,6 +10,7 @@ local internal = require('socket')
 local fiber = require('fiber')
 local fio = require('fio')
 local log = require('log')
+local buffer = require('buffer')
 
 ffi.cdef[[
     struct socket {
@@ -51,6 +52,9 @@ ffi.cdef[[
         int    p_proto;      /* protocol number */
     };
     struct protoent *getprotobyname(const char *name);
+
+    void *memmem(const void *haystack, size_t haystacklen,
+        const void *needle, size_t needlelen);
 ]]
 
 local function sprintf(fmt, ...)
@@ -166,31 +170,63 @@ socket_methods.sysconnect = function(self, host, port)
     return false
 end
 
-socket_methods.syswrite = function(self, octets)
+local function syswrite(self, charptr, size)
     local fd = check_socket(self)
     self._errno = nil
-    local done = ffi.C.write(fd, octets, string.len(octets))
+    local done = ffi.C.write(fd, charptr, size)
     if done < 0 then
         self._errno = boxerrno()
         return nil
     end
+
     return tonumber(done)
 end
 
-socket_methods.sysread = function(self, size)
+socket_methods.syswrite = function(self, arg1, arg2)
+    -- TODO: ffi.istype('char *', arg1) doesn't work for ffi.new('char[256]')
+    if type(arg1) == 'cdata' and arg2 ~= nil then
+        return syswrite(self, arg1, arg2)
+    elseif type(arg1) == 'string' then
+        return syswrite(self, arg1, #arg1)
+    else
+        error('Usage: socket:syswrite(data) or socket:syswrite(const char *, size)')
+    end
+end
+
+local function sysread(self, charptr, size)
     local fd = check_socket(self)
-    size = size or READAHEAD
-    self._errno = nil
-    local buf = ffi.new('char[?]', size)
-    local res = ffi.C.read(fd, buf, size)
 
+    self._errno = nil
+    local res = ffi.C.read(fd, charptr, size)
     if res < 0 then
         self._errno = boxerrno()
         return nil
     end
 
-    buf = ffi.string(buf, res)
-    return buf
+    return tonumber(res)
+end
+
+socket_methods.sysread = function(self, arg1, arg2)
+    -- TODO: ffi.istype('char *', arg1) doesn't work for ffi.new('char[256]')
+    if type(arg1) == 'cdata' and arg2 ~= nil then
+        return sysread(self, arg1, arg2)
+    end
+
+    local size = arg1 or READAHEAD
+
+    local buf = buffer.IBUF_SHARED
+    buf:reset()
+    local p = buf:alloc(size)
+
+    local res = sysread(self, p, size)
+    if res then
+        local str = ffi.string(p, res)
+        buf:recycle()
+        return str
+    else
+        buf:recycle()
+        return res
+    end
 end
 
 socket_methods.nonblock = function(self, nb)
@@ -546,136 +582,95 @@ local errno_is_fatal = {
     [boxerrno.ENOTSOCK] = true;
 }
 
-local function readchunk(self, limit, timeout)
-    if self.rbuf == nil then
-        self.rbuf = ''
-        self.rpos = 1
-        self.rlen = 0
-    end
-
-    if self.rlen >= limit then
-        self._errno = nil
-        local data = string.sub(self.rbuf, self.rpos, self.rpos - 1 + limit)
-        self.rlen = self.rlen - limit
-        self.rpos = self.rpos + limit
-        return data
-    end
-
-    while true do
-        local started = fiber.time()
-
-        local to_read
-        if limit ~= LIMIT_INFINITY and limit > READAHEAD then
-            to_read = limit - self.rlen
-        end
-        local data = self:sysread(to_read)
-        if data ~= nil then
-            self.rbuf = string.sub(self.rbuf, self.rpos) .. data
-            self.rpos = 1
-            self.rlen = string.len(self.rbuf)
-            if string.len(data) == 0 then   -- eof
-                limit = self.rlen
-            end
-            if self.rlen >= limit then
-               data = string.sub(self.rbuf, self.rpos, self.rpos - 1 + limit)
-               self.rlen = self.rlen - limit
-               self.rpos = self.rpos + limit
-               return data
-            end
-        elseif not errno_is_transient[self:errno()] then
-            self._errno = boxerrno()
-            return nil
-        end
-
-        if not self:readable(timeout) then
-            return nil
-        end
-        if timeout <= 0 then
-            break
-        end
-        timeout = timeout - ( fiber.time() - started )
+local function check_limit(self, limit)
+    if self.rbuf.size >= limit then
+        return limit
     end
-    self._errno = boxerrno.ETIMEDOUT
     return nil
 end
 
-local function readline_check(self, eols, limit)
+local function check_delimiter(self, limit, eols)
     if limit == 0 then
-        return ''
+        return 0
     end
-    if self.rlen == 0 then
+    local rbuf = self.rbuf
+    if rbuf.size == 0 then
         return nil
     end
 
     local shortest
     for i, eol in ipairs(eols) do
-        local data = string.match(self.rbuf, "^(.-" .. eol .. ")", self.rpos)
+        local data = ffi.C.memmem(rbuf.rpos, rbuf.size, eol, #eol)
         if data ~= nil then
-            if string.len(data) > limit then
-                data = string.sub(data, 1, limit)
-            end
-            if shortest == nil or string.len(shortest) > string.len(data) then
-                shortest = data
+            local len = ffi.cast('char *', data) - rbuf.rpos + #eol
+            if shortest == nil or shortest > len then
+                shortest = len
             end
         end
     end
-    if shortest == nil and self.rlen >= limit then
-        shortest = string.sub(self.rbuf, self.rpos, self.rpos - 1 + limit)
+    if shortest ~= nil and shortest <= limit then
+        return shortest
+    elseif limit <= rbuf.size then
+        return limit
     end
-    if shortest ~= nil then
-        local len = string.len(shortest)
-        self.rpos = self.rpos + len
-        self.rlen = self.rlen - len
-    end
-    return shortest
+    return nil
 end
 
-local function readline(self, limit, eol, timeout)
-    if self.rbuf == nil then
-        self.rbuf = ''
-        self.rpos = 1
-        self.rlen = 0
+local function read(self, limit, timeout, check, ...)
+    assert(limit >= 0)
+    limit = math.min(limit, LIMIT_INFINITY)
+    local rbuf = self.rbuf
+    if rbuf == nil then
+        rbuf = buffer.ibuf()
+        self.rbuf = rbuf
     end
 
-    self._errno = nil
-    local data = readline_check(self, eol, limit)
-    if data ~= nil then
+    local len = check(self, limit, ...)
+    if len ~= nil then
+        self._errno = nil
+        local data = ffi.string(rbuf.rpos, len)
+        rbuf.rpos = rbuf.rpos + len
         return data
     end
 
     local started = fiber.time()
     while timeout > 0 do
         local started = fiber.time()
-        
-        if not self:readable(timeout) then
-            self._errno = boxerrno()
-            return nil
-        end
-        
-        timeout = timeout - ( fiber.time() - started )
 
-        local to_read
-        if limit ~= LIMIT_INFINITY and limit > READAHEAD then
-            to_read = limit - self.rlen
-        end
-        local data = self:sysread(to_read)
-        if data ~= nil then
-            self.rbuf = string.sub(self.rbuf, self.rpos) .. data
-            self.rpos = 1
-            self.rlen = string.len(self.rbuf)
-            if string.len(data) == 0 then       -- eof
-                limit = self.rlen
-            end
-            data = readline_check(self, eol, limit)
-            if data ~= nil then
+        assert(rbuf.size < limit)
+        local to_read = math.min(limit - rbuf.size, READAHEAD)
+        local data = rbuf:reserve(to_read)
+        assert(rbuf.unused >= to_read)
+        local res = sysread(self, data, rbuf.unused)
+        if res == 0 then -- eof
+            self._errno = nil
+            local len = rbuf.size
+            local data = ffi.string(rbuf.rpos, len)
+            rbuf.rpos = rbuf.rpos + len
+            return data
+        elseif res ~= nil then
+            rbuf.wpos = rbuf.wpos + res
+            local len = check(self, limit, ...)
+            if len ~= nil then
+                self._errno = nil
+                local data = ffi.string(rbuf.rpos, len)
+                rbuf.rpos = rbuf.rpos + len
                 return data
             end
-
         elseif not errno_is_transient[self:errno()] then
             self._errno = boxerrno()
             return nil
         end
+
+        if not self:readable(timeout) then
+            return nil
+        end
+        if timeout <= 0 then
+            break
+        end
+        timeout = timeout - ( fiber.time() - started )
     end
+    self._errno = boxerrno.ETIMEDOUT
     return nil
 end
 
@@ -683,18 +678,18 @@ socket_methods.read = function(self, opts, timeout)
     check_socket(self)
     timeout = timeout or TIMEOUT_INFINITY
     if type(opts) == 'number' then
-        return readchunk(self, opts, timeout)
+        return read(self, opts, timeout, check_limit)
     elseif type(opts) == 'string' then
-        return readline(self, LIMIT_INFINITY, { opts }, timeout)
+        return read(self, LIMIT_INFINITY, timeout, check_delimiter, { opts })
     elseif type(opts) == 'table' then
-        local chunk = opts.chunk or opts.size or 4294967295
+        local chunk = opts.chunk or opts.size or LIMIT_INFINITY
         local delimiter = opts.delimiter or opts.line
         if delimiter == nil then
-            return readchunk(self, chunk, timeout)
+            return read(self, chunk, timeout, check_limit)
         elseif type(delimiter) == 'string' then
-            return readline(self, chunk, { delimiter }, timeout)
+            return read(self, chunk, timeout, check_delimiter, { delimiter })
         elseif type(delimiter) == 'table' then
-            return readline(self, chunk, delimiter, timeout)
+            return read(self, chunk, timeout, check_delimiter, delimiter)
         end
     end
     error('Usage: s:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)')
@@ -706,23 +701,28 @@ socket_methods.write = function(self, octets, timeout)
         timeout = TIMEOUT_INFINITY
     end
 
-    local total_len = #octets
+    local s = ffi.cast('const char *', octets)
+    local p = s
+    local e = s + #octets
+    if p == e then
+        return 0
+    end
+
     local started = fiber.time()
     while true do
-        local written = self:syswrite(octets)
-        if written == nil then
-            if not errno_is_transient[self:errno()] then
-                return nil
+        local written = syswrite(self, p, e - p)
+        if written == 0 then
+            return p - s -- eof
+        elseif written ~= nil then
+            p = p + written
+            assert(p <= e)
+            if p == e then
+                return e - s
             end
-            written = 0
+        elseif not errno_is_transient[self:errno()] then
+            return nil
         end
 
-        if written == string.len(octets) then
-            return total_len
-        end
-        if written > 0 then
-            octets = string.sub(octets, written + 1)
-        end
         timeout = timeout - (fiber.time() - started)
         if timeout <= 0 or not self:writable(timeout) then
             break
diff --git a/src/lua/buffer.lua b/src/lua/buffer.lua
new file mode 100644
index 0000000000000000000000000000000000000000..ba336ac665cb07b9193a6beaadbbd2a49688baa4
--- /dev/null
+++ b/src/lua/buffer.lua
@@ -0,0 +1,195 @@
+-- buffer.lua (internal file)
+
+local ffi = require('ffi')
+
+ffi.cdef[[
+struct slab_cache;
+struct slab_cache *
+tarantool_lua_slab_cache();
+
+struct ibuf
+{
+    struct slab_cache *slabc;
+    char *buf;
+    /** Start of input. */
+    char *rpos;
+    /** End of useful input */
+    char *wpos;
+    /** End of ibuf. */
+    char *epos;
+    size_t start_capacity;
+};
+
+void
+ibuf_create(struct ibuf *ibuf, struct slab_cache *slabc, size_t start_capacity);
+
+void
+ibuf_destroy(struct ibuf *ibuf);
+
+void
+ibuf_reinit(struct ibuf *ibuf);
+
+void *
+ibuf_reserve_nothrow_slow(struct ibuf *ibuf, size_t size);
+]]
+
+local builtin = ffi.C
+local ibuf_t = ffi.typeof('struct ibuf')
+
+local function errorf(method, s, ...)
+    error(string.format(s, ...))
+end
+
+local function checkibuf(buf, method)
+    if not ffi.istype(ibuf_t, buf) then
+        errorf('Attempt to call method without object, use ibuf:%s()', method)
+    end
+end
+
+local function ibuf_capacity(buf)
+    checkibuf(buf, 'capacity')
+    return tonumber(buf.epos - buf.buf)
+end
+
+local function ibuf_pos(buf)
+    checkibuf(buf, 'pos')
+    return tonumber(buf.rpos - buf.buf)
+end
+
+local function ibuf_used(buf)
+    checkibuf(buf, 'size')
+    return tonumber(buf.wpos - buf.rpos)
+end
+
+local function ibuf_unused(buf)
+    checkibuf(buf, 'unused')
+    return tonumber(buf.epos - buf.wpos)
+end
+
+local function ibuf_recycle(buf)
+    checkibuf(buf, 'recycle')
+    builtin.ibuf_reinit(buf)
+end
+
+local function ibuf_reset(buf)
+    checkibuf(buf, 'reset')
+    buf.rpos = buf.buf
+    buf.wpos = buf.buf
+end
+
+local function ibuf_reserve_slow(buf, size)
+    local ptr = builtin.ibuf_reserve_nothrow_slow(buf, size)
+    if ptr == nil then
+        errorf("Failed to allocate %d bytes in ibuf", size)
+    end
+    return ffi.cast('char *', ptr)
+end
+
+local function ibuf_reserve(buf, size)
+    checkibuf(buf, 'reserve')
+    if buf.wpos + size <= buf.epos then
+        return buf.wpos
+    end
+    return ibuf_reserve_slow(buf, size)
+end
+
+local function ibuf_alloc(buf, size)
+    checkibuf(buf, 'alloc')
+    local wpos
+    if buf.wpos + size <= buf.epos then
+        wpos = buf.wpos
+    else
+        wpos = ibuf_reserve_slow(buf, size)
+    end
+    buf.wpos = buf.wpos + size
+    return wpos
+end
+
+local function checksize(buf, size)
+    if ibuf.rpos + size > ibuf.wpos then
+        errorf("Attempt to read out of range bytes: needed=%d size=%d",
+            tonumber(size), ibuf_used(buf))
+    end
+end
+
+local function ibuf_checksize(buf, size)
+    checkibuf(buf, 'checksize')
+    checksize(buf, size)
+    return buf.rpos
+end
+
+local function ibuf_read(buf, size)
+    checkibuf(buf, 'read')
+    checksize(buf, size)
+    local rpos = buf.rpos
+    buf.rpos = rpos + size
+    return rpos
+end
+
+local ibuf_properties = {
+    size = ibuf_used;
+    capacity = ibuf_capacity;
+    pos = ibuf_pos;
+    unused = ibuf_unused;
+}
+
+local function ibuf_serialize(buf)
+    local properties = { rpos = buf.rpos, wpos = buf.wpos }
+    for key, getter in pairs(ibuf_properties) do
+        properties[key] = getter(buf)
+    end
+    return { ibuf = properties }
+end
+
+local ibuf_methods = {
+    recycle = ibuf_recycle;
+    reset = ibuf_reset;
+
+    reserve = ibuf_reserve;
+    alloc = ibuf_alloc;
+
+    checksize = ibuf_checksize;
+    read = ibuf_read;
+    __serialize = ibuf_serialize;
+}
+
+local function ibuf_index(buf, key)
+    local property = ibuf_properties[key]
+    if property ~= nil then
+        return property(buf)
+    end
+    local method = ibuf_methods[key]
+    if method ~= nil then
+        return method
+    end
+    return nil
+end
+
+local function ibuf_tostring(ibuf)
+    return '<ibuf>'
+end
+local ibuf_mt = {
+    __gc = ibuf_recycle;
+    __index = ibuf_index;
+    __tostring = ibuf_tostring;
+};
+
+ffi.metatype(ibuf_t, ibuf_mt);
+
+local function ibuf_new(arg, arg2)
+    local buf = ffi.new(ibuf_t)
+    local slabc = builtin.tarantool_lua_slab_cache()
+    builtin.ibuf_create(buf, slabc, 16320)
+    if arg == nil then
+        return buf
+    elseif type(arg) == 'number' then
+        ibuf_reserve(buf, arg)
+        return buf
+    end
+    errorf('Usage: ibuf([size])')
+end
+
+return {
+    ibuf = ibuf_new;
+    IBUF_SHARED = ibuf_new();
+}
diff --git a/src/lua/init.cc b/src/lua/init.cc
index 8e66de3f4e96e8313568fa495a04fa05aa4f922b..fde8c08f45fd2fc7badc849e7b27dde01fc942a4 100644
--- a/src/lua/init.cc
+++ b/src/lua/init.cc
@@ -75,6 +75,7 @@ extern char uuid_lua[],
 	fun_lua[],
 	digest_lua[],
 	init_lua[],
+	buffer_lua[],
 	fiber_lua[],
 	log_lua[],
 	uri_lua[],
@@ -89,6 +90,7 @@ extern char uuid_lua[],
 static const char *lua_modules[] = {
 	"tarantool", init_lua,
 	"fiber", fiber_lua,
+	"buffer", buffer_lua,
 	"msgpackffi", msgpackffi_lua,
 	"fun", fun_lua,
 	"digest", digest_lua,
@@ -388,12 +390,19 @@ tarantool_lua_init(const char *tarantool_bin, int argc, char **argv)
 
 char *history = NULL;
 
+struct slab_cache *
+tarantool_lua_slab_cache()
+{
+	return &cord()->slabc;
+}
+
 extern "C" const char *
 tarantool_error_message(void)
 {
 	/* called only from error handler */
-	assert(fiber()->exception != NULL);
-	return fiber()->exception->errmsg();
+	Exception *e = diag_last_error(&fiber()->diag);
+	assert(e != NULL);
+	return e->errmsg();
 }
 
 /**
diff --git a/src/lua/init.h b/src/lua/init.h
index d779255195dc807d1ed74067b82c01a6f2c0ad00..af863c6dca7b1c67322cd504c3fdf5b68d5681ca 100644
--- a/src/lua/init.h
+++ b/src/lua/init.h
@@ -83,6 +83,9 @@ tarantool_lua(struct lua_State *L,
 
 extern char *history;
 
+extern "C" struct slab_cache *
+tarantool_lua_slab_cache();
+
 /**
  * Return last exception text
  */
diff --git a/src/lua/log.lua b/src/lua/log.lua
index ae638a2866751c7bb3ea35a6214251ef521341b1..d086bcc6dfb7b0c18c6dc8030e851d99f7bc9710 100644
--- a/src/lua/log.lua
+++ b/src/lua/log.lua
@@ -19,9 +19,14 @@ ffi.cdef[[
     };
 
     pid_t logger_pid;
+    extern int log_level;
 ]]
 
 local function say(level, fmt, ...)
+    if ffi.C.log_level < level then
+-- don't waste cycles on debug.getinfo()
+        return
+    end
     local debug = require('debug')
     local str = string.format(tostring(fmt), ...)
     local frame = debug.getinfo(3, "Sl")
diff --git a/src/lua/msgpack.cc b/src/lua/msgpack.cc
index 22f8a78c0af307f5fdbced02f1399b3c04b4eee8..a31e674cdbead2963fe675e6baa8a8ac40605dd3 100644
--- a/src/lua/msgpack.cc
+++ b/src/lua/msgpack.cc
@@ -46,7 +46,8 @@ extern "C" {
 struct luaL_serializer *luaL_msgpack_default = NULL;
 
 static enum mp_type
-luamp_encode_extension_default(struct lua_State *L, int idx, struct obuf *buf);
+luamp_encode_extension_default(struct lua_State *L, int idx,
+			       struct mpstream *stream);
 
 static void
 luamp_decode_extension_default(struct lua_State *L, const char **data);
@@ -57,102 +58,110 @@ static luamp_decode_extension_f luamp_decode_extension =
 		luamp_decode_extension_default;
 
 void
-luamp_encode_array(struct luaL_serializer *cfg, struct obuf *buf, uint32_t size)
+luamp_encode_array(struct luaL_serializer *cfg, struct mpstream *stream,
+		   uint32_t size)
 {
 	(void) cfg;
 	assert(mp_sizeof_array(size) <= 5);
-	char *data = obuf_ensure(buf, 5);
+	char *data = mpstream_reserve(stream, 5);
 	char *pos = mp_encode_array(data, size);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_map(struct luaL_serializer *cfg, struct obuf *buf, uint32_t size)
+luamp_encode_map(struct luaL_serializer *cfg, struct mpstream *stream,
+		 uint32_t size)
 {
 	(void) cfg;
 	assert(mp_sizeof_map(size) <= 5);
-	char *data = obuf_ensure(buf, 5);
+	char *data = mpstream_reserve(stream, 5);
 	char *pos = mp_encode_map(data, size);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_uint(struct luaL_serializer *cfg, struct obuf *buf, uint64_t num)
+luamp_encode_uint(struct luaL_serializer *cfg, struct mpstream *stream,
+		  uint64_t num)
 {
 	(void) cfg;
 	assert(mp_sizeof_uint(num) <= 9);
-	char *data = obuf_ensure(buf, 9);
+	char *data = mpstream_reserve(stream, 9);
 	char *pos = mp_encode_uint(data, num);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_int(struct luaL_serializer *cfg, struct obuf *buf, int64_t num)
+luamp_encode_int(struct luaL_serializer *cfg, struct mpstream *stream,
+		 int64_t num)
 {
 	(void) cfg;
 	assert(mp_sizeof_int(num) <= 9);
-	char *data = obuf_ensure(buf, 9);
+	char *data = mpstream_reserve(stream, 9);
 	char *pos = mp_encode_int(data, num);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_float(struct luaL_serializer *cfg, struct obuf *buf, float num)
+luamp_encode_float(struct luaL_serializer *cfg, struct mpstream *stream,
+		   float num)
 {
 	(void) cfg;
 	assert(mp_sizeof_float(num) <= 5);
-	char *data = obuf_ensure(buf, 5);
+	char *data = mpstream_reserve(stream, 5);
 	char *pos = mp_encode_float(data, num);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_double(struct luaL_serializer *cfg, struct obuf *buf, double num)
+luamp_encode_double(struct luaL_serializer *cfg, struct mpstream *stream,
+		    double num)
 {
 	(void) cfg;
 	assert(mp_sizeof_double(num) <= 9);
-	char *data = obuf_ensure(buf, 9);
+	char *data = mpstream_reserve(stream, 9);
 	char *pos = mp_encode_double(data, num);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_str(struct luaL_serializer *cfg, struct obuf *buf,
+luamp_encode_str(struct luaL_serializer *cfg, struct mpstream *stream,
 		 const char *str, uint32_t len)
 {
 	(void) cfg;
 	assert(mp_sizeof_str(len) <= 5 + len);
-	char *data = obuf_ensure(buf, 5 + len);
+	char *data = mpstream_reserve(stream, 5 + len);
 	char *pos = mp_encode_str(data, str, len);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_nil(struct luaL_serializer *cfg, struct obuf *buf)
+luamp_encode_nil(struct luaL_serializer *cfg, struct mpstream *stream)
 {
 	(void) cfg;
 	assert(mp_sizeof_nil() <= 1);
-	char *data = obuf_ensure(buf, 1);
+	char *data = mpstream_reserve(stream, 1);
 	char *pos = mp_encode_nil(data);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 void
-luamp_encode_bool(struct luaL_serializer *cfg, struct obuf *buf, bool val)
+luamp_encode_bool(struct luaL_serializer *cfg, struct mpstream *stream,
+		  bool val)
 {
 	(void) cfg;
 	assert(mp_sizeof_bool(val) <= 1);
-	char *data = obuf_ensure(buf, 1);
+	char *data = mpstream_reserve(stream, 1);
 	char *pos = mp_encode_bool(data, val);
-	obuf_advance(buf, pos - data);
+	mpstream_advance(stream, pos - data);
 }
 
 static mp_type
-luamp_encode_extension_default(struct lua_State *L, int idx, struct obuf *b)
+luamp_encode_extension_default(struct lua_State *L, int idx,
+			       struct mpstream *stream)
 {
 	(void) L;
 	(void) idx;
-	(void) b;
+	(void) stream;
 	return MP_EXT;
 }
 
@@ -185,8 +194,8 @@ luamp_set_decode_extension(luamp_decode_extension_f handler)
 }
 
 static enum mp_type
-luamp_encode_r(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
-	       int level)
+luamp_encode_r(struct lua_State *L, struct luaL_serializer *cfg,
+	       struct mpstream *stream, int level)
 {
 	int index = lua_gettop(L);
 
@@ -195,7 +204,7 @@ luamp_encode_r(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
 	luaL_tofield(L, cfg, index, &field);
 	if (field.type == MP_EXT) {
 		/* Run trigger if type can't be encoded */
-		enum mp_type type = luamp_encode_extension(L, index, b);
+		enum mp_type type = luamp_encode_extension(L, index, stream);
 		if (type != MP_EXT)
 			return type; /* Value has been packed by the trigger */
 		/* Try to convert value to serializable type */
@@ -203,55 +212,55 @@ luamp_encode_r(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
 	}
 	switch (field.type) {
 	case MP_UINT:
-		luamp_encode_uint(cfg, b, field.ival);
+		luamp_encode_uint(cfg, stream, field.ival);
 		return MP_UINT;
 	case MP_STR:
-		luamp_encode_str(cfg, b, field.sval.data, field.sval.len);
+		luamp_encode_str(cfg, stream, field.sval.data, field.sval.len);
 		return MP_STR;
 	case MP_BIN:
-		luamp_encode_str(cfg, b, field.sval.data, field.sval.len);
+		luamp_encode_str(cfg, stream, field.sval.data, field.sval.len);
 		return MP_BIN;
 	case MP_INT:
-		luamp_encode_int(cfg, b, field.ival);
+		luamp_encode_int(cfg, stream, field.ival);
 		return MP_INT;
 	case MP_FLOAT:
-		luamp_encode_float(cfg, b, field.fval);
+		luamp_encode_float(cfg, stream, field.fval);
 		return MP_FLOAT;
 	case MP_DOUBLE:
-		luamp_encode_double(cfg, b, field.dval);
+		luamp_encode_double(cfg, stream, field.dval);
 		return MP_DOUBLE;
 	case MP_BOOL:
-		luamp_encode_bool(cfg, b, field.bval);
+		luamp_encode_bool(cfg, stream, field.bval);
 		return MP_BOOL;
 	case MP_NIL:
-		luamp_encode_nil(cfg, b);
+		luamp_encode_nil(cfg, stream);
 		return MP_NIL;
 	case MP_MAP:
 		/* Map */
 		if (level >= cfg->encode_max_depth) {
-			luamp_encode_nil(cfg, b); /* Limit nested maps */
+			luamp_encode_nil(cfg, stream); /* Limit nested maps */
 			return MP_NIL;
 		}
-		luamp_encode_map(cfg, b, field.size);
+		luamp_encode_map(cfg, stream, field.size);
 		lua_pushnil(L);  /* first key */
 		while (lua_next(L, index) != 0) {
 			lua_pushvalue(L, -2);
-			luamp_encode_r(L, cfg, b, level + 1);
+			luamp_encode_r(L, cfg, stream, level + 1);
 			lua_pop(L, 1);
-			luamp_encode_r(L, cfg, b, level + 1);
+			luamp_encode_r(L, cfg, stream, level + 1);
 			lua_pop(L, 1);
 		}
 		return MP_MAP;
 	case MP_ARRAY:
 		/* Array */
 		if (level >= cfg->encode_max_depth) {
-			luamp_encode_nil(cfg, b); /* Limit nested arrays */
+			luamp_encode_nil(cfg, stream); /* Limit nested arrays */
 			return MP_NIL;
 		}
-		luamp_encode_array(cfg, b, field.size);
+		luamp_encode_array(cfg, stream, field.size);
 		for (uint32_t i = 0; i < field.size; i++) {
 			lua_rawgeti(L, index, i + 1);
-			luamp_encode_r(L, cfg, b, level + 1);
+			luamp_encode_r(L, cfg, stream, level + 1);
 			lua_pop(L, 1);
 		}
 		return MP_ARRAY;
@@ -263,8 +272,8 @@ luamp_encode_r(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
 }
 
 enum mp_type
-luamp_encode(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
-	     int index)
+luamp_encode(struct lua_State *L, struct luaL_serializer *cfg,
+	     struct mpstream *stream, int index)
 {
 	int top = lua_gettop(L);
 	if (index < 0)
@@ -275,7 +284,7 @@ luamp_encode(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *b,
 		lua_pushvalue(L, index); /* copy a value to the stack top */
 	}
 
-	enum mp_type top_type = luamp_encode_r(L, cfg, b, 0);
+	enum mp_type top_type = luamp_encode_r(L, cfg, stream, 0);
 
 	if (!on_top) {
 		lua_remove(L, top + 1); /* remove a value copy */
@@ -367,15 +376,20 @@ lua_msgpack_encode(lua_State *L)
 		luaL_error(L, "msgpack.encode: a Lua object expected");
 
 	struct luaL_serializer *cfg = luaL_checkserializer(L);
+	(void) cfg;
+
+	struct region *gc= &fiber()->gc;
+	RegionGuard guard(gc);
+	struct mpstream stream;
+	mpstream_init(&stream, gc, region_reserve_cb, region_alloc_cb);
 
-	RegionGuard region_guard(&fiber()->gc);
+	luamp_encode_r(L, cfg, &stream, 0);
+	mpstream_flush(&stream);
 
-	struct obuf buf;
-	obuf_create(&buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
-	luamp_encode_r(L, cfg, &buf, 0);
+	size_t len = region_used(gc) - guard.used;
+	const char *res = (char *) region_join(gc, len);
 
-	const char *res = obuf_join(&buf);
-	lua_pushlstring(L, res, obuf_size(&buf));
+	lua_pushlstring(L, res, len);
 	return 1;
 }
 
diff --git a/src/lua/msgpack.h b/src/lua/msgpack.h
index de5d65047082b70f026ac746012060cb7cb5ef16..ad6bce4138d2630098cc3a6e8f1555b2101d0d53 100644
--- a/src/lua/msgpack.h
+++ b/src/lua/msgpack.h
@@ -48,48 +48,125 @@ extern "C" {
  */
 extern luaL_serializer *luaL_msgpack_default;
 
-struct obuf;
+/**
+ * A streaming API so that it's possible to encode to any output
+ * stream.
+ */
+
+extern "C" {
+/**
+ * Ask the allocator to reserve at least size bytes. It can reserve
+ * more, and update *size with the new size.
+ */
+typedef	void *(*luamp_reserve_f)(void *ctx, size_t *size);
+
+/** Actually use the bytes. */
+typedef	void *(*luamp_alloc_f)(void *ctx, size_t size);
+}
+
+struct mpstream {
+	/**
+	 * When pos >= end, or required size doesn't fit in
+	 * pos..end range alloc() is called to advance the stream
+	 * and reserve() to get a new chunk.
+	 */
+	char *buf, *pos, *end;
+	void *ctx;
+	luamp_reserve_f reserve;
+	luamp_alloc_f alloc;
+};
+
+static inline void
+mpstream_flush(struct mpstream *stream)
+{
+	stream->alloc(stream->ctx, stream->pos - stream->buf);
+	stream->buf = stream->pos;
+}
+
+static inline void
+mpstream_reset(struct mpstream *stream)
+{
+	size_t size = 0;
+	stream->buf = (char *) stream->reserve(stream->ctx, &size);
+	stream->pos = stream->buf;
+	stream->end = stream->pos + size;
+}
+
+static inline void
+mpstream_init(struct mpstream *stream, void *ctx,
+	      luamp_reserve_f reserve, luamp_alloc_f alloc)
+{
+	stream->ctx = ctx;
+	stream->reserve = reserve;
+	stream->alloc = alloc;
+	mpstream_reset(stream);
+}
+
+static inline char *
+mpstream_reserve(struct mpstream *stream, size_t size)
+{
+	if (stream->pos + size > stream->end) {
+		stream->alloc(stream->ctx, stream->pos - stream->buf);
+		stream->buf = (char *) stream->reserve(stream->ctx, &size);
+		stream->pos = stream->buf;
+		stream->end = stream->pos + size;
+	}
+	return stream->pos;
+}
+
+static inline void
+mpstream_advance(struct mpstream *stream, size_t size)
+{
+	assert(stream->pos + size <= stream->end);
+	stream->pos += size;
+}
 
 enum { LUAMP_ALLOC_FACTOR = 256 };
 
 void
-luamp_encode_array(struct luaL_serializer *cfg, struct obuf *buf, uint32_t size);
+luamp_encode_array(struct luaL_serializer *cfg, struct mpstream *stream,
+		   uint32_t size);
 
 void
-luamp_encode_map(struct luaL_serializer *cfg, struct obuf *buf, uint32_t size);
+luamp_encode_map(struct luaL_serializer *cfg, struct mpstream *stream, uint32_t size);
 
 void
-luamp_encode_uint(struct luaL_serializer *cfg, struct obuf *buf, uint64_t num);
+luamp_encode_uint(struct luaL_serializer *cfg, struct mpstream *stream,
+		  uint64_t num);
 
 void
-luamp_encode_int(struct luaL_serializer *cfg, struct obuf *buf, int64_t num);
+luamp_encode_int(struct luaL_serializer *cfg, struct mpstream *stream,
+		 int64_t num);
 
 void
-luamp_encode_float(struct luaL_serializer *cfg, struct obuf *buf, float num);
+luamp_encode_float(struct luaL_serializer *cfg, struct mpstream *stream,
+		   float num);
 
 void
-luamp_encode_double(struct luaL_serializer *cfg, struct obuf *buf, double num);
+luamp_encode_double(struct luaL_serializer *cfg, struct mpstream *stream,
+		    double num);
 
 void
-luamp_encode_str(struct luaL_serializer *cfg, struct obuf *buf, const char *str,
-		 uint32_t len);
+luamp_encode_str(struct luaL_serializer *cfg, struct mpstream *stream,
+		 const char *str, uint32_t len);
 
 void
 luamp_encode_nil(struct luaL_serializer *cfg);
 
 void
-luamp_encode_bool(struct luaL_serializer *cfg, struct obuf *buf, bool val);
+luamp_encode_bool(struct luaL_serializer *cfg, struct mpstream *stream,
+		  bool val);
 
 enum mp_type
-luamp_encode(struct lua_State *L, struct luaL_serializer *cfg, struct obuf *buf,
-	     int index);
+luamp_encode(struct lua_State *L, struct luaL_serializer *cfg,
+	     struct mpstream *stream, int index);
 
 void
 luamp_decode(struct lua_State *L, struct luaL_serializer *cfg,
 	     const char **data);
 
 typedef enum mp_type
-(*luamp_encode_extension_f)(struct lua_State *, int, struct obuf *);
+(*luamp_encode_extension_f)(struct lua_State *, int, struct mpstream *);
 
 /**
  * @brief Set a callback that executed by encoder on unsupported Lua type
diff --git a/src/lua/msgpackffi.lua b/src/lua/msgpackffi.lua
index df937a49e888e3958929701a2d4fb47fc7ab470a..7fd585ca630c714dfb7df3fd1006f7dd7a428b7a 100644
--- a/src/lua/msgpackffi.lua
+++ b/src/lua/msgpackffi.lua
@@ -1,6 +1,7 @@
 -- msgpackffi.lua (internal file)
 
 local ffi = require('ffi')
+local buffer = require('buffer')
 local builtin = ffi.C
 local msgpack = require('msgpack') -- .NULL, .array_mt, .map_mt, .cfg
 local MAXNESTING = 16
@@ -27,37 +28,6 @@ local bswap_u32 = bit.bswap
 local bswap_u64 = bit.bswap
 --]]
 
---------------------------------------------------------------------------------
---- Buffer
---------------------------------------------------------------------------------
-
-local DEFAULT_CAPACITY = 4096;
-
-local tmpbuf = {}
-tmpbuf.s = ffi.new("char[?]", DEFAULT_CAPACITY)
-tmpbuf.e = tmpbuf.s + DEFAULT_CAPACITY
-tmpbuf.p = tmpbuf.s
-tmpbuf.reserve = function(buf, needed)
-    if buf.p + needed <= buf.e then
-        return
-    end
-
-    local size = buf.p - buf.s
-    local capacity = buf.e - buf.s
-    while capacity - size < needed do
-        capacity = 2 * capacity
-    end
-
-    local s = ffi.new("char[?]", capacity)
-    ffi.copy(s, buf.s, size)
-    buf.s = s
-    buf.e = s + capacity
-    buf.p = s + size
-end
-tmpbuf.reset = function(buf)
-    buf.p = buf.s
-end
-
 --------------------------------------------------------------------------------
 -- Encoder
 --------------------------------------------------------------------------------
@@ -76,54 +46,46 @@ local function on_encode(ctype_or_udataname, callback)
 end
 
 local function encode_fix(buf, code, num)
-    buf:reserve(1)
-    buf.p[0] = bit.bor(code, tonumber(num))
-    -- buf.p[0] = bit.bor(code, num) -- LuaJIT 2.1
-    buf.p = buf.p + 1
+    local p = buf:alloc(1)
+    p[0] = bit.bor(code, tonumber(num))
 end
 
 local function encode_u8(buf, code, num)
-    buf:reserve(2)
-    buf.p[0] = code
-    ffi.cast(uint16_ptr_t, buf.p + 1)[0] = num
-    buf.p = buf.p + 2
+    local p = buf:alloc(2)
+    p[0] = code
+    ffi.cast(uint8_ptr_t, p + 1)[0] = num
 end
 
 local function encode_u16(buf, code, num)
-    buf:reserve(3)
-    buf.p[0] = code
-    ffi.cast(uint16_ptr_t, buf.p + 1)[0] = bswap_u16(num)
-    buf.p = buf.p + 3
+    local p = buf:alloc(3)
+    p[0] = code
+    ffi.cast(uint16_ptr_t, p + 1)[0] = bswap_u16(num)
 end
 
 local function encode_u32(buf, code, num)
-    buf:reserve(5)
-    buf.p[0] = code
-    ffi.cast(uint32_ptr_t, buf.p + 1)[0] = bswap_u32(num)
-    buf.p = buf.p + 5
+    local p = buf:alloc(5)
+    p[0] = code
+    ffi.cast(uint32_ptr_t, p + 1)[0] = bswap_u32(num)
 end
 
 local function encode_u64(buf, code, num)
-    buf:reserve(9)
-    buf.p[0] = code
-    ffi.cast(uint64_ptr_t, buf.p + 1)[0] = bswap_u64(ffi.cast('uint64_t', num))
-    buf.p = buf.p + 9
+    local p = buf:alloc(9)
+    p[0] = code
+    ffi.cast(uint64_ptr_t, p + 1)[0] = bswap_u64(ffi.cast('uint64_t', num))
 end
 
 local function encode_float(buf, num)
-    buf:reserve(5)
-    buf.p[0] = 0xca;
-    ffi.cast(float_ptr_t, buf.p + 1)[0] = num
-    ffi.cast(uint32_ptr_t, buf.p + 1)[0] = bswap_u32(ffi.cast(uint32_ptr_t, buf.p + 1)[0])
-    buf.p = buf.p + 5
+    local p = buf:alloc(5)
+    p[0] = 0xca;
+    ffi.cast(float_ptr_t, p + 1)[0] = num
+    ffi.cast(uint32_ptr_t, p + 1)[0] = bswap_u32(ffi.cast(uint32_ptr_t, p + 1)[0])
 end
 
 local function encode_double(buf, num)
-    buf:reserve(9)
-    buf.p[0] = 0xcb;
-    ffi.cast(double_ptr_t, buf.p + 1)[0] = num
-    ffi.cast(uint64_ptr_t, buf.p + 1)[0] = bswap_u64(ffi.cast(uint64_ptr_t, buf.p + 1)[0])
-    buf.p = buf.p + 9
+    local p = buf:alloc(9)
+    p[0] = 0xcb;
+    ffi.cast(double_ptr_t, p + 1)[0] = num
+    ffi.cast(uint64_ptr_t, p + 1)[0] = bswap_u64(ffi.cast(uint64_ptr_t, p + 1)[0])
 end
 
 local function encode_int(buf, num)
@@ -166,8 +128,8 @@ local function encode_str(buf, str)
     else
         encode_u32(buf, 0xdb, len)
     end
-    ffi.copy(buf.p, str, len)
-    buf.p = buf.p + len
+    local p = buf:alloc(len)
+    ffi.copy(p, str, len)
 end
 
 local function encode_array(buf, size)
@@ -199,9 +161,8 @@ local function encode_bool_cdata(buf, val)
 end
 
 local function encode_nil(buf)
-    buf:reserve(1)
-    buf.p[0] = 0xc0
-    buf.p = buf.p + 1
+    local p = buf:alloc(1)
+    p[0] = 0xc0
 end
 
 local function encode_r(buf, obj, level)
@@ -263,9 +224,12 @@ local function encode_r(buf, obj, level)
 end
 
 local function encode(obj)
+    local tmpbuf = buffer.IBUF_SHARED
     tmpbuf:reset()
     encode_r(tmpbuf, obj, 0)
-    return ffi.string(tmpbuf.s, tmpbuf.p - tmpbuf.s)
+    local r = ffi.string(tmpbuf.rpos, tmpbuf.size)
+    tmpbuf:recycle()
+    return r
 end
 
 on_encode(ffi.typeof('uint8_t'), encode_int)
@@ -503,6 +467,7 @@ end
 --------------------------------------------------------------------------------
 
 local function encode_tuple(obj)
+    local tmpbuf = buffer.IBUF_SHARED
     tmpbuf:reset()
     if obj == nil then
         encode_fix(tmpbuf, 0x90, 0)  -- empty array
@@ -516,7 +481,7 @@ local function encode_tuple(obj)
         encode_fix(tmpbuf, 0x90, 1)  -- array of one element
         encode_r(tmpbuf, obj, 1)
     end
-    return tmpbuf.s, tmpbuf.p
+    return tmpbuf.rpos, tmpbuf.wpos
 end
 
 --------------------------------------------------------------------------------
diff --git a/src/lua/pickle.cc b/src/lua/pickle.cc
index 9666a2e34355127a125d18953b8c0b9493d2b71d..0120c74a103a63bc585cd52a9dabcfe1ca0cf78e 100644
--- a/src/lua/pickle.cc
+++ b/src/lua/pickle.cc
@@ -53,9 +53,8 @@ lbox_pack(struct lua_State *L)
 	size_t size;
 	const char *str;
 
-	RegionGuard region_guard(&fiber()->gc);
-	struct obuf buf;
-	obuf_create(&buf, &fiber()->gc, LUAMP_ALLOC_FACTOR);
+	struct region *buf = &fiber()->gc;
+	RegionGuard region_guard(buf);
 
 	struct luaL_field field;
 	double dbl;
@@ -72,7 +71,7 @@ lbox_pack(struct lua_State *L)
 			if (field.type != MP_UINT && field.type != MP_INT)
 				luaL_error(L, "pickle.pack: expected 8-bit int");
 
-			obuf_dup(&buf, &field.ival, sizeof(uint8_t));
+			region_dup(buf, &field.ival, sizeof(uint8_t));
 			break;
 		case 'S':
 		case 's':
@@ -80,7 +79,7 @@ lbox_pack(struct lua_State *L)
 			if (field.type != MP_UINT && field.type != MP_INT)
 				luaL_error(L, "pickle.pack: expected 16-bit int");
 
-			obuf_dup(&buf, &field.ival, sizeof(uint16_t));
+			region_dup(buf, &field.ival, sizeof(uint16_t));
 			break;
 		case 'n':
 			/* signed and unsigned 16-bit big endian integers */
@@ -88,7 +87,7 @@ lbox_pack(struct lua_State *L)
 				luaL_error(L, "pickle.pack: expected 16-bit int");
 
 			field.ival = (uint16_t) htons((uint16_t) field.ival);
-			obuf_dup(&buf, &field.ival, sizeof(uint16_t));
+			region_dup(buf, &field.ival, sizeof(uint16_t));
 			break;
 		case 'I':
 		case 'i':
@@ -96,7 +95,7 @@ lbox_pack(struct lua_State *L)
 			if (field.type != MP_UINT && field.ival != MP_INT)
 				luaL_error(L, "pickle.pack: expected 32-bit int");
 
-			obuf_dup(&buf, &field.ival, sizeof(uint32_t));
+			region_dup(buf, &field.ival, sizeof(uint32_t));
 			break;
 		case 'N':
 			/* signed and unsigned 32-bit big endian integers */
@@ -104,7 +103,7 @@ lbox_pack(struct lua_State *L)
 				luaL_error(L, "pickle.pack: expected 32-bit int");
 
 			field.ival = htonl(field.ival);
-			obuf_dup(&buf, &field.ival, sizeof(uint32_t));
+			region_dup(buf, &field.ival, sizeof(uint32_t));
 			break;
 		case 'L':
 		case 'l':
@@ -112,7 +111,7 @@ lbox_pack(struct lua_State *L)
 			if (field.type != MP_UINT && field.type != MP_INT)
 				luaL_error(L, "pickle.pack: expected 64-bit int");
 
-			obuf_dup(&buf, &field.ival, sizeof(uint64_t));
+			region_dup(buf, &field.ival, sizeof(uint64_t));
 			break;
 		case 'Q':
 		case 'q':
@@ -121,21 +120,21 @@ lbox_pack(struct lua_State *L)
 				luaL_error(L, "pickle.pack: expected 64-bit int");
 
 			field.ival = bswap_u64(field.ival);
-			obuf_dup(&buf,  &field.ival, sizeof(uint64_t));
+			region_dup(buf,  &field.ival, sizeof(uint64_t));
 			break;
 		case 'd':
 			dbl = (double) lua_tonumber(L, i);
-			obuf_dup(&buf, &dbl, sizeof(double));
+			region_dup(buf, &dbl, sizeof(double));
 			break;
 		case 'f':
 			flt = (float) lua_tonumber(L, i);
-			obuf_dup(&buf, &flt, sizeof(float));
+			region_dup(buf, &flt, sizeof(float));
 			break;
 		case 'A':
 		case 'a':
 			/* A sequence of bytes */
 			str = luaL_checklstring(L, i, &size);
-			obuf_dup(&buf, str, size);
+			region_dup(buf, str, size);
 			break;
 		default:
 			luaL_error(L, "pickle.pack: unsupported pack "
@@ -145,8 +144,9 @@ lbox_pack(struct lua_State *L)
 		format++;
 	}
 
-	const char *res = obuf_join(&buf);
-	lua_pushlstring(L, res, obuf_size(&buf));
+	size_t len = region_used(buf) - region_guard.used;
+	const char *res = (char *) region_join(buf, len);
+	lua_pushlstring(L, res, len);
 	return 1;
 }
 
diff --git a/src/lua/utils.cc b/src/lua/utils.cc
index fa62f3901be48d6fe14501b760ed2dfef73863e0..9c61376e58cd1e8a966abd1b72b3c7951a735b31 100644
--- a/src/lua/utils.cc
+++ b/src/lua/utils.cc
@@ -96,6 +96,24 @@ luaL_ctypeid(struct lua_State *L, const char *ctypename)
 	return ctypeid;
 }
 
+int
+luaL_cdef(struct lua_State *L, const char *what)
+{
+	int idx = lua_gettop(L);
+	(void) idx;
+	/* This function calls ffi.cdef  */
+
+	/* Get ffi.typeof function */
+	luaL_loadstring(L, "return require('ffi').cdef");
+	lua_call(L, 0, 1);
+	/* FFI must exist */
+	assert(lua_gettop(L) == idx + 1 && lua_isfunction(L, idx + 1));
+	/* Push the argument to ffi.cdef */
+	lua_pushstring(L, what);
+	/* Call ffi.cdef() */
+	return lua_pcall(L, 1, 0, 0);
+}
+
 int
 luaL_setcdatagc(struct lua_State *L, int idx)
 {
@@ -708,9 +726,10 @@ tarantool_lua_utils_init(struct lua_State *L)
 	return 0;
 }
 
+const struct type type_LuajitError = make_type("LuajitError", &type_Exception);
 LuajitError::LuajitError(const char *file, unsigned line,
 			 struct lua_State *L)
-	:Exception(file, line)
+	: Exception(&type_LuajitError, file, line)
 {
 	const char *msg = lua_tostring(L, -1);
 	snprintf(m_errmsg, sizeof(m_errmsg), "%s", msg ? msg : "");
diff --git a/src/lua/utils.h b/src/lua/utils.h
index f1d346695eb1e4f2e3ae449f4cb755ae8bee1bda..d28e79e5587a464c9598d3a8319a02790c333a06 100644
--- a/src/lua/utils.h
+++ b/src/lua/utils.h
@@ -101,6 +101,17 @@ luaL_setcdatagc(struct lua_State *L, int idx);
 LUA_API uint32_t
 luaL_ctypeid(struct lua_State *L, const char *ctypename);
 
+/**
+* @brief Declare symbols for FFI
+* @param L Lua State
+* @param what C definitions
+* @sa ffi.cdef(def)
+* @retval 0 on success
+* @retval LUA_ERRRUN, LUA_ERRMEM, LUA_ERRERR otherwise
+*/
+LUA_API int
+luaL_cdef(struct lua_State *L, const char *ctypename);
+
 /** \endcond public */
 
 static inline lua_Integer
@@ -434,6 +445,7 @@ tarantool_lua_utils_init(struct lua_State *L);
 } /* extern "C" */
 
 #include "exception.h"
+extern const struct type type_LuajitError;
 class LuajitError: public Exception {
 public:
 	LuajitError(const char *file, unsigned line,
diff --git a/src/main.cc b/src/main.cc
index 60befc47e21b0e58ef187ecc9a4796eb31e0e501..752696063a36d5608b3095c1acf59138cd9f8c5c 100644
--- a/src/main.cc
+++ b/src/main.cc
@@ -139,7 +139,7 @@ static void
 sig_snapshot(ev_loop * /* loop */, struct ev_signal * /* w */,
 	     int /* revents */)
 {
-	if (snapshot_pid) {
+	if (snapshot_in_progress) {
 		say_warn("Snapshot process is already running,"
 			" the signal is ignored");
 		return;
diff --git a/src/box/port.c b/src/reflection.c
similarity index 75%
rename from src/box/port.c
rename to src/reflection.c
index 9eed9679704592c59c09bff202ae5567e2b2fcca..f1de38743c2c78d21597034344438249b1fc50d4 100644
--- a/src/box/port.c
+++ b/src/reflection.c
@@ -26,25 +26,22 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
-#include "port.h"
 
-void
-null_port_eof(struct port *port __attribute__((unused)))
-{
-}
+#include "reflection.h"
+/* TODO: sorry, unimplemented: non-trivial designated initializers */
 
-static void
-null_port_add_tuple(struct port *port __attribute__((unused)),
-		    struct tuple *tuple __attribute__((unused)))
-{
-}
-
-static struct port_vtab null_port_vtab = {
-	null_port_add_tuple,
-	null_port_eof,
+const struct method METHODS_SENTINEL = {
+	.owner = NULL,
+	.name = NULL,
+	.rtype = CTYPE_VOID,
+	.atype = {},
+	.nargs = 0,
+	.isconst = false,
+	._spacer = {}
 };
 
-struct port null_port = {
-	/* .vtab = */ &null_port_vtab,
-};
+extern inline bool
+type_assignable(const struct type *type, const struct type *object);
 
+extern inline const struct method *
+type_method_by_name(const struct type *type, const char *name);
diff --git a/src/reflection.h b/src/reflection.h
new file mode 100644
index 0000000000000000000000000000000000000000..746a70fdd6fab954ab8bd12b34b367ee2463d710
--- /dev/null
+++ b/src/reflection.h
@@ -0,0 +1,275 @@
+#ifndef TARANTOOL_REFLECTION_H_INCLUDED
+#define TARANTOOL_REFLECTION_H_INCLUDED
+/*
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above
+ *    copyright notice, this list of conditions and the
+ *    following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ *    copyright notice, this list of conditions and the following
+ *    disclaimer in the documentation and/or other materials
+ *    provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDER> ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * <COPYRIGHT HOLDER> OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stddef.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <string.h> /* strcmp */
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct type;
+struct method;
+
+/**
+ * Primitive C types
+ */
+enum ctype {
+	CTYPE_VOID = 0,
+	CTYPE_INT,
+	CTYPE_CONST_CHAR_PTR
+};
+
+struct type {
+	const char *name;
+	const struct type *parent;
+	const struct method *methods;
+};
+
+inline bool
+type_assignable(const struct type *type, const struct type *object)
+{
+	assert(object != NULL);
+	do {
+		if (object == type)
+			return true;
+		assert(object->parent != object);
+		object = object->parent;
+	} while (object != NULL);
+	return false;
+}
+
+/**
+ * Determine if the specified object is assignment-compatible with
+ * the object represented by type.
+ */
+#define type_cast(T, obj) ({						\
+		T *r = NULL;						\
+		if (type_assignable(&type_ ## T, (obj->type)))		\
+			r = (T *) obj;					\
+		(r);							\
+	})
+
+#if defined(__cplusplus)
+/* Pointer to arbitrary C++ member function */
+typedef void (type::*method_thiscall_f)(void);
+#endif
+
+enum { METHOD_ARG_MAX = 8 };
+
+struct method {
+	const struct type *owner;
+	const char *name;
+	enum ctype rtype;
+	enum ctype atype[METHOD_ARG_MAX];
+	int nargs;
+	bool isconst;
+
+	union {
+		/* Add extra space to get proper struct size in C */
+		void *_spacer[2];
+#if defined(__cplusplus)
+		method_thiscall_f thiscall;
+#endif /* defined(__cplusplus) */
+	};
+};
+
+#define type_foreach_method(m, method)					\
+	for(const struct type *_m = (m); _m != NULL; _m = _m->parent)	\
+		for (const struct method *(method) = _m->methods;	\
+		     (method)->name != NULL; (method)++)
+
+inline const struct method *
+type_method_by_name(const struct type *type, const char *name)
+{
+	type_foreach_method(type, method) {
+		if (strcmp(method->name, name) == 0)
+			return method;
+	}
+	return NULL;
+}
+
+extern const struct method METHODS_SENTINEL;
+
+#if defined(__cplusplus)
+} /* extern "C" */
+
+static_assert(sizeof(((struct method *) 0)->thiscall) <=
+	      sizeof(((struct method *) 0)->_spacer), "sizeof(thiscall)");
+
+/*
+ * Begin of C++ syntax sugar
+ */
+
+/*
+ * Initializer for struct type without methods
+ */
+inline type
+make_type(const char *name, const type *parent)
+{
+	/* TODO: sorry, unimplemented: non-trivial designated initializers */
+	type t;
+	t.name = name;
+	t.parent = parent;
+	t.methods = &METHODS_SENTINEL;
+	return t;
+}
+
+/*
+ * Initializer for struct type with methods
+ */
+inline struct type
+make_type(const char *name, const type *parent, const method *methods)
+{
+	/* TODO: sorry, unimplemented: non-trivial designated initializers */
+	type t;
+	t.name = name;
+	t.parent = parent;
+	t.methods = methods;
+	return t;
+}
+
+template<typename T> inline enum ctype ctypeof();
+template<> inline enum ctype ctypeof<void>() { return CTYPE_VOID; }
+template<> inline enum ctype ctypeof<int>() { return CTYPE_INT; }
+template<> inline enum ctype ctypeof<const char *>() { return CTYPE_CONST_CHAR_PTR; }
+
+/**
+ * \cond false
+ */
+
+template <int N, typename... Args>
+struct method_helper;
+
+/** A helper for recursive templates */
+template <int N, typename A, typename... Args>
+struct method_helper<N, A, Args... >  {
+	static bool
+	invokable(const method *method)
+	{
+		if (method->atype[N] != ctypeof<A>())
+			return false;
+		return method_helper<N + 1, Args... >::invokable(method);
+	}
+
+	static void
+	init(struct method *method)
+	{
+		method->atype[N] = ctypeof<A>();
+		return method_helper<N + 1, Args... >::init(method);
+	}
+};
+
+template <int N>
+struct method_helper<N> {
+	static bool
+	invokable(const method *)
+	{
+		return true;
+	}
+
+	static void
+	init(struct method *method)
+	{
+		method->nargs = N;
+	}
+};
+
+/**
+ * \endcond false
+ */
+
+/**
+ * Initializer for R (T::*)(void) C++ member methods
+ */
+template<typename R, typename... Args, typename T> inline method
+make_method(const struct type *owner, const char *name,
+	R (T::*method_arg)(Args...))
+{
+	struct method m;
+	m.owner = owner;
+	m.name = name;
+	m.thiscall = (method_thiscall_f) method_arg;
+	m.isconst = false;
+	m.rtype = ctypeof<R>();
+	method_helper<0, Args...>::init(&m);
+	return m;
+}
+
+template<typename R, typename... Args, typename T> inline method
+make_method(const struct type *owner, const char *name,
+	R (T::*method_arg)(Args...) const)
+{
+	struct method m = make_method(owner, name, (R (T::*)(Args...)) method_arg);
+	m.isconst = true;
+	return m;
+}
+
+/**
+ * Check if method is invokable with provided argument types
+ */
+template<typename R, typename... Args, typename T> inline bool
+method_invokable(const struct method *method, T *object)
+{
+	static_assert(sizeof...(Args) <= METHOD_ARG_MAX, "too many arguments");
+	if (!type_assignable(method->owner, object->type))
+		return false;
+	if (method->rtype != ctypeof<R>())
+		return false;
+	if (method->nargs != sizeof...(Args))
+		return false;
+	return method_helper<0, Args...>::invokable(method);
+}
+
+template<typename R, typename... Args, typename T> inline bool
+method_invokable(const struct method *method, const T *object)
+{
+	if (!method->isconst)
+		return false;
+	return method_invokable<R, Args...>(method, const_cast<T*>(object));
+}
+
+/**
+ * Invoke method with object and provided arguments.
+ */
+template<typename R, typename... Args, typename T > inline R
+method_invoke(const struct method *method, T *object, Args... args)
+{
+	assert((method_invokable<R, Args...>(method, object)));
+	typedef R (T::*MemberFunction)(Args...);
+	return (object->*(MemberFunction) method->thiscall)(args...);
+}
+
+#endif /* defined(__cplusplus) */
+
+#endif /* TARANTOOL_REFLECTION_H_INCLUDED */
diff --git a/src/say.cc b/src/say.cc
index ec96fddcf17b78bb3a1714cd5c116f27d15cb343..9935e91a963194482d8133164958b61dc21435bc 100644
--- a/src/say.cc
+++ b/src/say.cc
@@ -48,8 +48,7 @@ pid_t logger_pid;
 static bool booting = true;
 static bool logger_background = true;
 static const char *binary_filename;
-static int log_level_default = S_INFO;
-static int *log_level = &log_level_default;
+int log_level = S_INFO;
 
 static void
 sayf(int level, const char *filename, int line, const char *error,
@@ -89,7 +88,7 @@ say_init(const char *argv0)
 void
 say_set_log_level(int new_level)
 {
-	*log_level = new_level;
+	log_level = new_level;
 }
 
 /**
@@ -216,7 +215,7 @@ say_init_file()
 void
 say_logger_init(const char *path, int level, int nonblock, int background)
 {
-	*log_level = level;
+	log_level = level;
 	logger_nonblock = nonblock;
 	logger_background = background;
 	setvbuf(stderr, NULL, _IONBF, 0);
@@ -307,7 +306,7 @@ static void
 sayf(int level, const char *filename, int line, const char *error, const char *format, ...)
 {
 	int errsv = errno; /* Preserve the errno. */
-	if (*log_level < level)
+	if (log_level < level)
 		return;
 	va_list ap;
 	va_start(ap, format);
diff --git a/src/say.h b/src/say.h
index c2f74ed9ec107641b8c2621160d0b5c7e48d92e4..1896958e92a0f7ba31befd51e1d785a59d118e1b 100644
--- a/src/say.h
+++ b/src/say.h
@@ -52,6 +52,7 @@ enum say_level {
 /** \endcond public */
 
 extern int log_fd;
+extern int log_level;
 extern pid_t logger_pid;
 
 void
diff --git a/src/sio.cc b/src/sio.cc
index 8c981f041c845b112d1a4c96a2e57e1434a044ba..ca5e71661030d65fa9ac274a3b60567c34f335c5 100644
--- a/src/sio.cc
+++ b/src/sio.cc
@@ -46,13 +46,15 @@
 #include "say.h"
 #include "trivia/util.h"
 
+const struct type type_SocketError = make_type("SocketError",
+	&type_SystemError);
 SocketError::SocketError(const char *file, unsigned line, int fd,
 			 const char *format, ...)
-	: SystemError(file, line)
+	: SystemError(&type_SocketError, file, line)
 {
 	int save_errno = errno;
 
-	char buf[TNT_ERRMSG_MAX];
+	char buf[EXCEPTION_ERRMSG_MAX];
 
 	va_list ap;
 	va_start(ap, format);
diff --git a/src/sio.h b/src/sio.h
index d527c3802650a54283f883ddae758891936165c1..1bfcecdf2197569ef494ef6b20789d9ffd2cd1ef 100644
--- a/src/sio.h
+++ b/src/sio.h
@@ -43,6 +43,7 @@
 
 enum { SERVICE_NAME_MAXLEN = 32 };
 
+extern const struct type type_SocketError;
 class SocketError: public SystemError {
 public:
 	SocketError(const char *file, unsigned line, int fd,
diff --git a/test/app/console.test.lua b/test/app/console.test.lua
index 3ff518d0e118168531d8a273c2c3e0358c14ace6..ad99b7c11928eb34d3ef09fcbfb20ed011ac65f4 100755
--- a/test/app/console.test.lua
+++ b/test/app/console.test.lua
@@ -16,7 +16,7 @@ os.remove(CONSOLE_SOCKET)
 os.remove(IPROTO_SOCKET)
 
 --
-local EOL = "\n%.%.%.\n"
+local EOL = "\n...\n"
 
 test = tap.test("console")
 
diff --git a/test/app/function1.c b/test/app/function1.c
index b73e500823a27fd7398fd08b153020198ea630da..d0308c662bfe1eb1780a4eafffe0dbc36c2b0216 100644
--- a/test/app/function1.c
+++ b/test/app/function1.c
@@ -1,8 +1,18 @@
 #include "tarantool.h"
+#include <stdio.h>
 
 int
 function1(struct request *request, struct port *port)
 {
 	say_info("-- function1 -  called --");
+	printf("ok - function1\n");
+	return 0;
+}
+
+int
+test(struct request *request, struct port *port)
+{
+	say_info("-- test  -  called --");
+	printf("ok - test\n");
 	return 0;
 }
diff --git a/test/app/function1.result b/test/app/function1.result
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..208183413473d241189f187817cf0d9f79201ca6 100644
--- a/test/app/function1.result
+++ b/test/app/function1.result
@@ -0,0 +1,2 @@
+ok - function1
+ok - test
diff --git a/test/app/function1.test.lua b/test/app/function1.test.lua
index f84ff151a4e5bda83b1701ce0c1a095adbb2e412..f63a02f9ae43c26ccd05db7170439295a649c5f0 100755
--- a/test/app/function1.test.lua
+++ b/test/app/function1.test.lua
@@ -15,8 +15,11 @@ net = require('net.box')
 
 box.schema.func.create('function1', {language = "C"})
 box.schema.user.grant('guest', 'execute', 'function', 'function1')
+box.schema.func.create('function1.test', {language = "C"})
+box.schema.user.grant('guest', 'execute', 'function', 'function1.test')
 
 c = net:new(os.getenv("LISTEN"))
 c:call('function1')
+c:call('function1.test')
 
 os.exit(0)
diff --git a/test/big/iterator.result b/test/big/iterator.result
index cc76c3a7df98a995e0cd2a5b658603f928f43998..6d6010a0a902f76c5c91b9a98fe4555b895b8479 100644
--- a/test/big/iterator.result
+++ b/test/big/iterator.result
@@ -891,7 +891,7 @@ space.index['primary']:pairs({}, {iterator = -666 })
 -- Test cases for #123: box.index.count does not check arguments properly
 space.index['primary']:pairs(function() end, { iterator = box.index.EQ })
 ---
-- error: 'builtin/msgpackffi.lua:261: can not encode Lua type: ''function'''
+- error: 'builtin/msgpackffi.lua:222: can not encode Lua type: ''function'''
 ...
 -- Check that iterators successfully invalidated when index deleted
 gen, param, state = space.index['i1']:pairs(nil, { iterator = box.index.GE })
diff --git a/test/big/lua.result b/test/big/lua.result
index 31b231e74366ae11caee99997d58216b3ac2425a..6bf25aeb88b3046b5d2eef726faaef23d6c40b21 100644
--- a/test/big/lua.result
+++ b/test/big/lua.result
@@ -574,7 +574,7 @@ space.index['i1']:count()
 -- Test cases for #123: box.index.count does not check arguments properly
 space.index['i1']:count(function() end)
 ---
-- error: 'builtin/msgpackffi.lua:261: can not encode Lua type: ''function'''
+- error: 'builtin/msgpackffi.lua:222: can not encode Lua type: ''function'''
 ...
 space:drop()
 ---
diff --git a/test/box/admin.result b/test/box/admin.result
index 126e00164fc920df5be100ae72c93989569280a5..18ae3194f84488a14a3866394d9d888acc5d6901 100644
--- a/test/box/admin.result
+++ b/test/box/admin.result
@@ -63,7 +63,7 @@ end;
 ...
 function test_box_info()
     local tmp = box.info()
-    local num = {'pid', 'snapshot_pid', 'uptime'}
+    local num = {'pid', 'uptime'}
     local str = {'version', 'status' }
     local failed = {}
     if check_type(tmp.server, 'table') == false then
diff --git a/test/box/admin.test.lua b/test/box/admin.test.lua
index 5744c28ade11321b5a83077e5cb899b5511d9c1e..22e7b76088d8989b60345834a87102b93c6e951e 100644
--- a/test/box/admin.test.lua
+++ b/test/box/admin.test.lua
@@ -19,7 +19,7 @@ end;
 
 function test_box_info()
     local tmp = box.info()
-    local num = {'pid', 'snapshot_pid', 'uptime'}
+    local num = {'pid', 'uptime'}
     local str = {'version', 'status' }
     local failed = {}
     if check_type(tmp.server, 'table') == false then
diff --git a/test/box/bsdsocket.result b/test/box/bsdsocket.result
index 4e0e3fa6e54888169147b56e2f7a3d5b072e06d2..7ff73d18ff01fabc0584c0507cecde68d955e5b7 100644
--- a/test/box/bsdsocket.result
+++ b/test/box/bsdsocket.result
@@ -25,6 +25,9 @@ errno = require 'errno'
 fio = require 'fio'
 ---
 ...
+ffi = require('ffi')
+---
+...
 type(socket)
 ---
 - table
@@ -70,7 +73,7 @@ s:close()
 s:close()
 
 ---
-- error: 'builtin/socket.lua:83: attempt to use closed socket'
+- error: 'builtin/socket.lua:87: attempt to use closed socket'
 ...
 LISTEN = require('uri').parse(box.cfg.listen)
 ---
@@ -134,14 +137,15 @@ s:wait(.01)
 ---
 - RW
 ...
-handshake = s:sysread(128)
+handshake = ffi.new('char[128]')
 ---
 ...
-string.len(handshake)
+-- test sysread with char *
+s:sysread(handshake, 128)
 ---
 - 128
 ...
-string.sub(handshake, 1, 9)
+ffi.string(handshake, 9)
 ---
 - Tarantool
 ...
@@ -151,7 +155,8 @@ ping = msgpack.encode({ [0] = 64, [1] = 0 }) .. msgpack.encode({})
 ping = msgpack.encode(string.len(ping)) .. ping
 ---
 ...
-s:syswrite(ping)
+-- test syswrite with char *
+s:syswrite(ffi.cast('const char *', ping), #ping)
 ---
 - 7
 ...
@@ -279,7 +284,7 @@ s:getsockopt('SOL_SOCKET', 'SO_DEBUG')
 ...
 s:setsockopt('SOL_SOCKET', 'SO_ACCEPTCONN', 1)
 ---
-- error: 'builtin/socket.lua:357: Socket option SO_ACCEPTCONN is read only'
+- error: 'builtin/socket.lua:393: Socket option SO_ACCEPTCONN is read only'
 ...
 s:getsockopt('SOL_SOCKET', 'SO_RCVBUF') > 32
 ---
@@ -482,14 +487,6 @@ sc:shutdown('W')
 ---
 - true
 ...
-sa:read(100, 1)
----
-- ' world'
-...
-sa:read(100, 1)
----
-- 
-...
 sa:close()
 ---
 - true
@@ -1028,7 +1025,7 @@ ch:get(1)
 ...
 s:error()
 ---
-- error: 'builtin/socket.lua:83: attempt to use closed socket'
+- error: 'builtin/socket.lua:87: attempt to use closed socket'
 ...
 -- random port
 port = 33123
@@ -1395,7 +1392,7 @@ end);
 client = socket.tcp_connect('unix/', path)
 ---
 ...
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 ---
 ...
 buf == "a 10\n"
@@ -1405,7 +1402,7 @@ buf == "a 10\n"
 remaining = remaining - #buf
 ---
 ...
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 ---
 ...
 buf == "b 15\n"
@@ -1415,7 +1412,7 @@ buf == "b 15\n"
 remaining = remaining - #buf
 ---
 ...
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 ---
 ...
 buf == "abc"
@@ -1429,14 +1426,14 @@ remaining == 0
 ---
 - true
 ...
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 ---
 ...
 buf == ""
 ---
 - true
 ...
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 ---
 ...
 buf == ""
diff --git a/test/box/bsdsocket.test.lua b/test/box/bsdsocket.test.lua
index e35748f619ff8034822e8f53a3990f5b4dcf4759..4e25afbe27606f87898809b2013a80ea5e6a32dd 100644
--- a/test/box/bsdsocket.test.lua
+++ b/test/box/bsdsocket.test.lua
@@ -7,6 +7,7 @@ msgpack = require 'msgpack'
 log = require 'log'
 errno = require 'errno'
 fio = require 'fio'
+ffi = require('ffi')
 type(socket)
 
 socket('PF_INET', 'SOCK_STREAM', 'tcp121222');
@@ -45,14 +46,16 @@ s:writable(.00000000000001)
 s:writable(0)
 s:wait(.01)
 
-handshake = s:sysread(128)
-string.len(handshake)
-string.sub(handshake, 1, 9)
+handshake = ffi.new('char[128]')
+-- test sysread with char *
+s:sysread(handshake, 128)
+ffi.string(handshake, 9)
 
 ping = msgpack.encode({ [0] = 64, [1] = 0 }) .. msgpack.encode({})
 ping = msgpack.encode(string.len(ping)) .. ping
 
-s:syswrite(ping)
+-- test syswrite with char *
+s:syswrite(ffi.cast('const char *', ping), #ping)
 s:readable(1)
 s:wait(.01)
 
@@ -114,7 +117,6 @@ sc:sysconnect('127.0.0.1', 3457) or errno() == errno.EINPROGRESS
 sc:writable(10)
 sc:write('Hello, world')
 
-
 sa, addr = s:accept()
 addr2 = sa:name()
 addr2.host == addr.host
@@ -150,8 +152,6 @@ sa:read('ine', 0.1)
 sc:send('Hello, world')
 sa:read(',', 1)
 sc:shutdown('W')
-sa:read(100, 1)
-sa:read(100, 1)
 sa:close()
 sc:close()
 
@@ -410,7 +410,6 @@ s:close()
 
 os.remove(path)
 
-
 server, addr = socket.tcp_server('unix/', path, function(s) s:write('Hello, world') end)
 type(addr)
 server ~= nil
@@ -475,19 +474,19 @@ server = socket.tcp_server('unix/', path, function(s)
 end);
 --# setopt delimiter ''
 client = socket.tcp_connect('unix/', path)
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 buf == "a 10\n"
 remaining = remaining - #buf
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 buf == "b 15\n"
 remaining = remaining - #buf
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 buf == "abc"
 remaining = remaining - #buf
 remaining == 0
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 buf == ""
-buf = client:read({ size = remaining, delimiter = "[\r\n]+"})
+buf = client:read({ size = remaining, delimiter = "\n"})
 buf == ""
 client:close()
 server:close()
diff --git a/test/box/errinj.result b/test/box/errinj.result
index 1a12647ca3a30871d8025094bc219a6c6b7d9948..9fe76826c1891094109f85be78542c9526653a5d 100644
--- a/test/box/errinj.result
+++ b/test/box/errinj.result
@@ -14,6 +14,8 @@ errinj.info()
 ---
 - ERRINJ_WAL_WRITE:
     state: false
+  ERRINJ_RELAY:
+    state: false
   ERRINJ_TESTING:
     state: false
   ERRINJ_WAL_ROTATE:
diff --git a/test/box/errinj_index.result b/test/box/errinj_index.result
index dc640cc3d8025199364585ded2b197a0b43c77c2..f3943dccac9fafc18e97b34e11a62221ef17ce3b 100644
--- a/test/box/errinj_index.result
+++ b/test/box/errinj_index.result
@@ -93,11 +93,11 @@ res
 ...
 for i = 501,2500 do s:insert{i, i} end
 ---
-- error: Failed to allocate 16384 bytes in MemtxTree for replace
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 s:delete{1}
 ---
-- [1, 1, 'test1']
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 res = {}
 ---
@@ -107,7 +107,8 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [2, 2, 'test2']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
   - [3, 3, 'test3']
   - [4, 4, 'test4']
   - [5, 5, 'test5']
@@ -125,29 +126,10 @@ for i = 501,510 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [501, 501]
-  - [502, 502]
-  - [503, 503]
-  - [504, 504]
-  - [505, 505]
-  - [506, 506]
-  - [507, 507]
-  - [508, 508]
-  - [509, 509]
-  - [510, 510]
-...
-res = {}
----
-...
-for i = 2001,2010 do table.insert(res, (s:get{i})) end
----
-...
-res
----
 - []
 ...
---count must be greater that 1000 but less than 2000
-function check_iter_and_size() local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end return count <= 1000 and "fail 1" or count >= 2000 and "fail 2" or "ok" end
+--count must be exactly 10
+function check_iter_and_size() local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end return count == 10 and "ok" or "fail" end
 ---
 ...
 check_iter_and_size()
@@ -156,11 +138,11 @@ check_iter_and_size()
 ...
 for i = 2501,3500 do s:insert{i, i} end
 ---
-- error: Failed to allocate 16384 bytes in MemtxTree for replace
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 s:delete{2}
 ---
-- [2, 2, 'test2']
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 check_iter_and_size()
 ---
@@ -174,7 +156,9 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [3, 3, 'test3']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
   - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
@@ -183,18 +167,6 @@ res
   - [9, 9, 'test9']
   - [10, 10, 'test10']
 ...
-for i = 3501,4500 do s:insert{i, i} end
----
-- error: Failed to allocate 16384 bytes in MemtxTree for replace
-...
-s:delete{3}
----
-- [3, 3, 'test3']
-...
-check_iter_and_size()
----
-- ok
-...
 errinj.set("ERRINJ_INDEX_ALLOC", false)
 ---
 - ok
@@ -210,7 +182,10 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [4, 4, 'test4']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
+  - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
   - [7, 7, 'test7']
@@ -230,7 +205,10 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [4, 4, 'test4']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
+  - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
   - [7, 7, 'test7']
@@ -352,11 +330,11 @@ res
 ...
 for i = 501,2500 do s:insert{i, i} end
 ---
-- error: Failed to allocate 10 bytes in hash_table for key
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 s:delete{1}
 ---
-- [1, 1, 'test1']
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 res = {}
 ---
@@ -366,7 +344,8 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [2, 2, 'test2']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
   - [3, 3, 'test3']
   - [4, 4, 'test4']
   - [5, 5, 'test5']
@@ -396,23 +375,19 @@ res
 ---
 - []
 ...
---since every insertion is rejected, count must be (10 - number of deletions)
-function check_iter_and_size(size_must_be) local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end print (count) return count ~= size_must_be and "fail 1" or "ok" end
----
-...
-check_iter_and_size(9)
+check_iter_and_size()
 ---
 - ok
 ...
 for i = 2501,3500 do s:insert{i, i} end
 ---
-- error: Failed to allocate 9 bytes in hash_table for key
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 s:delete{2}
 ---
-- [2, 2, 'test2']
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
-check_iter_and_size(8)
+check_iter_and_size()
 ---
 - ok
 ...
@@ -424,7 +399,9 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [3, 3, 'test3']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
   - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
@@ -435,13 +412,13 @@ res
 ...
 for i = 3501,4500 do s:insert{i, i} end
 ---
-- error: Failed to allocate 8 bytes in hash_table for key
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
 s:delete{3}
 ---
-- [3, 3, 'test3']
+- error: Failed to allocate 16384 bytes in mempool for new slab
 ...
-check_iter_and_size(7)
+check_iter_and_size()
 ---
 - ok
 ...
@@ -460,7 +437,10 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [4, 4, 'test4']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
+  - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
   - [7, 7, 'test7']
@@ -480,7 +460,10 @@ for i = 1,10 do table.insert(res, (s:get{i})) end
 ...
 res
 ---
-- - [4, 4, 'test4']
+- - [1, 1, 'test1']
+  - [2, 2, 'test2']
+  - [3, 3, 'test3']
+  - [4, 4, 'test4']
   - [5, 5, 'test5']
   - [6, 6, 'test6']
   - [7, 7, 'test7']
diff --git a/test/box/errinj_index.test.lua b/test/box/errinj_index.test.lua
index c725b490fe1f757a87c5e5ee222f684acd29b741..6068965ee2b81012e967031d2adf5d77e5a00eec 100644
--- a/test/box/errinj_index.test.lua
+++ b/test/box/errinj_index.test.lua
@@ -31,12 +31,9 @@ res
 res = {}
 for i = 501,510 do table.insert(res, (s:get{i})) end
 res
-res = {}
-for i = 2001,2010 do table.insert(res, (s:get{i})) end
-res
 
---count must be greater that 1000 but less than 2000
-function check_iter_and_size() local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end return count <= 1000 and "fail 1" or count >= 2000 and "fail 2" or "ok" end
+--count must be exactly 10
+function check_iter_and_size() local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end return count == 10 and "ok" or "fail" end
 check_iter_and_size()
 
 for i = 2501,3500 do s:insert{i, i} end
@@ -46,10 +43,6 @@ res = {}
 for i = 1,10 do table.insert(res, (s:get{i})) end
 res
 
-for i = 3501,4500 do s:insert{i, i} end
-s:delete{3}
-check_iter_and_size()
-
 errinj.set("ERRINJ_INDEX_ALLOC", false)
 
 for i = 4501,5500 do s:insert{i, i} end
@@ -102,20 +95,18 @@ res = {}
 for i = 2001,2010 do table.insert(res, (s:get{i})) end
 res
 
---since every insertion is rejected, count must be (10 - number of deletions)
-function check_iter_and_size(size_must_be) local count = 0 for _, t in s.index[0]:pairs() do count = count + 1 end print (count) return count ~= size_must_be and "fail 1" or "ok" end
-check_iter_and_size(9)
+check_iter_and_size()
 
 for i = 2501,3500 do s:insert{i, i} end
 s:delete{2}
-check_iter_and_size(8)
+check_iter_and_size()
 res = {}
 for i = 1,10 do table.insert(res, (s:get{i})) end
 res
 
 for i = 3501,4500 do s:insert{i, i} end
 s:delete{3}
-check_iter_and_size(7)
+check_iter_and_size()
 
 errinj.set("ERRINJ_INDEX_ALLOC", false)
 
diff --git a/test/box/info.result b/test/box/info.result
index c5d5c708c79fd9d89dbe4ce9d025430c4345a550..a37be2dc142a26b621ded268c2d5a0d7a0a26bd1 100644
--- a/test/box/info.result
+++ b/test/box/info.result
@@ -54,13 +54,8 @@ t
 - - pid
   - replication
   - server
-  - snapshot_pid
   - status
   - uptime
   - vclock
   - version
 ...
-box.info.snapshot_pid
----
-- 0
-...
diff --git a/test/box/info.test.lua b/test/box/info.test.lua
index 135461a0de5367c76207fc500fcbc4be405bcb73..e104a7103fd80552fdd91d7002b62d30eef5497b 100644
--- a/test/box/info.test.lua
+++ b/test/box/info.test.lua
@@ -14,4 +14,3 @@ t = {}
 for k, _ in pairs(box.info()) do table.insert(t, k) end
 table.sort(t)
 t
-box.info.snapshot_pid
diff --git a/test/box/misc.result b/test/box/misc.result
index f1189711d0fb2c428b15b7157c696557391ee042..23bd9464a229430e3239a6a39d1091818e96e1e2 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -94,9 +94,11 @@ box.error.raise()
 ...
 box.error.last()
 ---
-- type: ClientError
-  message: Illegal parameters, bla bla
+- line: -1
   code: 1
+  type: ClientError
+  message: Illegal parameters, bla bla
+  file: '[C]'
 ...
 box.error.clear()
 ---
diff --git a/test/box/select.result b/test/box/select.result
index cc4522a46fd83340722bd7bcee010c85d4c4c0b4..e621bcfb0ce102b73036a8fb5fc03c8ea9617b7b 100644
--- a/test/box/select.result
+++ b/test/box/select.result
@@ -1,3 +1,6 @@
+msgpack = require('msgpack')
+---
+...
 s = box.schema.space.create('select', { temporary = true })
 ---
 ...
@@ -8,65 +11,104 @@ index2 = s:create_index('second', { type = 'tree', unique = true,  parts = {2, '
 ---
 ...
 for i = 1, 20 do s:insert({ i, 1, 2, 3 }) end
+---
+...
+--# setopt delimiter ';'
+local function test_op(op, idx, ...)
+    local t1 = idx[op .. '_ffi'](idx, ...)
+    local t2 = idx[op .. '_luac'](idx, ...)
+    if msgpack.encode(t1) ~= msgpack.encode(t2) then
+        return 'different result from '..op..'_ffi and '..op..'_luac', t1, t2
+    end
+    return t1
+end
+test = setmetatable({}, {
+    __index = function(_, op) return function(...) return test_op(op, ...) end end
+})
+---
+...
+--# setopt delimiter ''
+local function test_op(op, idx, ...)
+    local t1 = idx[op .. '_ffi'](idx, ...)
+    local t2 = idx[op .. '_luac'](idx, ...)
+    if msgpack.encode(t1) ~= msgpack.encode(t2) then
+        return 'different result from '..op..'_ffi and '..op..'_luac', t1, t2
+    end
+    return t1
+end
+test = setmetatable({}, {
+    __index = function(_, op) return function(...) return test_op(op, ...) end end
+})
+
 ---
 ...
 --------------------------------------------------------------------------------
 -- get tests
 --------------------------------------------------------------------------------
-s.index[0]:get()
+s.index[0].get == s.index[0].get_ffi or s.index[0].get == s.index[0].get_luac
+---
+- true
+...
+test.get(s.index[0])
 ---
 - error: Invalid key part count in an exact match (expected 1, got 0)
 ...
-s.index[0]:get({})
+test.get(s.index[0], {})
 ---
 - error: Invalid key part count in an exact match (expected 1, got 0)
 ...
-s.index[0]:get(nil)
+test.get(s.index[0], nil)
 ---
 - error: Invalid key part count in an exact match (expected 1, got 0)
 ...
-s.index[0]:get(1)
+test.get(s.index[0], 1)
 ---
 - [1, 1, 2, 3]
 ...
-s.index[0]:get({1})
+test.get(s.index[0], {1})
 ---
 - [1, 1, 2, 3]
 ...
-s.index[0]:get({1, 2})
+test.get(s.index[0], {1, 2})
 ---
 - error: Invalid key part count in an exact match (expected 1, got 2)
 ...
-s.index[0]:get(0)
+test.get(s.index[0], 0)
 ---
+- null
 ...
-s.index[0]:get({0})
+test.get(s.index[0], {0})
 ---
+- null
 ...
-s.index[0]:get("0")
+test.get(s.index[0], "0")
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[0]:get({"0"})
+test.get(s.index[0], {"0"})
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[1]:get(1)
+test.get(s.index[1], 1)
 ---
 - error: Invalid key part count in an exact match (expected 2, got 1)
 ...
-s.index[1]:get({1})
+test.get(s.index[1], {1})
 ---
 - error: Invalid key part count in an exact match (expected 2, got 1)
 ...
-s.index[1]:get({1, 2})
+test.get(s.index[1], {1, 2})
 ---
 - [2, 1, 2, 3]
 ...
 --------------------------------------------------------------------------------
 -- select tests
 --------------------------------------------------------------------------------
-s.index[0]:select()
+s.index[0].select == s.index[0].select_ffi or s.index[0].select == s.index[0].select_luac
+---
+- true
+...
+test.select(s.index[0])
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -89,7 +131,7 @@ s.index[0]:select()
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({})
+test.select(s.index[0], {})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -112,7 +154,7 @@ s.index[0]:select({})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil)
+test.select(s.index[0], nil)
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -135,7 +177,7 @@ s.index[0]:select(nil)
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = 'ALL'})
+test.select(s.index[0], {}, {iterator = 'ALL'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -158,7 +200,7 @@ s.index[0]:select({}, {iterator = 'ALL'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.ALL })
+test.select(s.index[0], nil, {iterator = box.index.ALL })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -181,7 +223,7 @@ s.index[0]:select(nil, {iterator = box.index.ALL })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
+test.select(s.index[0], {}, {iterator = box.index.ALL, limit = 10})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -194,15 +236,15 @@ s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
   - [9, 1, 2, 3]
   - [10, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.ALL, limit = 0})
+test.select(s.index[0], nil, {iterator = box.index.ALL, limit = 0})
 ---
 - []
 ...
-s.index[0]:select({}, {iterator = 'ALL', limit = 1, offset = 15})
+test.select(s.index[0], {}, {iterator = 'ALL', limit = 1, offset = 15})
 ---
 - - [16, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
+test.select(s.index[0], nil, {iterator = 'ALL', limit = 20, offset = 15})
 ---
 - - [16, 1, 2, 3]
   - [17, 1, 2, 3]
@@ -210,7 +252,7 @@ s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = box.index.EQ})
+test.select(s.index[0], nil, {iterator = box.index.EQ})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -233,7 +275,7 @@ s.index[0]:select(nil, {iterator = box.index.EQ})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = 'EQ'})
+test.select(s.index[0], {}, {iterator = 'EQ'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -256,7 +298,7 @@ s.index[0]:select({}, {iterator = 'EQ'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'REQ'})
+test.select(s.index[0], nil, {iterator = 'REQ'})
 ---
 - - [20, 1, 2, 3]
   - [19, 1, 2, 3]
@@ -279,7 +321,7 @@ s.index[0]:select(nil, {iterator = 'REQ'})
   - [2, 1, 2, 3]
   - [1, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.REQ})
+test.select(s.index[0], {}, {iterator = box.index.REQ})
 ---
 - - [20, 1, 2, 3]
   - [19, 1, 2, 3]
@@ -302,45 +344,45 @@ s.index[0]:select({}, {iterator = box.index.REQ})
   - [2, 1, 2, 3]
   - [1, 1, 2, 3]
 ...
-s.index[0]:select(nil, {iterator = 'EQ', limit = 2, offset = 1})
+test.select(s.index[0], nil, {iterator = 'EQ', limit = 2, offset = 1})
 ---
 - - [2, 1, 2, 3]
   - [3, 1, 2, 3]
 ...
-s.index[0]:select({}, {iterator = box.index.REQ, limit = 2, offset = 1})
+test.select(s.index[0], {}, {iterator = box.index.REQ, limit = 2, offset = 1})
 ---
 - - [19, 1, 2, 3]
   - [18, 1, 2, 3]
 ...
-s.index[0]:select(1)
+test.select(s.index[0], 1)
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select({1})
+test.select(s.index[0], {1})
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select({1, 2})
+test.select(s.index[0], {1, 2})
 ---
 - error: Invalid key part count (expected [0..1], got 2)
 ...
-s.index[0]:select(0)
+test.select(s.index[0], 0)
 ---
 - []
 ...
-s.index[0]:select({0})
+test.select(s.index[0], {0})
 ---
 - []
 ...
-s.index[0]:select("0")
+test.select(s.index[0], "0")
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[0]:select({"0"})
+test.select(s.index[0], {"0"})
 ---
 - error: 'Supplied key type of part 0 does not match index part type: expected NUM'
 ...
-s.index[1]:select(1)
+test.select(s.index[1], 1)
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -363,7 +405,7 @@ s.index[1]:select(1)
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1})
+test.select(s.index[1], {1})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -386,12 +428,12 @@ s.index[1]:select({1})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1}, {limit = 2})
+test.select(s.index[1], {1}, {limit = 2})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
 ...
-s.index[1]:select(1, {iterator = 'EQ'})
+test.select(s.index[1], 1, {iterator = 'EQ'})
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -414,29 +456,29 @@ s.index[1]:select(1, {iterator = 'EQ'})
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[1]:select({1}, {iterator = box.index.EQ, offset = 16, limit = 2})
+test.select(s.index[1], {1}, {iterator = box.index.EQ, offset = 16, limit = 2})
 ---
 - - [17, 1, 2, 3]
   - [18, 1, 2, 3]
 ...
-s.index[1]:select({1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
+test.select(s.index[1], {1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
 ---
 - - [4, 1, 2, 3]
   - [3, 1, 2, 3]
 ...
-s.index[1]:select({1, 2}, {iterator = 'EQ'})
+test.select(s.index[1], {1, 2}, {iterator = 'EQ'})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[1]:select({1, 2}, {iterator = box.index.REQ})
+test.select(s.index[1], {1, 2}, {iterator = box.index.REQ})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[1]:select({1, 2})
+test.select(s.index[1], {1, 2})
 ---
 - - [2, 1, 2, 3]
 ...
-s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test.select(s.index[0], nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -459,7 +501,7 @@ s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test.select(s.index[0], {}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -482,19 +524,19 @@ s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(1)
+test.select(s.index[0], 1)
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = box.index.EQ })
+test.select(s.index[0], 1, { iterator = box.index.EQ })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'EQ' })
+test.select(s.index[0], 1, { iterator = 'EQ' })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE' })
+test.select(s.index[0], 1, { iterator = 'GE' })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
@@ -517,16 +559,16 @@ s.index[0]:select(1, { iterator = 'GE' })
   - [19, 1, 2, 3]
   - [20, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE', limit = 2 })
+test.select(s.index[0], 1, { iterator = 'GE', limit = 2 })
 ---
 - - [1, 1, 2, 3]
   - [2, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'LE', limit = 2 })
+test.select(s.index[0], 1, { iterator = 'LE', limit = 2 })
 ---
 - - [1, 1, 2, 3]
 ...
-s.index[0]:select(1, { iterator = 'GE', offset = 10, limit = 2 })
+test.select(s.index[0], 1, { iterator = 'GE', offset = 10, limit = 2 })
 ---
 - - [11, 1, 2, 3]
   - [12, 1, 2, 3]
@@ -535,6 +577,43 @@ s:select(2)
 ---
 - - [2, 1, 2, 3]
 ...
+--------------------------------------------------------------------------------
+-- min/max tests
+--------------------------------------------------------------------------------
+test.min(s.index[1])
+---
+- [1, 1, 2, 3]
+...
+test.max(s.index[1])
+---
+- [20, 1, 2, 3]
+...
+--------------------------------------------------------------------------------
+-- count tests
+--------------------------------------------------------------------------------
+test.count(s.index[1])
+---
+- 20
+...
+test.count(s.index[0], nil)
+---
+- 20
+...
+test.count(s.index[0], {})
+---
+- 20
+...
+test.count(s.index[0], 10, { iterator = 'GT'})
+---
+- 10
+...
+--------------------------------------------------------------------------------
+-- random tests
+--------------------------------------------------------------------------------
+test.random(s.index[0], 48)
+---
+- [9, 1, 2, 3]
+...
 s:drop()
 ---
 ...
diff --git a/test/box/select.test.lua b/test/box/select.test.lua
index 2ac00390ff93c1d046dba40c749b81a9d48af898..d36d866f59d121f9ede0b8f4636a5b98877acbcb 100644
--- a/test/box/select.test.lua
+++ b/test/box/select.test.lua
@@ -1,80 +1,124 @@
+msgpack = require('msgpack')
+
 s = box.schema.space.create('select', { temporary = true })
 index1 = s:create_index('primary', { type = 'tree' })
 index2 = s:create_index('second', { type = 'tree', unique = true,  parts = {2, 'num', 1, 'num'}})
 for i = 1, 20 do s:insert({ i, 1, 2, 3 }) end
 
+--# setopt delimiter ';'
+local function test_op(op, idx, ...)
+    local t1 = idx[op .. '_ffi'](idx, ...)
+    local t2 = idx[op .. '_luac'](idx, ...)
+    if msgpack.encode(t1) ~= msgpack.encode(t2) then
+        return 'different result from '..op..'_ffi and '..op..'_luac', t1, t2
+    end
+    return t1
+end
+test = setmetatable({}, {
+    __index = function(_, op) return function(...) return test_op(op, ...) end end
+})
+--# setopt delimiter ''
+
+
 --------------------------------------------------------------------------------
 -- get tests
 --------------------------------------------------------------------------------
 
-s.index[0]:get()
-s.index[0]:get({})
-s.index[0]:get(nil)
-s.index[0]:get(1)
-s.index[0]:get({1})
-s.index[0]:get({1, 2})
-s.index[0]:get(0)
-s.index[0]:get({0})
-s.index[0]:get("0")
-s.index[0]:get({"0"})
-
-s.index[1]:get(1)
-s.index[1]:get({1})
-s.index[1]:get({1, 2})
+s.index[0].get == s.index[0].get_ffi or s.index[0].get == s.index[0].get_luac
+
+test.get(s.index[0])
+test.get(s.index[0], {})
+test.get(s.index[0], nil)
+test.get(s.index[0], 1)
+test.get(s.index[0], {1})
+test.get(s.index[0], {1, 2})
+test.get(s.index[0], 0)
+test.get(s.index[0], {0})
+test.get(s.index[0], "0")
+test.get(s.index[0], {"0"})
+
+test.get(s.index[1], 1)
+test.get(s.index[1], {1})
+test.get(s.index[1], {1, 2})
 
 --------------------------------------------------------------------------------
 -- select tests
 --------------------------------------------------------------------------------
 
-s.index[0]:select()
-s.index[0]:select({})
-s.index[0]:select(nil)
-s.index[0]:select({}, {iterator = 'ALL'})
-s.index[0]:select(nil, {iterator = box.index.ALL })
-s.index[0]:select({}, {iterator = box.index.ALL, limit = 10})
-s.index[0]:select(nil, {iterator = box.index.ALL, limit = 0})
-s.index[0]:select({}, {iterator = 'ALL', limit = 1, offset = 15})
-s.index[0]:select(nil, {iterator = 'ALL', limit = 20, offset = 15})
-
-s.index[0]:select(nil, {iterator = box.index.EQ})
-s.index[0]:select({}, {iterator = 'EQ'})
-s.index[0]:select(nil, {iterator = 'REQ'})
-s.index[0]:select({}, {iterator = box.index.REQ})
-
-s.index[0]:select(nil, {iterator = 'EQ', limit = 2, offset = 1})
-s.index[0]:select({}, {iterator = box.index.REQ, limit = 2, offset = 1})
-
-s.index[0]:select(1)
-s.index[0]:select({1})
-s.index[0]:select({1, 2})
-s.index[0]:select(0)
-s.index[0]:select({0})
-s.index[0]:select("0")
-s.index[0]:select({"0"})
-
-s.index[1]:select(1)
-s.index[1]:select({1})
-s.index[1]:select({1}, {limit = 2})
-s.index[1]:select(1, {iterator = 'EQ'})
-s.index[1]:select({1}, {iterator = box.index.EQ, offset = 16, limit = 2})
-s.index[1]:select({1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
-s.index[1]:select({1, 2}, {iterator = 'EQ'})
-s.index[1]:select({1, 2}, {iterator = box.index.REQ})
-s.index[1]:select({1, 2})
-
-s.index[0]:select(nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
-s.index[0]:select({}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
-
-s.index[0]:select(1)
-s.index[0]:select(1, { iterator = box.index.EQ })
-s.index[0]:select(1, { iterator = 'EQ' })
-s.index[0]:select(1, { iterator = 'GE' })
-s.index[0]:select(1, { iterator = 'GE', limit = 2 })
-s.index[0]:select(1, { iterator = 'LE', limit = 2 })
-s.index[0]:select(1, { iterator = 'GE', offset = 10, limit = 2 })
+s.index[0].select == s.index[0].select_ffi or s.index[0].select == s.index[0].select_luac
+
+test.select(s.index[0])
+test.select(s.index[0], {})
+test.select(s.index[0], nil)
+test.select(s.index[0], {}, {iterator = 'ALL'})
+
+test.select(s.index[0], nil, {iterator = box.index.ALL })
+test.select(s.index[0], {}, {iterator = box.index.ALL, limit = 10})
+test.select(s.index[0], nil, {iterator = box.index.ALL, limit = 0})
+test.select(s.index[0], {}, {iterator = 'ALL', limit = 1, offset = 15})
+test.select(s.index[0], nil, {iterator = 'ALL', limit = 20, offset = 15})
+
+test.select(s.index[0], nil, {iterator = box.index.EQ})
+test.select(s.index[0], {}, {iterator = 'EQ'})
+test.select(s.index[0], nil, {iterator = 'REQ'})
+test.select(s.index[0], {}, {iterator = box.index.REQ})
+
+test.select(s.index[0], nil, {iterator = 'EQ', limit = 2, offset = 1})
+test.select(s.index[0], {}, {iterator = box.index.REQ, limit = 2, offset = 1})
+
+test.select(s.index[0], 1)
+test.select(s.index[0], {1})
+test.select(s.index[0], {1, 2})
+test.select(s.index[0], 0)
+test.select(s.index[0], {0})
+test.select(s.index[0], "0")
+test.select(s.index[0], {"0"})
+
+test.select(s.index[1], 1)
+test.select(s.index[1], {1})
+test.select(s.index[1], {1}, {limit = 2})
+test.select(s.index[1], 1, {iterator = 'EQ'})
+test.select(s.index[1], {1}, {iterator = box.index.EQ, offset = 16, limit = 2})
+test.select(s.index[1], {1}, {iterator = box.index.REQ, offset = 16, limit = 2 })
+test.select(s.index[1], {1, 2}, {iterator = 'EQ'})
+test.select(s.index[1], {1, 2}, {iterator = box.index.REQ})
+test.select(s.index[1], {1, 2})
+
+test.select(s.index[0], nil, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+test.select(s.index[0], {}, { iterator = 'ALL', offset = 0, limit = 4294967295 })
+
+test.select(s.index[0], 1)
+test.select(s.index[0], 1, { iterator = box.index.EQ })
+test.select(s.index[0], 1, { iterator = 'EQ' })
+test.select(s.index[0], 1, { iterator = 'GE' })
+test.select(s.index[0], 1, { iterator = 'GE', limit = 2 })
+test.select(s.index[0], 1, { iterator = 'LE', limit = 2 })
+test.select(s.index[0], 1, { iterator = 'GE', offset = 10, limit = 2 })
 
 s:select(2)
 
+--------------------------------------------------------------------------------
+-- min/max tests
+--------------------------------------------------------------------------------
+
+test.min(s.index[1])
+test.max(s.index[1])
+
+--------------------------------------------------------------------------------
+-- count tests
+--------------------------------------------------------------------------------
+
+test.count(s.index[1])
+test.count(s.index[0], nil)
+test.count(s.index[0], {})
+test.count(s.index[0], 10, { iterator = 'GT'})
+
+--------------------------------------------------------------------------------
+-- random tests
+--------------------------------------------------------------------------------
+
+test.random(s.index[0], 48)
+
 s:drop()
 
 s = box.schema.space.create('select', { temporary = true })
diff --git a/test/replication/catch.result b/test/replication/catch.result
new file mode 100644
index 0000000000000000000000000000000000000000..76b5554dac3fc9f03a652ba2dd17f13d14693337
--- /dev/null
+++ b/test/replication/catch.result
@@ -0,0 +1,89 @@
+net_box = require('net.box')
+---
+...
+errinj = box.error.injection
+---
+...
+box.schema.user.grant('guest', 'replication')
+---
+...
+--# create server replica with rpl_master=default, script='replication/replica.lua'
+--# start server replica
+--# set connection replica
+box.schema.user.grant('guest', 'read,write,execute', 'universe')
+---
+...
+--# set connection default
+s = box.schema.space.create('test');
+---
+...
+index = s:create_index('primary', {type = 'hash'})
+---
+...
+--# set connection replica
+fiber = require('fiber')
+---
+...
+while box.space.test == nil do fiber.sleep(0.01) end
+---
+...
+--# set connection default
+--# stop server replica
+-- insert values on the master while replica os stopped and can't fetch them
+for i=1,100 do s:insert{i, 'this is test message12345'} end
+---
+...
+-- sleep after every tuple
+errinj.set("ERRINJ_RELAY", true)
+---
+- ok
+...
+--# start server replica
+--# set connection replica
+-- Check that replica doesn't enter read-write mode
+-- before catching up with the master: to check that we inject
+-- sleep into the master relay_send function and attempt a data
+-- modifying statement in replica while it's still fetching
+-- data from the master.
+-- In next 2 cases we try to delete tuple
+-- during fetching process(local delete, remote delete)
+-- case #1: delete tuple in replica
+box.space.test:len()
+---
+- 1
+...
+d = box.space.test:delete{1}
+---
+...
+box.space.test:get(1) ~= nil
+---
+- false
+...
+-- case #2: delete tuple by net.box
+--# set connection default
+--# set variable r_uri to 'replica.listen'
+c = net_box:new(r_uri)
+---
+...
+d = c.space.test:delete{1}
+---
+...
+c.space.test:get(1) ~= nil
+---
+- false
+...
+-- check sync
+errinj.set("ERRINJ_RELAY", false)
+---
+- ok
+...
+-- cleanup
+--# stop server replica
+--# cleanup server replica
+--# set connection default
+box.space.test:drop()
+---
+...
+box.schema.user.revoke('guest', 'replication')
+---
+...
diff --git a/test/replication/catch.test.lua b/test/replication/catch.test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..5ed3e89d712e96f14baf7ab6d57f20d14dd20273
--- /dev/null
+++ b/test/replication/catch.test.lua
@@ -0,0 +1,57 @@
+net_box = require('net.box')
+errinj = box.error.injection
+
+box.schema.user.grant('guest', 'replication')
+--# create server replica with rpl_master=default, script='replication/replica.lua'
+--# start server replica
+--# set connection replica
+box.schema.user.grant('guest', 'read,write,execute', 'universe')
+
+--# set connection default
+s = box.schema.space.create('test');
+index = s:create_index('primary', {type = 'hash'})
+
+--# set connection replica
+fiber = require('fiber')
+while box.space.test == nil do fiber.sleep(0.01) end
+--# set connection default
+--# stop server replica
+
+-- insert values on the master while replica os stopped and can't fetch them
+for i=1,100 do s:insert{i, 'this is test message12345'} end
+
+-- sleep after every tuple
+errinj.set("ERRINJ_RELAY", true)
+
+--# start server replica
+--# set connection replica
+
+-- Check that replica doesn't enter read-write mode
+-- before catching up with the master: to check that we inject
+-- sleep into the master relay_send function and attempt a data
+-- modifying statement in replica while it's still fetching
+-- data from the master.
+-- In next 2 cases we try to delete tuple
+-- during fetching process(local delete, remote delete)
+-- case #1: delete tuple in replica
+box.space.test:len()
+d = box.space.test:delete{1}
+box.space.test:get(1) ~= nil
+
+-- case #2: delete tuple by net.box
+--# set connection default
+--# set variable r_uri to 'replica.listen'
+c = net_box:new(r_uri)
+d = c.space.test:delete{1}
+c.space.test:get(1) ~= nil
+
+-- check sync
+errinj.set("ERRINJ_RELAY", false)
+
+-- cleanup
+--# stop server replica
+--# cleanup server replica
+--# set connection default
+box.space.test:drop()
+box.schema.user.revoke('guest', 'replication')
+
diff --git a/test/replication/cluster.result b/test/replication/cluster.result
index 8268cb49c928a56530282207372c01dd15a0056c..5200f3f9e154a11d382f51c21a894e478b56029d 100644
--- a/test/replication/cluster.result
+++ b/test/replication/cluster.result
@@ -114,7 +114,7 @@ box.info.vclock[2]
 ...
 box.space._schema:replace{"test", 48}
 ---
-- error: Can't modify data because this server in read-only mode.
+- error: Can't modify data because this server is in read-only mode.
 ...
 box.space._cluster:insert{10, "<replica uuid>"}
 ---
diff --git a/test/replication/readonly.result b/test/replication/readonly.result
index dd38c37d9d0d642d8bc6f6e8389af98152c91a90..a7848717d0a7bcc1fcea98adc0807cef4fa23889 100644
--- a/test/replication/readonly.result
+++ b/test/replication/readonly.result
@@ -33,7 +33,7 @@ box.info.server.lsn
 ...
 space = box.schema.space.create("ro")
 ---
-- error: Can't modify data because this server in read-only mode.
+- error: Can't modify data because this server is in read-only mode.
 ...
 box.info.vclock[2]
 ---
diff --git a/test/replication/suite.ini b/test/replication/suite.ini
index c0a179cce971a84ef394aa6bb06c3a4b3403a547..1841d9fb39fe062c9f022ce0e51252673293b256 100644
--- a/test/replication/suite.ini
+++ b/test/replication/suite.ini
@@ -3,3 +3,4 @@ core = tarantool
 script =  master.lua
 description = tarantool/box, replication
 disabled = consistent.test.lua status.test.py
+release_disabled = catch.test.lua
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 485c94971ae3735ff25282a7cc619063165f66fd..6136e80c216479bc05f4ef55915e072352ebb306 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -30,6 +30,10 @@ add_executable(slab_cache.test slab_cache.c)
 target_link_libraries(slab_cache.test small)
 add_executable(region.test region.c)
 target_link_libraries(region.test small)
+add_executable(ibuf.test ibuf.c)
+target_link_libraries(ibuf.test small)
+add_executable(obuf.test obuf.c)
+target_link_libraries(obuf.test small)
 add_executable(mempool.test mempool.c)
 target_link_libraries(mempool.test small)
 add_executable(small_alloc.test small_alloc.c)
@@ -44,9 +48,9 @@ target_link_libraries(bps_tree.test small)
 add_executable(bps_tree_itr.test bps_tree_itr.cc ${CMAKE_SOURCE_DIR}/third_party/qsort_arg.c)
 target_link_libraries(bps_tree_itr.test small)
 add_executable(rtree.test rtree.cc ${CMAKE_SOURCE_DIR}/src/lib/salad/rtree.c)
-target_link_libraries(rtree.test)
+target_link_libraries(rtree.test small)
 add_executable(rtree_itr.test rtree_itr.cc ${CMAKE_SOURCE_DIR}/src/lib/salad/rtree.c)
-target_link_libraries(rtree_itr.test)
+target_link_libraries(rtree_itr.test small)
 add_executable(matras.test matras.cc)
 target_link_libraries(matras.test small)
 add_executable(light.test light.cc)
@@ -103,3 +107,8 @@ target_link_libraries(guava.test salad)
 add_executable(find_path.test find_path.c
     ${CMAKE_SOURCE_DIR}/src/find_path.c
 )
+
+add_executable(reflection_c.test reflection_c.c unit.c
+    ${CMAKE_SOURCE_DIR}/src/reflection.c)
+add_executable(reflection_cxx.test reflection_cxx.cc unit.c
+    ${CMAKE_SOURCE_DIR}/src/reflection.c)
diff --git a/test/unit/fiber.cc b/test/unit/fiber.cc
index 866280a3798482c30fc74867ec5d00a4e22cdbe4..76d3e4c8e4c6d3ab11ea241c56d717d17aeb4eb0 100644
--- a/test/unit/fiber.cc
+++ b/test/unit/fiber.cc
@@ -89,7 +89,7 @@ fiber_join_test()
 	fiber_yield();
 	note("by this time the fiber should be dead already");
 	fiber_cancel(fiber);
-	Exception::clear(&fiber->exception);
+	diag_clear(&fiber->diag);
 	fiber_join(fiber);
 
 	footer();
diff --git a/test/unit/ibuf.c b/test/unit/ibuf.c
new file mode 100644
index 0000000000000000000000000000000000000000..46bb04e46f06a4f5030922a649722c2ff9c97e5d
--- /dev/null
+++ b/test/unit/ibuf.c
@@ -0,0 +1,50 @@
+#include "small/quota.h"
+#include "small/ibuf.h"
+#include "small/slab_cache.h"
+#include "unit.h"
+#include <stdio.h>
+
+struct slab_cache cache;
+struct slab_arena arena;
+struct quota quota;
+
+void
+ibuf_basic()
+{
+	header();
+
+	struct ibuf ibuf;
+
+	ibuf_create(&ibuf, &cache, 16320);
+
+	fail_unless(ibuf_used(&ibuf) == 0);
+
+	void *ptr = ibuf_alloc_nothrow(&ibuf, 10);
+
+	fail_unless(ptr);
+
+	fail_unless(ibuf_used(&ibuf) == 10);
+
+	ptr = ibuf_alloc_nothrow(&ibuf, 1000000);
+	fail_unless(ptr);
+
+	fail_unless(ibuf_used(&ibuf) == 1000010);
+
+	ibuf_reset(&ibuf);
+
+	fail_unless(ibuf_used(&ibuf) == 0);
+
+	footer();
+}
+
+int main()
+{
+	quota_init(&quota, UINT_MAX);
+	slab_arena_create(&arena, &quota, 0,
+			  4000000, MAP_PRIVATE);
+	slab_cache_create(&cache, &arena);
+
+	ibuf_basic();
+
+	slab_cache_destroy(&cache);
+}
diff --git a/test/unit/ibuf.result b/test/unit/ibuf.result
new file mode 100644
index 0000000000000000000000000000000000000000..f52d68ddee1abd997ec8b6f82a7133b980475786
--- /dev/null
+++ b/test/unit/ibuf.result
@@ -0,0 +1,3 @@
+	*** ibuf_basic ***
+	*** ibuf_basic: done ***
+ 
\ No newline at end of file
diff --git a/test/unit/obuf.c b/test/unit/obuf.c
new file mode 100644
index 0000000000000000000000000000000000000000..9b091569db495f8852936adb2b4f331a6d4c5938
--- /dev/null
+++ b/test/unit/obuf.c
@@ -0,0 +1,82 @@
+#include "small/quota.h"
+#include "small/obuf.h"
+#include "small/slab_cache.h"
+#include "unit.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <time.h>
+
+enum {
+	OBJSIZE_MIN = sizeof(int),
+	OBJSIZE_MAX = 5000,
+	OBJECTS_MAX = 1000,
+	OSCILLATION_MAX = 1024,
+	ITERATIONS_MAX = 5000,
+};
+
+/** Keep global to easily inspect the core. */
+long seed;
+
+void
+alloc_checked(struct obuf *buf)
+{
+	int pos = rand() % OBJECTS_MAX;
+	int size = rand() % OBJSIZE_MAX;
+	if (size < OBJSIZE_MIN || size > OBJSIZE_MAX)
+		size = OBJSIZE_MIN;
+
+	obuf_alloc_nothrow(buf, size);
+}
+
+static void
+basic_alloc_streak(struct obuf *buf)
+{
+	int oscillation = rand() % OSCILLATION_MAX;
+	int i;
+
+	for (i = 0; i < oscillation; ++i)
+		alloc_checked(buf);
+}
+
+void
+obuf_basic(struct slab_cache *slabc)
+{
+	int i;
+	header();
+
+	struct obuf buf;
+	obuf_create(&buf, slabc, 16320);
+
+	for (i = 0; i < ITERATIONS_MAX; i++) {
+		basic_alloc_streak(&buf);
+		obuf_reset(&buf);
+		fail_unless(obuf_used(&buf) == 0);
+	}
+	obuf_destroy(&buf);
+	fail_unless(slab_cache_used(slabc) == 0);
+	slab_cache_check(slabc);
+
+	footer();
+}
+
+int main()
+{
+	struct slab_cache cache;
+	struct slab_arena arena;
+	struct quota quota;
+
+	seed = time(0);
+
+	srand(seed);
+
+	quota_init(&quota, UINT_MAX);
+
+	slab_arena_create(&arena, &quota, 0, 4000000,
+			  MAP_PRIVATE);
+	slab_cache_create(&cache, &arena);
+
+	obuf_basic(&cache);
+
+	slab_cache_destroy(&cache);
+}
+
diff --git a/test/unit/obuf.result b/test/unit/obuf.result
new file mode 100644
index 0000000000000000000000000000000000000000..6bb57e2b1ac9a5e65330b83a5a9e675d830ceca9
--- /dev/null
+++ b/test/unit/obuf.result
@@ -0,0 +1,3 @@
+	*** obuf_basic ***
+	*** obuf_basic: done ***
+ 
\ No newline at end of file
diff --git a/test/unit/reflection_c.c b/test/unit/reflection_c.c
new file mode 100644
index 0000000000000000000000000000000000000000..e35258a6836ddea8432a551a957a02c66cbd063e
--- /dev/null
+++ b/test/unit/reflection_c.c
@@ -0,0 +1,33 @@
+#include "reflection.h"
+
+#include "unit.h"
+
+static struct type type_Object = {
+	.parent = NULL,
+	.name = "Object",
+	.methods = NULL,
+};
+static struct type type_Database = {
+	.parent = &type_Object,
+	.name = "Database",
+	.methods = NULL,
+};
+static struct type type_Tarantool = {
+	.parent = &type_Database,
+	.name = "Tarantool",
+	.methods = NULL
+};
+
+int
+main()
+{
+	plan(4);
+
+	/* inheritance */
+	ok(type_assignable(&type_Object, &type_Tarantool), "assignable");
+	ok(type_assignable(&type_Database, &type_Tarantool), "assignable");
+	ok(type_assignable(&type_Tarantool, &type_Tarantool), "assignable");
+	ok(!type_assignable(&type_Tarantool, &type_Database), "assignable");
+
+	return check_plan();
+}
diff --git a/test/unit/reflection_c.result b/test/unit/reflection_c.result
new file mode 100644
index 0000000000000000000000000000000000000000..294fa97995a666faff2f7fd898eb89d52a2a4cf7
--- /dev/null
+++ b/test/unit/reflection_c.result
@@ -0,0 +1,5 @@
+1..4
+ok 1 - assignable
+ok 2 - assignable
+ok 3 - assignable
+ok 4 - assignable
diff --git a/test/unit/reflection_cxx.cc b/test/unit/reflection_cxx.cc
new file mode 100644
index 0000000000000000000000000000000000000000..6369fd0b300094781d9921f0bcf6b58dacae042a
--- /dev/null
+++ b/test/unit/reflection_cxx.cc
@@ -0,0 +1,178 @@
+#include "unit.h"
+
+#include <string.h>
+#include "reflection.h"
+
+extern const struct type type_Object;
+struct Object {
+	Object()
+		: type(&type_Object)
+	{}
+
+	virtual ~Object()
+	{}
+
+	const struct type *type;
+	Object(const struct type *type_arg)
+		: type(type_arg)
+	{}
+};
+const struct type type_Object = make_type("Object", NULL);
+
+extern const struct type type_Database;
+struct Database: public Object {
+	Database()
+		: Object(&type_Database)
+	{}
+
+	virtual const char *
+	getString() const
+	{
+		return m_str;
+	}
+
+	virtual void
+	putString(const char *str)
+	{
+		snprintf(m_str, sizeof(m_str), "%s", str);
+	}
+
+	virtual int
+	getInt() const
+	{
+		return m_int;
+	}
+
+	virtual void
+	putInt(int val) {
+		m_int = val;
+	}
+protected:
+	Database(const struct type *type)
+		: Object(type)
+	{}
+	int m_int;
+	char m_str[128];
+};
+static const struct method database_methods[] = {
+	make_method(&type_Database, "getString", &Database::getString),
+	make_method(&type_Database, "getInt", &Database::getInt),
+	make_method(&type_Database, "putString", &Database::putString),
+	make_method(&type_Database, "putInt", &Database::putInt),
+	METHODS_SENTINEL
+};
+const struct type type_Database = make_type("Database", &type_Object,
+	database_methods);
+
+extern const struct type type_Tarantool;
+struct Tarantool: public Database {
+	Tarantool()
+		: Database(&type_Tarantool)
+	{}
+
+	void inc() {
+		++m_int;
+	}
+};
+static const struct method tarantool_methods[] = {
+	make_method(&type_Tarantool, "inc", &Tarantool::inc),
+	METHODS_SENTINEL
+};
+const struct type type_Tarantool = make_type("Tarantool", &type_Database,
+	tarantool_methods);
+
+int
+main()
+{
+	plan(30);
+
+	Object obj;
+	Tarantool tntobj;
+	const struct method *get_string = type_method_by_name(tntobj.type,
+		"getString");
+	const struct method *put_string = type_method_by_name(tntobj.type,
+		"putString");
+	const struct method *get_int = type_method_by_name(tntobj.type,
+		"getInt");
+	const struct method *put_int = type_method_by_name(tntobj.type,
+		"putInt");
+	const struct method *inc = type_method_by_name(tntobj.type,
+		"inc");
+
+	/* struct type members */
+	ok(strcmp(type_Object.name, "Object") == 0, "type.name");
+	is(type_Object.parent, NULL, "type.parent");
+	is(type_Database.parent, &type_Object, "type.parent");
+
+	/* inheritance */
+	ok(type_assignable(&type_Object, &type_Tarantool), "is_instance");
+	ok(type_assignable(&type_Database, &type_Tarantool), "is_instance");
+	ok(type_assignable(&type_Tarantool, &type_Tarantool), "is_instance");
+	ok(!type_assignable(&type_Tarantool, &type_Database), "is_instance");
+
+	/* methods */
+	const char *methods_order[] = {
+		"inc",
+		"getString",
+		"getInt",
+		"putString",
+		"putInt"
+	};
+	int i = 0;
+	type_foreach_method(&type_Tarantool, method) {
+		ok(strcmp(method->name, methods_order[i]) == 0, "methods order");
+		++i;
+	}
+
+
+	/*
+	 * struct method members
+	 */
+	is(get_string->owner, &type_Database, "method.owner");
+	ok(strcmp(get_string->name, "getString") == 0, "method.name");
+	is(get_string->rtype, CTYPE_CONST_CHAR_PTR, "method.rtype (non void)");
+	is(put_string->rtype, CTYPE_VOID, "method.rtype (void)");
+	is(get_string->nargs, 0, "method.nargs (zero)");
+	is(put_string->nargs, 1, "method.nargs (non-zero)");
+	is(put_string->atype[0], CTYPE_CONST_CHAR_PTR, "method.atype");
+	is(get_string->isconst, true, "method.isconst");
+	is(put_string->isconst, false, "!method.isconst");
+
+	/*
+	 * Invokable
+	 */
+	ok(!method_invokable<int>(get_string, &tntobj),
+		"!invokable<invalid args>");
+	ok(!(method_invokable<const char *, int> (get_string, &tntobj)),
+		"!invokable<extra args>");
+	ok(!method_invokable<int>(get_string, &obj),
+		"!invokable<>(invalid object)");
+	ok(method_invokable<const char *>(get_string, &tntobj),
+		"invokable<const char *>");
+	ok((method_invokable<void, const char *>(put_string, &tntobj)),
+		"invokable<void, const char *>");
+
+	/*
+	 * Invoke
+	 */
+
+	/* int */
+	method_invoke<void, int>(put_int, &tntobj, 48);
+	int iret = method_invoke<int>(get_int, &tntobj);
+	is(iret, 48, "invoke (int)");
+
+	/* const char */
+	method_invoke<void, const char *>(put_string, &tntobj, "test string");
+	const char *sret = method_invoke<const char *>(get_string, &tntobj);
+	ok(strcmp(sret, "test string") == 0, "invoke (const char *)");
+
+	method_invoke<void>(inc, &tntobj);
+	iret = method_invoke<int>(get_int, &tntobj);
+	is(iret, 49, "invoke (void)");
+
+	const Tarantool *tntconstptr = &tntobj;
+	ok((!method_invokable<void, const char *>(put_string, tntconstptr)),
+		"!invokable<>() on const method with non-const object");
+
+	return check_plan();
+}
diff --git a/test/unit/reflection_cxx.result b/test/unit/reflection_cxx.result
new file mode 100644
index 0000000000000000000000000000000000000000..0f4f51eb44dd8fbf89c7ad74706126bdf0570bfd
--- /dev/null
+++ b/test/unit/reflection_cxx.result
@@ -0,0 +1,31 @@
+1..30
+ok 1 - type.name
+ok 2 - type.parent
+ok 3 - type.parent
+ok 4 - is_instance
+ok 5 - is_instance
+ok 6 - is_instance
+ok 7 - is_instance
+ok 8 - methods order
+ok 9 - methods order
+ok 10 - methods order
+ok 11 - methods order
+ok 12 - methods order
+ok 13 - method.owner
+ok 14 - method.name
+ok 15 - method.rtype (non void)
+ok 16 - method.rtype (void)
+ok 17 - method.nargs (zero)
+ok 18 - method.nargs (non-zero)
+ok 19 - method.atype
+ok 20 - method.isconst
+ok 21 - !method.isconst
+ok 22 - !invokable<invalid args>
+ok 23 - !invokable<extra args>
+ok 24 - !invokable<>(invalid object)
+ok 25 - invokable<const char *>
+ok 26 - invokable<void, const char *>
+ok 27 - invoke (int)
+ok 28 - invoke (const char *)
+ok 29 - invoke (void)
+ok 30 - !invokable<>() on const method with non-const object
diff --git a/test/unit/region.c b/test/unit/region.c
index df4da9dcab81ecc3da8cb2341c92f9f055b20055..ac8dd2005e59d2125ef78205b050e2555100174f 100644
--- a/test/unit/region.c
+++ b/test/unit/region.c
@@ -61,14 +61,14 @@ region_test_truncate()
 
 	fail_unless(ptr);
 
-	size_t size = region_used(&region);
+	size_t used = region_used(&region);
 
 	region_alloc_nothrow(&region, 10000);
 	region_alloc_nothrow(&region, 10000000);
 
-	region_truncate(&region, size);
+	region_truncate(&region, used);
 
-	fail_unless(region_used(&region) == size);
+	fail_unless(region_used(&region) == used);
 
 	region_free(&region);
 
diff --git a/test/unit/rtree.cc b/test/unit/rtree.cc
index 9e4960c862b0a49f901150f994eb98212e11396e..e8836deb9d5c81722f0e45d55e4c8de1d7373ccf 100644
--- a/test/unit/rtree.cc
+++ b/test/unit/rtree.cc
@@ -9,15 +9,17 @@
 
 static int page_count = 0;
 
+const uint32_t extent_size = RTREE_PAGE_SIZE * 8;
+
 static void *
-page_alloc()
+extent_alloc()
 {
 	page_count++;
-	return malloc(RTREE_PAGE_SIZE);
+	return malloc(extent_size);
 }
 
 static void
-page_free(void *page)
+extent_free(void *page)
 {
 	page_count--;
 	free(page);
@@ -34,7 +36,7 @@ simple_check()
 	header();
 
 	struct rtree tree;
-	rtree_init(&tree, page_alloc, page_free);
+	rtree_init(&tree, extent_size, extent_alloc, extent_free);
 
 	printf("Insert 1..X, remove 1..X\n");
 	for (size_t i = 1; i <= rounds; i++) {
@@ -241,8 +243,6 @@ neighbor_test()
 	header();
 
 	const int test_count = 1000;
-	struct rtree_iterator iterator;
-	rtree_iterator_init(&iterator);
 	struct rtree_rect arr[test_count];
 	static struct rtree_rect basis;
 
@@ -255,10 +255,12 @@ neighbor_test()
 
 	for (size_t i = 0; i <= test_count; i++) {
 		struct rtree tree;
-		rtree_init(&tree, page_alloc, page_free);
+		rtree_init(&tree, extent_size, extent_alloc, extent_free);
 
 		rtree_test_build(&tree, arr, i);
 
+		struct rtree_iterator iterator;
+		rtree_iterator_init(&iterator);
 		if (!rtree_search(&tree, &basis, SOP_NEIGHBOR, &iterator) && i != 0) {
 			fail("search is successful", "true");
 		}
@@ -269,10 +271,10 @@ neighbor_test()
 				fail("wrong search result", "true");
 			}
 		}
+		rtree_iterator_destroy(&iterator);
 		rtree_destroy(&tree);
 	}
 
-	rtree_iterator_destroy(&iterator);
 
 	footer();
 }
diff --git a/test/unit/rtree_itr.cc b/test/unit/rtree_itr.cc
index fae7c431108f5346f2cdcdcd1126d6337794bf1e..08bc959aec8ec7efc0c194ba0aeadea47b822ce2 100644
--- a/test/unit/rtree_itr.cc
+++ b/test/unit/rtree_itr.cc
@@ -6,19 +6,21 @@
 #include "unit.h"
 #include "salad/rtree.h"
 
-static int page_count = 0;
+static int extent_count = 0;
+
+const uint32_t extent_size = RTREE_PAGE_SIZE * 8;
 
 static void *
-page_alloc()
+extent_alloc()
 {
-	page_count++;
-	return malloc(RTREE_PAGE_SIZE);
+	extent_count++;
+	return malloc(extent_size);
 }
 
 static void
-page_free(void *page)
+extent_free(void *page)
 {
-	page_count--;
+	extent_count--;
 	free(page);
 }
 
@@ -28,7 +30,7 @@ itr_check()
 	header();
 
 	struct rtree tree;
-	rtree_init(&tree, page_alloc, page_free);
+	rtree_init(&tree, extent_size, extent_alloc, extent_free);
 
 	/* Filling tree */
 	const size_t count1 = 10000;
@@ -182,8 +184,8 @@ itr_check()
 	}
 
 	rtree_purge(&tree);
-	rtree_destroy(&tree);
 	rtree_iterator_destroy(&iterator);
+	rtree_destroy(&tree);
 
 	footer();
 }
@@ -197,9 +199,6 @@ itr_invalidate_check()
 	const size_t max_delete_count = 100;
 	const size_t max_insert_count = 200;
 	const size_t attempt_count = 100;
-	struct rtree_iterator iterators[test_size];
-	for (size_t i = 0; i < test_size; i++)
-		rtree_iterator_init(iterators + i);
 
 	struct rtree_rect rect;
 
@@ -212,7 +211,10 @@ itr_invalidate_check()
 			del_cnt = test_size - del_pos;
 		}
 		struct rtree tree;
-		rtree_init(&tree, page_alloc, page_free);
+		rtree_init(&tree, extent_size, extent_alloc, extent_free);
+		struct rtree_iterator iterators[test_size];
+		for (size_t i = 0; i < test_size; i++)
+			rtree_iterator_init(iterators + i);
 
 		for (size_t i = 0; i < test_size; i++) {
 			rtree_set2d(&rect, i, i, i, i);
@@ -240,6 +242,9 @@ itr_invalidate_check()
 				fail("Iterator was not invalidated (18)", "true");
 			}
 		}
+
+		for (size_t i = 0; i < test_size; i++)
+			rtree_iterator_destroy(iterators + i);
 		rtree_destroy(&tree);
 	}
 
@@ -250,7 +255,10 @@ itr_invalidate_check()
 		size_t ins_cnt = rand() % max_insert_count + 1;
 
 		struct rtree tree;
-		rtree_init(&tree, page_alloc, page_free);
+		rtree_init(&tree, extent_size, extent_alloc, extent_free);
+		struct rtree_iterator iterators[test_size];
+		for (size_t i = 0; i < test_size; i++)
+			rtree_iterator_init(iterators + i);
 
 		for (size_t i = 0; i < test_size; i++) {
 			rtree_set2d(&rect, i, i, i, i);
@@ -276,12 +284,12 @@ itr_invalidate_check()
 				fail("Iterator was not invalidated (22)", "true");
 			}
 		}
+
+		for (size_t i = 0; i < test_size; i++)
+			rtree_iterator_destroy(iterators + i);
 		rtree_destroy(&tree);
 	}
 
-	for (size_t i = 0; i < test_size; i++)
-		rtree_iterator_destroy(iterators + i);
-
 	footer();
 }
 
@@ -290,7 +298,7 @@ main(void)
 {
 	itr_check();
 	itr_invalidate_check();
-	if (page_count != 0) {
+	if (extent_count != 0) {
 		fail("memory leak!", "false");
 	}
 }