Skip to content

Commit

Permalink
readme: update README.md
Browse files Browse the repository at this point in the history
The commit completely rewrites decoding section since the decoder was
reworked. Along the way, commit marks all snippets as `c++` code and
drops `conn.status.is_failed` from docs since it doesn't exist anymore.

Closes #94
  • Loading branch information
drewdzzz committed Oct 15, 2024
1 parent 3cfffe0 commit fcf86e5
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 90 deletions.
137 changes: 48 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Connector can be embedded in any C++ application with including main header:
To create client one should specify buffer's and network provider's implementations
as template parameters. Connector's main class has the following signature:

```
```c++
template<class BUFFER, class NetProvider = EpollNetProvider<BUFFER>>
class Connector;
```
Expand All @@ -122,7 +122,7 @@ one can use default one: `tnt::Buffer<16 * 1024>` and
`EpollNetProvider<tnt::Buffer<16 * 1024>>`.
So the default instantiation would look
like:
```
```c++
using Buf_t = tnt::Buffer<16 * 1024>;
using Net_t = EpollNetProvider<Buf_t >;
Connector<Buf_t, Net_t> client;
Expand All @@ -131,7 +131,7 @@ Connector<Buf_t, Net_t> client;
Client itself is not enough to work with Tarantool instances, so let's also create
connection objects. Connection takes buffer and network provider as template
parameters as well (note that they must be the same as ones of client):
```
```c++
Connection<Buf_t, Net_t> conn(client);
```
Expand All @@ -140,17 +140,18 @@ Connection<Buf_t, Net_t> conn(client);
Now assume Tarantool instance is listening `3301` port on localhost. To connect
to the server we should invoke `Connector::connect()` method of client object and
pass three arguments: connection instance, address and port.
`int rc = client.connect(conn, address, port)`.
```c++
int rc = client.connect(conn, address, port);
```

### Error handling

Implementation of connector is exception
free, so we rely on return codes: in case of fail, `connect()` will return `rc < 0`.
To get error message corresponding to the last error happened during communication
with server, we can invoke `Connection::getError()` method:
```
```c++
if (rc != 0) {
assert(conn.status.is_failed);
std::cerr << conn.getError() << std::endl;
}
```
Expand All @@ -161,16 +162,18 @@ one can use `Connection::reset()`.
### Preparing requests

To execute simplest request (i.e. ping), one can invoke corresponding method of
connection object:
`rid_t ping = conn.ping();`
connection object:
```c++
rid_t ping = conn.ping();
```
Each request method returns request id, which is sort of future. It can be used
to get the result of request execution once it is ready (i.e. response). Requests
are queued in the input buffer of connection until `Connector::wait()` is called.

### Sending requests

That said, to send requests to the server side, we should invoke `client.wait()`:
```
```c++
client.wait(conn, ping, WAIT_TIMEOUT);
```
Basically, `wait()` takes connection to poll (both IN and OUT), request id and
Expand All @@ -187,7 +190,7 @@ in case response is not ready yet). Note that on each future it can be called
only once: `getResponse()` erases request id from internal map once it is
returned to user.

```
```c++
std::optional<Response<Buf_t>> response = conn.getResponse(ping);
```
Response consists of header and body (`response.header` and `response.body`).
Expand All @@ -204,103 +207,59 @@ Assume we have space with `id = 512` and following format on the server:
`CREATE TABLE t(id INT PRIMARY KEY, a TEXT, b DOUBLE);`
Preparing analogue of `t:replace(1, "111", 1.01);` request can be done this way:

```
```c++
std::tuple data = std::make_tuple(1 /* field 1*/, "111" /* field 2*/, 1.01 /* field 3*/);
rid_t replace = conn.space[512].replace(data);
```
To execute select query `t.index[1]:select({1}, {limit = 1})`:

