![Latest Version] ![Docs badge]
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
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
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
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 ping = routegroup
.builder()
.withmethod("GET")
.withpath("/ping")
.build(|ctx: &mut Context, _request: Request| -> Result<_, Box
let s = server::Server::new(); s.register(Box::new(echo)); s.register(Box::new(ping)); } ```
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:
rust
use shors::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 transport required exported stored procedure - rpc_handler.
Create stored procedure. Example (where RPC_SERVER is the Server instance): ```rust
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)
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
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
RPCSERVER.with(|srv| { srv.register(Box::new(sumroute)); });
Ok(()) } ```
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)
Rpc client require register some lua code in luaopen_ function (or in any init function).
Example: ```rust
pub unsafe extern "C" fn luaopenlibstub(l: *mut ffilua::luaState) -> cint { let lua = tlua::StaticLua::fromstatic(l); shors::initlua_functions(&lua).unwrap(); 1 } ```
```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, ¶ms)?;
```
```rust let lua = tarantool::lua_state();
rpc::client::Builder::new(&lua)
.async_shard_endpoint("/ping")
.call(&mut Context::background(), bucket_id, &())?;
```
```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, ¶ms)?;
```
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(), &())?;
```
with-trace
not set bash
make unit-test
bash
(cd tests/testapplication/ && cartridge build)
make int-test
!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:
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), }
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
fn pushtolua(&self, lua: L) -> Result
impl