Connecting to Tarantool from C++
To simplify the start of your working with the Tarantool C++ connector, we will use the example application from the connector repository. We will go step by step through the application code and explain what each part does.
The following main topics are discussed in this manual:
Pre-requisites
To go through this Getting Started exercise, you need the following
pre-requisites to be done:
- clone the connector source code and ensure having Tarantool and third-party software
- start Tarantool and create a database
- set up access rights.
The Tarantool C++ connector is currently supported for Linux only.
The connector itself is a header-only library, so, it doesn’t require
installation and building as such. All you need is to clone the connector
source code and embed it in your C++ project.
Also, make sure you have other necessary software and Tarantool installed.
Make sure you have the following third-party software. If you miss some of
the items, install them:
- Git, a version control system
- unzip utility
- gcc compiler complied with the C++17 standard
- cmake and make tools.
If you don’t have Tarantool on your OS, install it in one of the ways:
- from a package–refer to OS-specific instructions
- from the source.
Clone the Tarantool C++ connector repository.
git clone git@github.com:tarantool/tntcxx.git
Start Tarantool locally
or in Docker
and create a space with the following schema and index:
box.cfg{listen = 3301}
t = box.schema.space.create('t')
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
t:create_index('primary', {
type = 'hash',
parts = {'id'}
})
Important
Do not close the terminal window where Tarantool is running.
You will need it later to connect to Tarantool from your C++ application.
To be able to execute the necessary operations in Tarantool, you need to grant
the guest
user with the read-write rights. The simplest way is to grant
the user with the super role:
box.schema.user.grant('guest', 'super')
Connecting to Tarantool
There are three main parts of the C++ connector: the IO-zero-copy buffer,
the msgpack encoder/decoder, and the client that handles requests.
To set up connection to a Tarantool instance from a C++ application, you need
to do the following:
- embed the connector into the application
- instantiate a connector client and a connection object
- define connection parameters and invoke the method to connect
- define error handling behavior.
Embed the connector in your C++ application by including the main header:
#include "../src/Client/Connector.hpp"
First, we should create a connector client. It can handle many connections
to Tarantool instances asynchronously. To instantiate a client, you should specify
the buffer and the network provider implementations as template parameters.
The connector’s main class has the following signature:
template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>
class Connector;
The buffer is parametrized by allocator. It means that users can
choose which allocator will be used to provide memory for the buffer’s blocks.
Data is organized into a linked list of blocks of fixed size that is specified
as the template parameter of the buffer.
You can either implement your own buffer or network provider or use the default
ones as we do in our example. So, the default connector instantiation looks
as follows:
using Buf_t = tnt::Buffer<16 * 1024>;
#include "../src/Client/LibevNetProvider.hpp"
using Net_t = LibevNetProvider<Buf_t, DefaultStream>;
Connector<Buf_t, Net_t> client;
To use the BUFFER
class, the buffer header should also be included:
#include "../src/Buffer/Buffer.hpp"
A client itself is not enough to work with Tarantool instances–we
also need to create connection objects. A connection also takes the buffer and
the network provider as template parameters. Note that they must be the same
as ones of the client:
Connection<Buf_t, Net_t> conn(client);
Our Tarantool instance is listening to
the 3301
port on localhost
.
Let’s define the corresponding variables as well as the WAIT_TIMEOUT
variable
for connection timeout.
const char *address = "127.0.0.1";
int port = 3301;
int WAIT_TIMEOUT = 1000; //milliseconds
To connect to the Tarantool instance, we should invoke
the Connector::connect()
method of the client object and
pass three arguments: connection instance, address, and port.
int rc = client.connect(conn, {.address = address,
.service = std::to_string(port),
/*.user = ...,*/
/*.passwd = ...,*/
/* .transport = STREAM_SSL, */});
Implementation of the connector is exception free, so we rely on the return
codes: in case of fail, the connect()
method returns rc < 0
. To get the
error message corresponding to the last error occured during
communication with the instance, we can invoke the Connection::getError()
method.
if (rc != 0) {
//assert(conn.getError().saved_errno != 0);
std::cerr << conn.getError().msg << std::endl;
return -1;
}
To reset connection after errors, that is, to clean up the error message and
connection status, the Connection::reset()
method is used.
Working with requests
In this section, we will show how to:
We will also go through the case of having several connections
and executing a number of requests from different connections simultaneously.
In our example C++ application, we execute the following types of requests:
ping
replace
select
.
Note
Examples on other request types, namely, insert
, delete
, upsert
,
and update
, will be added to this manual later.
Each request method returns a request ID that is a sort of future.
This ID can be used to get the response message when it is ready.
Requests are queued in the output buffer of connection
until the Connector::wait()
method is called.
At this step, requests are encoded in the MessagePack
format and saved in the
output connection buffer. They are ready to be sent but the network
communication itself will be done later.
Let’s remind that for the requests manipulating with data we are dealing
with the Tarantool space t
created earlier,
and the space has the following format:
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
ping
rid_t ping = conn.ping();
replace
Equals to Lua request <space_name>:replace(pk_value, "111", 1)
.
uint32_t space_id = 512;
int pk_value = 666;
std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
rid_t replace = conn.space[space_id].replace(data);
select
Equals to Lua request <space_name>.index[0]:select({pk_value}, {limit = 1})
.
uint32_t index_id = 0;
uint32_t limit = 1;
uint32_t offset = 0;
IteratorType iter = IteratorType::EQ;
auto i = conn.space[space_id].index[index_id];
rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);
To send requests to the server side, invoke the client.wait()
method.
client.wait(conn, ping, WAIT_TIMEOUT);
The wait()
method takes the connection to poll,
the request ID, and, optionally, the timeout as parameters. Once a response
for the specified request is ready, wait()
terminates. It also
provides a negative return code in case of system related fails, for example,
a broken or timeouted connection. If wait()
returns 0
, then a response
has been received and expected to be parsed.
Now let’s send our requests to the Tarantool instance.
The futureIsReady()
function checks availability of a future and returns
true
or false
.
while (! conn.futureIsReady(ping)) {
/*
* wait() is the main function responsible for sending/receiving
* requests and implements event-loop under the hood. It may
* fail due to several reasons:
* - connection is timed out;
* - connection is broken (e.g. closed);
* - epoll is failed.
*/
if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {
std::cerr << conn.getError().msg << std::endl;
conn.reset();
}
}
To get the response when it is ready, use
the Connection::getResponse()
method. It takes the request ID and returns
an optional object containing the response. If the response is not ready yet,
the method returns std::nullopt
. Note that on each future,
getResponse()
can be called only once: it erases the request ID from
the internal map once it is returned to a user.
A response consists of a header and a body (response.header
and
response.body
). Depending on success of the request execution on the server
side, body may contain either runtime error(s) accessible by
response.body.error_stack
or data (tuples)–response.body.data
.
In turn, data is a vector of tuples. However, tuples are not decoded and
come in the form of pointers to the start and the end of msgpacks.
See the “Decoding and reading the data” section to
understand how to decode tuples.
There are two options for single connection it regards to receiving responses:
we can either wait for one specific future or for all of them at once.
We’ll try both options in our example. For the ping
request, let’s use the
first option.
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
/*
* Since conn.futureIsReady(ping) returned <true>, then response
* must be ready.
*/
assert(response != std::nullopt);
/*
* If request is successfully executed on server side, response
* will contain data (i.e. tuple being replaced in case of :replace()
* request or tuples satisfying search conditions in case of :select();
* responses for pings contain nothing - empty map).
* To tell responses containing data from error responses, one can
* rely on response code storing in the header or check
* Response->body.data and Response->body.error_stack members.
*/
printResponse<Buf_t>(*response);
For the replace
and select
requests, let’s examine the option of
waiting for both futures at once.
/* Let's wait for both futures at once. */
std::vector<rid_t> futures(2);
futures[0] = replace;
futures[1] = select;
/* No specified timeout means that we poll futures until they are ready.*/
client.waitAll(conn, futures);
for (size_t i = 0; i < futures.size(); ++i) {
assert(conn.futureIsReady(futures[i]));
response = conn.getResponse(futures[i]);
assert(response != std::nullopt);
printResponse<Buf_t>(*response);
}
Now, let’s have a look at the case when we establish two connections
to Tarantool instance simultaneously.
/* Let's create another connection. */
Connection<Buf_t, Net_t> another(client);
if (client.connect(another, {.address = address,
.service = std::to_string(port),
/* .transport = STREAM_SSL, */}) != 0) {
std::cerr << conn.getError().msg << std::endl;
return -1;
}
/* Simultaneously execute two requests from different connections. */
rid_t f1 = conn.ping();
rid_t f2 = another.ping();
/*
* waitAny() returns the first connection received response.
* All connections registered via :connect() call are participating.
*/
std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);
Connection<Buf_t, Net_t> first = *conn_opt;
if (first == conn) {
assert(conn.futureIsReady(f1));
(void) f1;
} else {
assert(another.futureIsReady(f2));
(void) f2;
}
Building and launching C++ application
Now, we are going to build our example C++ application, launch it
to connect to the Tarantool instance and execute all the requests defined.
Make sure you are in the root directory of the cloned C++ connector repository.
To build the example application:
cd examples
cmake .
make
Make sure the Tarantool session
you started earlier is running. Launch the application:
./Simple
As you can see from the execution log, all the connections to Tarantool
defined in our application have been established and all the requests
have been executed successfully.
Decoding and reading the data
Responses from a Tarantool instance contain raw data, that is, the data encoded
into the MessagePack tuples. To decode client’s data,
the user has to write their own
decoders (readers) based on the database schema and include them in one’s
application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use
the reader from our example.
First, the structure corresponding our example space format
is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array,
so, in fact, we should parse the array first. Let’s define another reader
for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).
To go through this Getting Started exercise, you need the following pre-requisites to be done:
- clone the connector source code and ensure having Tarantool and third-party software
- start Tarantool and create a database
- set up access rights.
The Tarantool C++ connector is currently supported for Linux only.
The connector itself is a header-only library, so, it doesn’t require installation and building as such. All you need is to clone the connector source code and embed it in your C++ project.
Also, make sure you have other necessary software and Tarantool installed.
Make sure you have the following third-party software. If you miss some of the items, install them:
- Git, a version control system
- unzip utility
- gcc compiler complied with the C++17 standard
- cmake and make tools.
If you don’t have Tarantool on your OS, install it in one of the ways:
- from a package–refer to OS-specific instructions
- from the source.
Clone the Tarantool C++ connector repository.
git clone git@github.com:tarantool/tntcxx.git
Start Tarantool locally or in Docker and create a space with the following schema and index:
box.cfg{listen = 3301}
t = box.schema.space.create('t')
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
t:create_index('primary', {
type = 'hash',
parts = {'id'}
})
Important
Do not close the terminal window where Tarantool is running. You will need it later to connect to Tarantool from your C++ application.
To be able to execute the necessary operations in Tarantool, you need to grant
the guest
user with the read-write rights. The simplest way is to grant
the user with the super role:
box.schema.user.grant('guest', 'super')
Connecting to Tarantool
There are three main parts of the C++ connector: the IO-zero-copy buffer,
the msgpack encoder/decoder, and the client that handles requests.
To set up connection to a Tarantool instance from a C++ application, you need
to do the following:
- embed the connector into the application
- instantiate a connector client and a connection object
- define connection parameters and invoke the method to connect
- define error handling behavior.
Embed the connector in your C++ application by including the main header:
#include "../src/Client/Connector.hpp"
First, we should create a connector client. It can handle many connections
to Tarantool instances asynchronously. To instantiate a client, you should specify
the buffer and the network provider implementations as template parameters.
The connector’s main class has the following signature:
template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>
class Connector;
The buffer is parametrized by allocator. It means that users can
choose which allocator will be used to provide memory for the buffer’s blocks.
Data is organized into a linked list of blocks of fixed size that is specified
as the template parameter of the buffer.
You can either implement your own buffer or network provider or use the default
ones as we do in our example. So, the default connector instantiation looks
as follows:
using Buf_t = tnt::Buffer<16 * 1024>;
#include "../src/Client/LibevNetProvider.hpp"
using Net_t = LibevNetProvider<Buf_t, DefaultStream>;
Connector<Buf_t, Net_t> client;
To use the BUFFER
class, the buffer header should also be included:
#include "../src/Buffer/Buffer.hpp"
A client itself is not enough to work with Tarantool instances–we
also need to create connection objects. A connection also takes the buffer and
the network provider as template parameters. Note that they must be the same
as ones of the client:
Connection<Buf_t, Net_t> conn(client);
Our Tarantool instance is listening to
the 3301
port on localhost
.
Let’s define the corresponding variables as well as the WAIT_TIMEOUT
variable
for connection timeout.
const char *address = "127.0.0.1";
int port = 3301;
int WAIT_TIMEOUT = 1000; //milliseconds
To connect to the Tarantool instance, we should invoke
the Connector::connect()
method of the client object and
pass three arguments: connection instance, address, and port.
int rc = client.connect(conn, {.address = address,
.service = std::to_string(port),
/*.user = ...,*/
/*.passwd = ...,*/
/* .transport = STREAM_SSL, */});
Implementation of the connector is exception free, so we rely on the return
codes: in case of fail, the connect()
method returns rc < 0
. To get the
error message corresponding to the last error occured during
communication with the instance, we can invoke the Connection::getError()
method.
if (rc != 0) {
//assert(conn.getError().saved_errno != 0);
std::cerr << conn.getError().msg << std::endl;
return -1;
}
To reset connection after errors, that is, to clean up the error message and
connection status, the Connection::reset()
method is used.
Working with requests
In this section, we will show how to:
We will also go through the case of having several connections
and executing a number of requests from different connections simultaneously.
In our example C++ application, we execute the following types of requests:
ping
replace
select
.
Note
Examples on other request types, namely, insert
, delete
, upsert
,
and update
, will be added to this manual later.
Each request method returns a request ID that is a sort of future.
This ID can be used to get the response message when it is ready.
Requests are queued in the output buffer of connection
until the Connector::wait()
method is called.
At this step, requests are encoded in the MessagePack
format and saved in the
output connection buffer. They are ready to be sent but the network
communication itself will be done later.
Let’s remind that for the requests manipulating with data we are dealing
with the Tarantool space t
created earlier,
and the space has the following format:
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
ping
rid_t ping = conn.ping();
replace
Equals to Lua request <space_name>:replace(pk_value, "111", 1)
.
uint32_t space_id = 512;
int pk_value = 666;
std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
rid_t replace = conn.space[space_id].replace(data);
select
Equals to Lua request <space_name>.index[0]:select({pk_value}, {limit = 1})
.
uint32_t index_id = 0;
uint32_t limit = 1;
uint32_t offset = 0;
IteratorType iter = IteratorType::EQ;
auto i = conn.space[space_id].index[index_id];
rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);
To send requests to the server side, invoke the client.wait()
method.
client.wait(conn, ping, WAIT_TIMEOUT);
The wait()
method takes the connection to poll,
the request ID, and, optionally, the timeout as parameters. Once a response
for the specified request is ready, wait()
terminates. It also
provides a negative return code in case of system related fails, for example,
a broken or timeouted connection. If wait()
returns 0
, then a response
has been received and expected to be parsed.
Now let’s send our requests to the Tarantool instance.
The futureIsReady()
function checks availability of a future and returns
true
or false
.
while (! conn.futureIsReady(ping)) {
/*
* wait() is the main function responsible for sending/receiving
* requests and implements event-loop under the hood. It may
* fail due to several reasons:
* - connection is timed out;
* - connection is broken (e.g. closed);
* - epoll is failed.
*/
if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {
std::cerr << conn.getError().msg << std::endl;
conn.reset();
}
}
To get the response when it is ready, use
the Connection::getResponse()
method. It takes the request ID and returns
an optional object containing the response. If the response is not ready yet,
the method returns std::nullopt
. Note that on each future,
getResponse()
can be called only once: it erases the request ID from
the internal map once it is returned to a user.
A response consists of a header and a body (response.header
and
response.body
). Depending on success of the request execution on the server
side, body may contain either runtime error(s) accessible by
response.body.error_stack
or data (tuples)–response.body.data
.
In turn, data is a vector of tuples. However, tuples are not decoded and
come in the form of pointers to the start and the end of msgpacks.
See the “Decoding and reading the data” section to
understand how to decode tuples.
There are two options for single connection it regards to receiving responses:
we can either wait for one specific future or for all of them at once.
We’ll try both options in our example. For the ping
request, let’s use the
first option.
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
/*
* Since conn.futureIsReady(ping) returned <true>, then response
* must be ready.
*/
assert(response != std::nullopt);
/*
* If request is successfully executed on server side, response
* will contain data (i.e. tuple being replaced in case of :replace()
* request or tuples satisfying search conditions in case of :select();
* responses for pings contain nothing - empty map).
* To tell responses containing data from error responses, one can
* rely on response code storing in the header or check
* Response->body.data and Response->body.error_stack members.
*/
printResponse<Buf_t>(*response);
For the replace
and select
requests, let’s examine the option of
waiting for both futures at once.
/* Let's wait for both futures at once. */
std::vector<rid_t> futures(2);
futures[0] = replace;
futures[1] = select;
/* No specified timeout means that we poll futures until they are ready.*/
client.waitAll(conn, futures);
for (size_t i = 0; i < futures.size(); ++i) {
assert(conn.futureIsReady(futures[i]));
response = conn.getResponse(futures[i]);
assert(response != std::nullopt);
printResponse<Buf_t>(*response);
}
Now, let’s have a look at the case when we establish two connections
to Tarantool instance simultaneously.
/* Let's create another connection. */
Connection<Buf_t, Net_t> another(client);
if (client.connect(another, {.address = address,
.service = std::to_string(port),
/* .transport = STREAM_SSL, */}) != 0) {
std::cerr << conn.getError().msg << std::endl;
return -1;
}
/* Simultaneously execute two requests from different connections. */
rid_t f1 = conn.ping();
rid_t f2 = another.ping();
/*
* waitAny() returns the first connection received response.
* All connections registered via :connect() call are participating.
*/
std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);
Connection<Buf_t, Net_t> first = *conn_opt;
if (first == conn) {
assert(conn.futureIsReady(f1));
(void) f1;
} else {
assert(another.futureIsReady(f2));
(void) f2;
}
Building and launching C++ application
Now, we are going to build our example C++ application, launch it
to connect to the Tarantool instance and execute all the requests defined.
Make sure you are in the root directory of the cloned C++ connector repository.
To build the example application:
cd examples
cmake .
make
Make sure the Tarantool session
you started earlier is running. Launch the application:
./Simple
As you can see from the execution log, all the connections to Tarantool
defined in our application have been established and all the requests
have been executed successfully.
Decoding and reading the data
Responses from a Tarantool instance contain raw data, that is, the data encoded
into the MessagePack tuples. To decode client’s data,
the user has to write their own
decoders (readers) based on the database schema and include them in one’s
application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use
the reader from our example.
First, the structure corresponding our example space format
is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array,
so, in fact, we should parse the array first. Let’s define another reader
for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).
There are three main parts of the C++ connector: the IO-zero-copy buffer, the msgpack encoder/decoder, and the client that handles requests.
To set up connection to a Tarantool instance from a C++ application, you need to do the following:
- embed the connector into the application
- instantiate a connector client and a connection object
- define connection parameters and invoke the method to connect
- define error handling behavior.
Embed the connector in your C++ application by including the main header:
#include "../src/Client/Connector.hpp"
First, we should create a connector client. It can handle many connections to Tarantool instances asynchronously. To instantiate a client, you should specify the buffer and the network provider implementations as template parameters. The connector’s main class has the following signature:
template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>
class Connector;
The buffer is parametrized by allocator. It means that users can choose which allocator will be used to provide memory for the buffer’s blocks. Data is organized into a linked list of blocks of fixed size that is specified as the template parameter of the buffer.
You can either implement your own buffer or network provider or use the default ones as we do in our example. So, the default connector instantiation looks as follows:
using Buf_t = tnt::Buffer<16 * 1024>;
#include "../src/Client/LibevNetProvider.hpp"
using Net_t = LibevNetProvider<Buf_t, DefaultStream>;
Connector<Buf_t, Net_t> client;
To use the BUFFER
class, the buffer header should also be included:
#include "../src/Buffer/Buffer.hpp"
A client itself is not enough to work with Tarantool instances–we also need to create connection objects. A connection also takes the buffer and the network provider as template parameters. Note that they must be the same as ones of the client:
Connection<Buf_t, Net_t> conn(client);
Our Tarantool instance is listening to
the 3301
port on localhost
.
Let’s define the corresponding variables as well as the WAIT_TIMEOUT
variable
for connection timeout.
const char *address = "127.0.0.1";
int port = 3301;
int WAIT_TIMEOUT = 1000; //milliseconds
To connect to the Tarantool instance, we should invoke
the Connector::connect()
method of the client object and
pass three arguments: connection instance, address, and port.
int rc = client.connect(conn, {.address = address,
.service = std::to_string(port),
/*.user = ...,*/
/*.passwd = ...,*/
/* .transport = STREAM_SSL, */});
Implementation of the connector is exception free, so we rely on the return
codes: in case of fail, the connect()
method returns rc < 0
. To get the
error message corresponding to the last error occured during
communication with the instance, we can invoke the Connection::getError()
method.
if (rc != 0) {
//assert(conn.getError().saved_errno != 0);
std::cerr << conn.getError().msg << std::endl;
return -1;
}
To reset connection after errors, that is, to clean up the error message and
connection status, the Connection::reset()
method is used.
Working with requests
In this section, we will show how to:
We will also go through the case of having several connections
and executing a number of requests from different connections simultaneously.
In our example C++ application, we execute the following types of requests:
ping
replace
select
.
Note
Examples on other request types, namely, insert
, delete
, upsert
,
and update
, will be added to this manual later.
Each request method returns a request ID that is a sort of future.
This ID can be used to get the response message when it is ready.
Requests are queued in the output buffer of connection
until the Connector::wait()
method is called.
At this step, requests are encoded in the MessagePack
format and saved in the
output connection buffer. They are ready to be sent but the network
communication itself will be done later.
Let’s remind that for the requests manipulating with data we are dealing
with the Tarantool space t
created earlier,
and the space has the following format:
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
ping
rid_t ping = conn.ping();
replace
Equals to Lua request <space_name>:replace(pk_value, "111", 1)
.
uint32_t space_id = 512;
int pk_value = 666;
std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
rid_t replace = conn.space[space_id].replace(data);
select
Equals to Lua request <space_name>.index[0]:select({pk_value}, {limit = 1})
.
uint32_t index_id = 0;
uint32_t limit = 1;
uint32_t offset = 0;
IteratorType iter = IteratorType::EQ;
auto i = conn.space[space_id].index[index_id];
rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);
To send requests to the server side, invoke the client.wait()
method.
client.wait(conn, ping, WAIT_TIMEOUT);
The wait()
method takes the connection to poll,
the request ID, and, optionally, the timeout as parameters. Once a response
for the specified request is ready, wait()
terminates. It also
provides a negative return code in case of system related fails, for example,
a broken or timeouted connection. If wait()
returns 0
, then a response
has been received and expected to be parsed.
Now let’s send our requests to the Tarantool instance.
The futureIsReady()
function checks availability of a future and returns
true
or false
.
while (! conn.futureIsReady(ping)) {
/*
* wait() is the main function responsible for sending/receiving
* requests and implements event-loop under the hood. It may
* fail due to several reasons:
* - connection is timed out;
* - connection is broken (e.g. closed);
* - epoll is failed.
*/
if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {
std::cerr << conn.getError().msg << std::endl;
conn.reset();
}
}
To get the response when it is ready, use
the Connection::getResponse()
method. It takes the request ID and returns
an optional object containing the response. If the response is not ready yet,
the method returns std::nullopt
. Note that on each future,
getResponse()
can be called only once: it erases the request ID from
the internal map once it is returned to a user.
A response consists of a header and a body (response.header
and
response.body
). Depending on success of the request execution on the server
side, body may contain either runtime error(s) accessible by
response.body.error_stack
or data (tuples)–response.body.data
.
In turn, data is a vector of tuples. However, tuples are not decoded and
come in the form of pointers to the start and the end of msgpacks.
See the “Decoding and reading the data” section to
understand how to decode tuples.
There are two options for single connection it regards to receiving responses:
we can either wait for one specific future or for all of them at once.
We’ll try both options in our example. For the ping
request, let’s use the
first option.
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
/*
* Since conn.futureIsReady(ping) returned <true>, then response
* must be ready.
*/
assert(response != std::nullopt);
/*
* If request is successfully executed on server side, response
* will contain data (i.e. tuple being replaced in case of :replace()
* request or tuples satisfying search conditions in case of :select();
* responses for pings contain nothing - empty map).
* To tell responses containing data from error responses, one can
* rely on response code storing in the header or check
* Response->body.data and Response->body.error_stack members.
*/
printResponse<Buf_t>(*response);
For the replace
and select
requests, let’s examine the option of
waiting for both futures at once.
/* Let's wait for both futures at once. */
std::vector<rid_t> futures(2);
futures[0] = replace;
futures[1] = select;
/* No specified timeout means that we poll futures until they are ready.*/
client.waitAll(conn, futures);
for (size_t i = 0; i < futures.size(); ++i) {
assert(conn.futureIsReady(futures[i]));
response = conn.getResponse(futures[i]);
assert(response != std::nullopt);
printResponse<Buf_t>(*response);
}
Now, let’s have a look at the case when we establish two connections
to Tarantool instance simultaneously.
/* Let's create another connection. */
Connection<Buf_t, Net_t> another(client);
if (client.connect(another, {.address = address,
.service = std::to_string(port),
/* .transport = STREAM_SSL, */}) != 0) {
std::cerr << conn.getError().msg << std::endl;
return -1;
}
/* Simultaneously execute two requests from different connections. */
rid_t f1 = conn.ping();
rid_t f2 = another.ping();
/*
* waitAny() returns the first connection received response.
* All connections registered via :connect() call are participating.
*/
std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);
Connection<Buf_t, Net_t> first = *conn_opt;
if (first == conn) {
assert(conn.futureIsReady(f1));
(void) f1;
} else {
assert(another.futureIsReady(f2));
(void) f2;
}
Building and launching C++ application
Now, we are going to build our example C++ application, launch it
to connect to the Tarantool instance and execute all the requests defined.
Make sure you are in the root directory of the cloned C++ connector repository.
To build the example application:
cd examples
cmake .
make
Make sure the Tarantool session
you started earlier is running. Launch the application:
./Simple
As you can see from the execution log, all the connections to Tarantool
defined in our application have been established and all the requests
have been executed successfully.
Decoding and reading the data
Responses from a Tarantool instance contain raw data, that is, the data encoded
into the MessagePack tuples. To decode client’s data,
the user has to write their own
decoders (readers) based on the database schema and include them in one’s
application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use
the reader from our example.
First, the structure corresponding our example space format
is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array,
so, in fact, we should parse the array first. Let’s define another reader
for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).
In this section, we will show how to:
We will also go through the case of having several connections and executing a number of requests from different connections simultaneously.
In our example C++ application, we execute the following types of requests:
ping
replace
select
.
Note
Examples on other request types, namely, insert
, delete
, upsert
,
and update
, will be added to this manual later.
Each request method returns a request ID that is a sort of future.
This ID can be used to get the response message when it is ready.
Requests are queued in the output buffer of connection
until the Connector::wait()
method is called.
At this step, requests are encoded in the MessagePack format and saved in the output connection buffer. They are ready to be sent but the network communication itself will be done later.
Let’s remind that for the requests manipulating with data we are dealing
with the Tarantool space t
created earlier,
and the space has the following format:
t:format({
{name = 'id', type = 'unsigned'},
{name = 'a', type = 'string'},
{name = 'b', type = 'number'}
})
ping
rid_t ping = conn.ping();
replace
Equals to Lua request <space_name>:replace(pk_value, "111", 1)
.
uint32_t space_id = 512;
int pk_value = 666;
std::tuple data = std::make_tuple(pk_value /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
rid_t replace = conn.space[space_id].replace(data);
select
Equals to Lua request <space_name>.index[0]:select({pk_value}, {limit = 1})
.
uint32_t index_id = 0;
uint32_t limit = 1;
uint32_t offset = 0;
IteratorType iter = IteratorType::EQ;
auto i = conn.space[space_id].index[index_id];
rid_t select = i.select(std::make_tuple(pk_value), limit, offset, iter);
To send requests to the server side, invoke the client.wait()
method.
client.wait(conn, ping, WAIT_TIMEOUT);
The wait()
method takes the connection to poll,
the request ID, and, optionally, the timeout as parameters. Once a response
for the specified request is ready, wait()
terminates. It also
provides a negative return code in case of system related fails, for example,
a broken or timeouted connection. If wait()
returns 0
, then a response
has been received and expected to be parsed.
Now let’s send our requests to the Tarantool instance.
The futureIsReady()
function checks availability of a future and returns
true
or false
.
while (! conn.futureIsReady(ping)) {
/*
* wait() is the main function responsible for sending/receiving
* requests and implements event-loop under the hood. It may
* fail due to several reasons:
* - connection is timed out;
* - connection is broken (e.g. closed);
* - epoll is failed.
*/
if (client.wait(conn, ping, WAIT_TIMEOUT) != 0) {
std::cerr << conn.getError().msg << std::endl;
conn.reset();
}
}
To get the response when it is ready, use
the Connection::getResponse()
method. It takes the request ID and returns
an optional object containing the response. If the response is not ready yet,
the method returns std::nullopt
. Note that on each future,
getResponse()
can be called only once: it erases the request ID from
the internal map once it is returned to a user.
A response consists of a header and a body (response.header
and
response.body
). Depending on success of the request execution on the server
side, body may contain either runtime error(s) accessible by
response.body.error_stack
or data (tuples)–response.body.data
.
In turn, data is a vector of tuples. However, tuples are not decoded and
come in the form of pointers to the start and the end of msgpacks.
See the “Decoding and reading the data” section to
understand how to decode tuples.
There are two options for single connection it regards to receiving responses:
we can either wait for one specific future or for all of them at once.
We’ll try both options in our example. For the ping
request, let’s use the
first option.
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
/*
* Since conn.futureIsReady(ping) returned <true>, then response
* must be ready.
*/
assert(response != std::nullopt);
/*
* If request is successfully executed on server side, response
* will contain data (i.e. tuple being replaced in case of :replace()
* request or tuples satisfying search conditions in case of :select();
* responses for pings contain nothing - empty map).
* To tell responses containing data from error responses, one can
* rely on response code storing in the header or check
* Response->body.data and Response->body.error_stack members.
*/
printResponse<Buf_t>(*response);
For the replace
and select
requests, let’s examine the option of
waiting for both futures at once.
/* Let's wait for both futures at once. */
std::vector<rid_t> futures(2);
futures[0] = replace;
futures[1] = select;
/* No specified timeout means that we poll futures until they are ready.*/
client.waitAll(conn, futures);
for (size_t i = 0; i < futures.size(); ++i) {
assert(conn.futureIsReady(futures[i]));
response = conn.getResponse(futures[i]);
assert(response != std::nullopt);
printResponse<Buf_t>(*response);
}
Now, let’s have a look at the case when we establish two connections to Tarantool instance simultaneously.
/* Let's create another connection. */
Connection<Buf_t, Net_t> another(client);
if (client.connect(another, {.address = address,
.service = std::to_string(port),
/* .transport = STREAM_SSL, */}) != 0) {
std::cerr << conn.getError().msg << std::endl;
return -1;
}
/* Simultaneously execute two requests from different connections. */
rid_t f1 = conn.ping();
rid_t f2 = another.ping();
/*
* waitAny() returns the first connection received response.
* All connections registered via :connect() call are participating.
*/
std::optional<Connection<Buf_t, Net_t>> conn_opt = client.waitAny(WAIT_TIMEOUT);
Connection<Buf_t, Net_t> first = *conn_opt;
if (first == conn) {
assert(conn.futureIsReady(f1));
(void) f1;
} else {
assert(another.futureIsReady(f2));
(void) f2;
}
Building and launching C++ application
Now, we are going to build our example C++ application, launch it
to connect to the Tarantool instance and execute all the requests defined.
Make sure you are in the root directory of the cloned C++ connector repository.
To build the example application:
cd examples
cmake .
make
Make sure the Tarantool session
you started earlier is running. Launch the application:
./Simple
As you can see from the execution log, all the connections to Tarantool
defined in our application have been established and all the requests
have been executed successfully.
Decoding and reading the data
Responses from a Tarantool instance contain raw data, that is, the data encoded
into the MessagePack tuples. To decode client’s data,
the user has to write their own
decoders (readers) based on the database schema and include them in one’s
application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use
the reader from our example.
First, the structure corresponding our example space format
is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array,
so, in fact, we should parse the array first. Let’s define another reader
for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).
Now, we are going to build our example C++ application, launch it to connect to the Tarantool instance and execute all the requests defined.
Make sure you are in the root directory of the cloned C++ connector repository. To build the example application:
cd examples
cmake .
make
Make sure the Tarantool session you started earlier is running. Launch the application:
./Simple
As you can see from the execution log, all the connections to Tarantool defined in our application have been established and all the requests have been executed successfully.
Decoding and reading the data
Responses from a Tarantool instance contain raw data, that is, the data encoded
into the MessagePack tuples. To decode client’s data,
the user has to write their own
decoders (readers) based on the database schema and include them in one’s
application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use
the reader from our example.
First, the structure corresponding our example space format
is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array,
so, in fact, we should parse the array first. Let’s define another reader
for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).
Responses from a Tarantool instance contain raw data, that is, the data encoded into the MessagePack tuples. To decode client’s data, the user has to write their own decoders (readers) based on the database schema and include them in one’s application:
#include "Reader.hpp"
To show the logic of decoding a response, we will use the reader from our example.
First, the structure corresponding our example space format is defined:
/**
* Corresponds to tuples stored in user's space:
* box.execute("CREATE TABLE t (id UNSIGNED PRIMARY KEY, a TEXT, d DOUBLE);")
*/
struct UserTuple {
uint64_t field1;
std::string field2;
double field3;
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
Prototype of the base reader is given in src/mpp/Dec.hpp
:
template <class BUFFER, Type TYPE>
struct SimpleReaderBase : DefaultErrorHandler {
using BufferIterator_t = typename BUFFER::iterator;
/* Allowed type of values to be parsed. */
static constexpr Type VALID_TYPES = TYPE;
BufferIterator_t* StoreEndIterator() { return nullptr; }
};
Every new reader should inherit from it or directly from the
DefaultErrorHandler
.
To parse a particular value, we should define the Value()
method.
First two arguments of the method are common and unused as a rule,
but the third one defines the parsed value. In case of POD (Plain Old Data)
structures, it’s enough to provide a byte-to-byte copy. Since there are
fields of three different types in our schema, let’s define the corresponding
Value()
functions:
It’s also important to understand that a tuple itself is wrapped in an array, so, in fact, we should parse the array first. Let’s define another reader for that purpose.
The SetReader()
method sets the reader that is invoked while
each of the array’s entries is parsed. To make two readers defined above
work, we should create a decoder, set its iterator to the position of
the encoded tuple, and invoke the Read()
method (the code block below is
from the example application).