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

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") .witherrorhandler(|ctx, err| { shorserror!(ctx: ctx, "http error {}", err); Response { status: 501, body: "request fail".tostring(), } }) .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, ...) return box.func['mylib.rpc_handler']:call({ path, ctx, { ... } }) 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::<(Vec,)>()?.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")
    .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, ...) return box.func['libstub.rpc_handle']:call({ path, ctx, { ... } }) 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