SHORS

![Latest Version] ![Docs badge]

Shors - a library for creating transport layer for distributed systems built with tarantool-module. Shors contains four components: - http router - rpc router - rpc client - builtin components like: - middlewares (opentelemetry, metrics, etc) - logger

!!! Read before work with this lib

HTTP

Create http route

Use a route::Builder for create http routes. After route created just register it with http Server.

Example:

```rust use shors::transport::http::route::Builder; use shors::transport::http::{server, Request}; use shors::transport::Context; use std::error::Error;

fn makehttpendpoint() { let endpoint = Builder::new() .withmethod("GET") .withpath("/concat/a/:a/b/:b") .build( |ctx: &mut Context, request: Request| -> Result<_, Box> { let a = request .stash .get("a") .map(|s| s.asstr()) .unwrapordefault(); let b = request .stash .get("b") .map(|s| s.asstr()) .unwrapor_default();

          Ok(a.to_string() + b)
        },
      );

let s = server::Server::new(); s.register(Box::new(endpoint)); } ```

A more complex example (with groups, error handling, custom and builtin middlewares): ```rust use oncecell::sync::Lazy; use opentelemetry::sdk::export::trace::stdout; use opentelemetry::sdk::trace::Tracer; use shors::transport::http::route::Builder; use shors::transport::http::{server, Request, Response}; use shors::transport::Context; use shors::{middleware, shorserror}; use std::error::Error;

static OPENTELEMETRYTRACER: Lazy = Lazy::new(|| stdout::newpipeline().install_simple());

pub fn makehttpendpoints() { let routegroup = Builder::new() .withpath("/v1") .withmiddleware(|route| { println!("got new http request!"); route }) .withmiddleware(middleware::http::otel(&OPENTELEMETRY_TRACER)) .group();

#[derive(serde::Deserialize)] struct EchoRequest { text: String, }

let echo = routegroup .builder() .withmethod("POST") .withpath("/echo") .build( |ctx: &mut Context, request: Request| -> Result<_, Box> { let req: EchoRequest = request.parse()?; Ok(req.text) }, );

let ping = routegroup .builder() .withmethod("GET") .withpath("/ping") .build(|ctx: &mut Context, _request: Request| -> Result<_, Box> { Ok("pong") });

let s = server::Server::new(); s.register(Box::new(echo)); s.register(Box::new(ping)); } ```

RPC

Prepare

Rpc transport required exported stored procedure - rpc_handler.

Create stored procedure. Example (where RPC_SERVER is the Server instance): ```rust

[no_mangle]

pub extern "C" fn rpchandler(ctx: FunctionCtx, args: FunctionArgs) -> cint { RPC_SERVER.with(|srv| srv.handle(ctx, args)) } ```

And export it from cartridge role. Example:

lua box.schema.func.create('mylib.rpc_handler', { language = 'C', if_not_exists = true }) rawset(_G, 'rpc_handler', function(path, ctx, mp_request) return box.func['mylib.rpc_handle']:call({ path, ctx, mp_request }) end)

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: ```rust use oncecell::unsync::Lazy; use shors::log::RequestIdOwner; use shors::transport::rpc::server::Server; use shors::transport::{rpc, Context}; use std::error::Error; use shors::{middleware, shorserror};

threadlocal! { pub static RPCSERVER: Lazy = Lazy::new(Server::new); }

[tarantool::proc]

fn initrpc() -> tarantool::Result<()> { let routes = rpc::route::Builder::new() .witherrorhandler(|ctx, err| { shorserror!(ctx: ctx, "rpc error {}", err); }) .withmiddleware(|route| { println!("got new rpc request!"); route }) .withmiddleware(middleware::rpc::otel(&OPENTELEMETRY_TRACER)) .group();

let sumroute = routes.builder().withpath("/sum").build( |ctx: &mut Context, req: rpc::Request| -> Result<_, Box> { let numbers = req.parse::>()?.0; Ok(numbers.intoiter().sum::()) }, );

RPCSERVER.with(|srv| { srv.register(Box::new(sumroute)); });

Ok(()) } ```

RPC client

There is a special component for interaction with remote rpc endpoints. Currently, client can call rpc endpoint in four modes: - by bucketid (vshard) - by bucketid (vshard) async (without waiting for an response) - by replicaset id (call current master) - by cartridge role (call current master)

Prepare

Rpc client require register some lua code in luaopen_ function (or in any init function).

Example: ```rust

[no_mangle]

pub unsafe extern "C" fn luaopenlibstub(l: *mut ffilua::luaState) -> cint { let lua = tlua::StaticLua::fromstatic(l); shors::initlua_functions(&lua).unwrap(); 1 } ```

Call rpc endpoint by bucket_id

```rust let lua = tarantool::lua_state();

let params = vec![2, 3, 4];
let resp = rpc::client::Builder::new(&lua)
    .shard_endpoint("/add")
    .call(&mut Context::background(), bucket_id, &params)?;

```

Call rpc endpoint by bucket_id async

```rust let lua = tarantool::lua_state();

rpc::client::Builder::new(&lua)
    .async_shard_endpoint("/ping")
    .call(&mut Context::background(), bucket_id, &())?;

```

Call rpc endpoint by replicaset uuid

```rust let lua = tarantool::lua_state();

let params = vec![2, 3, 4];
let resp = rpc::client::Builder::new(&lua)
    .replicaset_endpoint("/add")
    .prefer_replica()
    .call(&mut Context::background(), rs_uuid, &params)?;

```

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:

lua return { role_name = 'app.roles.stub', ... rpc_handler = function(path, ctx, mp_request) return box.func['libstub.rpc_handle']:call({ path, ctx, mp_request }) end, }

Call example: ```rust let lua = tarantool::lua_state();

let resp = rpc::client::Builder::new(&lua)
  .role_endpoint("app.roles.stub", "/ping")
  .call(&mut Context::background(), &())?;

```

Builtin middlewares

Testing

Unit

bash make unit-test

Integration

bash (cd tests/testapplication/ && cartridge build) make int-test

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

Server side

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 ```rust #[derive(Debug, Deserialize, Serialize, tlua::Push)] enum Value { String(String), Code(String), }

[derive(Debug, Deserialize, Serialize, tlua::Push)]

struct Foo { bar: Value, } let example = Foo{ bar: Value::Code("abc".to_string()) }; tarantool::tlua will serialize this struct in lua table like this: lua { bar = "abc" } ```

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: lua { bar = {"Code" = "abc"} } 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): ```rust impl tlua::Push for Value where L: tlua::AsLua, { type Err = TuplePushError;

fn pushtolua(&self, lua: L) -> Result, (Self::Err, L)> { let mut hm = HashMap::::new(); match self { Value::String(s) => hm.insert("String".tostring(), s.tostring()), Value::Code(s) => hm.insert("Code".tostring(), s.tostring()), }; hm.pushtolua(lua) } }

impl tlua::PushOne for Value where L: tlua::AsLua {} ```