```
```c++
auto i = conn.space[512].index[1];
rid_t select = i.select(std::make_tuple(1), 1, 0 /*offset*/, IteratorType::EQ);
```

### Data readers

Responses from server contain raw data (i.e. encoded into msgpuck tuples). To
decode client's data, users have to write their own decoders (based on featured
schema). Let's define structure describing data stored in space `t`:
Responses from server contain raw data (i.e. encoded into MsgPack tuples).
Let's define structure describing data stored in space `t`:

```
```c++
struct UserTuple {
uint64_t field1;
std::string field2;
double 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; }
};
```
So every new reader should inherit from it or directly from `DefaultErrorHandler`.
To parse particular value, we should define `Value()` method. First two arguments
are common and unused as a rule, but the third - defines parsed value. So in
case of POD stuctures it's enough to provide byte-to-byte copy. Since in our
schema there are fields of three different types, let's descripe three `Value()`
functions:
```
struct UserTupleValueReader : mpp::DefaultErrorHandler {
/* Store instance of tuple to be parsed. */
UserTuple& tuple;
/* Enumerate all types which can be parsed. Otherwise */
static constexpr mpp::Type VALID_TYPES = mpp::MP_UINT | mpp::MP_STR | mpp::MP_DBL;
UserTupleValueReader(UserTuple& t) : tuple(t) {}
/* Value's extractors. */
void Value(const BufIter_t&, mpp::compact::Type, uint64_t u)
{
tuple.field1 = u;
}
void Value(const BufIter_t&, mpp::compact::Type, double d)
{
tuple.field3 = d;
}
void Value(const BufIter_t& itr, mpp::compact::Type, mpp::StrValue v)
{
BufIter_t tmp = itr;
tmp += v.offset;
std::string &dst = tuple.field2;
while (v.size) {
dst.push_back(*tmp);
++tmp;
--v.size;
}
}
static constexpr auto mpp = std::make_tuple(
&UserTuple::field1, &UserTuple::field2, &UserTuple::field3);
};
```
It is worth mentioning that tuple itself is wrapped into array, so in fact
firstly we should parse array. Let's define another one reader:
```
template <class BUFFER>
struct UserTupleReader : mpp::SimpleReaderBase<BUFFER, mpp::MP_ARR> {
mpp::Dec<BUFFER>& dec;
UserTuple& tuple;
UserTupleReader(mpp::Dec<BUFFER>& d, UserTuple& t) : dec(d), tuple(t) {}
void Value(const iterator_t<BUFFER>&, mpp::compact::Type, mpp::ArrValue)
{
dec.SetReader(false, UserTupleValueReader{tuple});
}
};
```
`SetReader();` sets the reader which is invoked while every entry of the array is
parsed. Now, to make these two readers work, we should create decoder, set
its iterator to the position of encoded tuple and invoke `Read()` method:
```
UserTuple tuple;
mpp::Dec dec(conn.getInBuf());
dec.SetPosition(*t.begin);
dec.SetReader(false, UserTupleReader<BUFFER>{dec, tuple});
dec.Read();
```
### Writing custom buffer and network provider
Member `mpp` is neccessary - it sets the relationship between the structure
members and associated tuple's fields. It is used by encoder and decoder
for Object <-> MsgPack serialization. For instance, such structure will be
serialied as a MsgPack array `[<field1>, <field2>, <field3>]`. If you need
to serialize non-static members of objects,
[pointers to data members](https://en.cppreference.com/w/cpp/language/pointer#Pointers_to_data_members)
can be used, just as in this example.
Let's get back to the example with `select`. Consider the request successful.
We can decode data in this way:
```c++
if (response.body.data != std::nullopt) {
std::vector<UserTuple> results;
bool ok = response.body.data->decode(results);
if (ok)
print_results(results);
}
```

TODO
Firstly, we check if the response actually contains any data (Tarantool has
sent `IPROTO_DATA` in response). According to
[`IPROTO` protocol](https://www.tarantool.io/ru/doc/latest/reference/internals/box_protocol/),
key `IPROTO_DATA`
[has](https://www.tarantool.io/ru/doc/latest/reference/internals/iproto/format/#body)
an array of tuples as value in response to `select`. So, in order to
successfully decode them, we should pass an array of tuples to decoder - that's
why `std::vector<UserTuple>` is needed. If decoding was successful, `results`
will contain all decoded `UserTuples`.
1 change: 0 additions & 1 deletion doc/tntcxx_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ Public methods
int rc = client.connect(conn, address, port);
if (rc != 0) {
assert(conn.status.is_failed);
std::cerr << conn.getError() << std::endl;
return -1;
}
Expand Down

0 comments on commit fcf86e5

Please sign in to comment.