SHORS
Shors - a library for creating transport layer for distributed systems built with tarantool-module. Shors contains four components:
- http router (open api integration)
- rpc router
- rpc client
- builtin components like:
- middlewares (opentelemetry, metrics, etc)
- logger
HTTP
Create http route
Use a route::Builder for create http routes. After route created just register it with http Server.
Example:
use Builder;
use ;
use Context;
use Error;
A more complex example (with groups, error handling, custom and builtin middlewares):
use Lazy;
use stdout;
use Tracer;
use Builder;
use ;
use Context;
use ;
use Error;
static OPENTELEMETRY_TRACER: = new;
Add OpenAPI docs
First, add shors = { ..., features = ["open-api"]} to Cargo.toml of your project.
Use .define_open_api method on route builder and define OpenAPI operation. Underline
shors using utoipa crate for create OpenAPI schema.
For user convenience this crate reexported as shors::utoipa.
!Important: if you use derive macros from shors::utoipa please add this line into .rs file:
use utoipa as utoipa;
for correct work of a derive macros.
To access the resulting OpenAPI document use a shors::transport::http::openapi::with_open_api function.
See test application routes for familiarize with examples of usage.
For usage of swagger see shors::transport::http::openapi::swagger_ui_route function.
RPC
Prepare
Rpc transport required exported stored procedure - rpc_handler.
Create stored procedure. Example (where RPC_SERVER is the Server instance):
pub extern "C"
And export it from cartridge role. Example:
box...
rawset
Create rpc routes
Working with rpc routes same as http: use route::Builder for creating rpc routes. After route created register it with rpc Server.
Complex example:
use Lazy;
use RequestIdOwner;
use Server;
use ;
use Error;
use ;
thread_local!
RPC client
There is a special component for interaction with remote rpc endpoints. Currently, client can call rpc endpoint in four modes:
- by bucket_id (vshard)
- by bucket_id (vshard) async (without waiting for an response)
- by replicaset id (call current master)
- by cartridge role (call current master)
- by cartridge role with uri (call instance by uri that can be not current master)
Prepare
The RPC client requires some lua code to be registered whether in the luaopen_ function or an init function.
Note that a luaopen-function is called when a related library is properly loaded e.g. from the init.lua
file or the RPC client is intentionally initialized in the init function, as shown in the following examples
Examples:
Initialization of rpc-client directly from the init method
Defining a luaopen-function
pub unsafe extern "C"
Call rpc endpoint by bucket_id
let lua = lua_state;
let params = vec!;
let resp = new
.shard_endpoint
.call?;
Call rpc endpoint by bucket_id async
let lua = lua_state;
new
.async_shard_endpoint
.call?;
Call rpc endpoint by replicaset uuid
let lua = lua_state;
let params = vec!;
let resp = new
.replicaset_endpoint
.prefer_replica
.call?;
Call rpc endpoint by cartridge role
NOTE: calling rpc endpoint by role require register exported rpc_handler stored procedure as exported role method. For example:
return
Call example:
let lua = lua_state;
let resp = new
.role_endpoint
.call?;
Call rpc endpoint by cartridge role
NOTE: depend on rpc_handler (detail in Call rpc endpoint by cartridge role item)
let lua = lua_state;
let resp = new
.role_endpoint
.with_uri
.call?;
Builtin middlewares
- http server
- debug - print debug information in debug logs
- otel - opentelemetry tracing
- otel_conditional - opentelemetry tracing, disabled if http-header
with-tracenot set - access_logs - nginx like access logs
- rpc server
- debug - print debug information in debug logs
- otel - opentelemetry tracing
- record_latency - record route latency as prometheus metric
- rpc client
- otel - opentelemetry tracing
- retry - retry call on server side errors
- record_latency - record call latency as prometheus metric
Testing
Unit
Integration
( && )
Request pipeline (actual for 0.1.x version)
!NOTE text bellow not actual for shors v 0.2.0+
Shors use vshard/cartridge API underline for make remote requests. Both cartridge and vshard api is a LUA api. So, look at pipeline of shors rpc request:
Client side
- Rust side:
- sender create an instance of rpc::client::Client
- sender use rpc::client::Client::call method with rust structure that represent request payload
- rust structure serialize into LUA table (using tarantool::tlua - Push trait)
- LUA side:
- calling vshard or cartridge method with LUA table derived from previous step
- vshard or cartridge api calling iproto
- iproto serialize LUA table into msgpack and send it into server side
Server side
- LUA side:
- received message from iproto, message representation is a tarantool tuple
- call function-handler in rust using internal routes table
- Rust side:
- Call .decode method on rpc::Request to restore the request
So serialization/deserialization scheme looks like this: rust structure -> lua table -> msgpack representation -> rust structure
There is a problem in this scheme: what if we use enum in fields of initial rust structure? For example
let example = Foo;
tarantool::tlua will serialize this struct in lua table like this:
So as you see, information about which enum variant using is lost. In the future, when we serialize this LUA table into msgpack and then deserialize it into a rust structure, we will get a serde error. Serde expect some type information for deserializing, but there is no. In this example if LUA table looks like:
Deserialization will be success and no errors occurred.
How we can fix this? Currently most generic approach is an implement tlua::Push trait for enum. For this example (note, this is example implementation, don't create hashmap at production code):