From 8506ad9f1a87a751947a8ad2cf86cfb6fa328683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1aki=20Garay?= Date: Fri, 17 Oct 2025 19:43:32 -0300 Subject: [PATCH] Added example to the README's and a version bump to re-publish and visibilize them on crates.io --- Cargo.toml | 6 +- README.md | 128 ++++++++++++++++++++++++++++++++++++++++- concurrency/README.md | 129 ++++++++++++++++++++++++++++++++++++++++- rt/README.md | 130 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 667b687..1a349dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,12 @@ members = [ ] [workspace.dependencies] -spawned-rt = { path = "rt", version = "0.4.2" } -spawned-concurrency = { path = "concurrency", version = "0.4.2" } +spawned-rt = { path = "rt", version = "0.4.3" } +spawned-concurrency = { path = "concurrency", version = "0.4.3" } tracing = { version = "0.1.41", features = ["log"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } [workspace.package] -version = "0.4.2" +version = "0.4.3" license = "MIT" edition = "2021" diff --git a/README.md b/README.md index 37e5b1d..2eddd3b 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,137 @@ Library for concurrent Rust. # Goals: - - Provide a framework to build robust, scalable and efficient applications in concurrent Rust. - Set coding guidelines to apply along LambdaClass repositories and codebase. - Starting point to ideate what we may expect for Concrete runtime implementation. +# Example: hit the ground running +Let's take a look at one of the examples in the [examples folder](https://github.com/lambdaclass/spawned/tree/main/examples), the [name server](https://github.com/lambdaclass/spawned/tree/main/examples/name_server). +The name server is a test of the `GenServer` abstraction using `tasks` implementation, and is based on Joe's Armstrong book: Programming Erlang, Second edition, Section 22.1 - The Road to the Generic Server. + +We would like to have a server that listens and responds to the following types of messages: + +```rust +#[derive(Debug, Clone)] +pub enum NameServerInMessage { + Add { key: String, value: String }, + Find { key: String }, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq)] +pub enum NameServerOutMessage { + Ok, + Found { value: String }, + NotFound, + Error, +} +``` + +To write our server code, we first need to define the type for our name server's state, and it's handle: +```rust +type NameServerHandle = GenServerHandle; + +pub struct NameServer { + inner: HashMap, +} + +impl NameServer { + pub fn new() -> Self { + NameServer { + inner: HashMap::new(), + } + } +} +``` + +Our name server's API has two async functions: `add`, and `find`, which correspond to the `NameServerInMessage` variants. Note that these map to the return messages' type: + +```rust +impl NameServer { + pub async fn add(server: &mut NameServerHandle, key: String, value: String) -> OutMessage { + match server.call(InMessage::Add { key, value }).await { + Ok(_) => OutMessage::Ok, + Err(_) => OutMessage::Error, + } + } + + pub async fn find(server: &mut NameServerHandle, key: String) -> OutMessage { + server + .call(InMessage::Find { key }) + .await + .unwrap_or(OutMessage::Error) + } +} +``` + +Now that our base state type is defined, we can implement the `GenServer` trait for our name server. Since the only thing we want to do differently than the defaults is how we handle `call` messages, we implement the async `handle_call` function and it's associated types: +```rust +impl GenServer for NameServer { + type CallMsg = InMessage; + type CastMsg = Unused; + type OutMsg = OutMessage; + type Error = std::fmt::Error; + + async fn handle_call( + &mut self, + message: Self::CallMsg, + _handle: &NameServerHandle, + ) -> CallResponse { + match message.clone() { + Self::CallMsg::Add { key, value } => { + self.inner.insert(key, value); + CallResponse::Reply(Self::OutMsg::Ok) + } + Self::CallMsg::Find { key } => match self.inner.get(&key) { + Some(result) => { + let value = result.to_string(); + CallResponse::Reply(Self::OutMsg::Found { value }) + } + None => CallResponse::Reply(Self::OutMsg::NotFound), + }, + } + } +} +``` + +Finally, we can write our `main` function: + +```rust +fn main() { + rt::run(async { + let mut name_server = NameServer::new().start(); + + let result = + NameServer::add(&mut name_server, "Joe".to_string(), "At Home".to_string()).await; + tracing::info!("Storing value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::Ok); + + let result = NameServer::find(&mut name_server, "Joe".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!( + result, + NameServerOutMessage::Found { + value: "At Home".to_string() + } + ); + + let result = NameServer::find(&mut name_server, "Bob".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::NotFound); + }) +} +``` + +If you run `cargo run --bin name_server` this should produce: +``` +2025-10-17T22:33:41.004784Z INFO name_server: Storing value result: Ok +2025-10-17T22:33:41.004902Z INFO name_server: Retrieving value result: Found { value: "At Home" } +2025-10-17T22:33:41.004940Z INFO name_server: Retrieving value result: NotFound +``` + +There are many more highlighting other features. + # Rationale Inspired by Erlang OTP, the main goal for `spawned` is to keep concurrency logic separated from business logic. It provides an actor-model abstraction that honoring Erlang naming we called `GenServer`. `GenServer` handle the concurrency logic, while providing callback functions to implement the business logic. diff --git a/concurrency/README.md b/concurrency/README.md index 0f9d443..68cbda3 100644 --- a/concurrency/README.md +++ b/concurrency/README.md @@ -1,7 +1,134 @@ # Spawned concurrency Some traits and structs to implement à-la-Erlang concurrent code. -Currently two versions: +This crate is part of [spawned](https://github.com/lambdaclass/spawned). To understand usage, we encourage you to read the [workspace README.md](https://github.com/lambdaclass/spawned/blob/main/README.md) but we reproduce a motivating example here. +## Example: hit the ground running +Let's take a look at one of the examples in the [examples folder](https://github.com/lambdaclass/spawned/tree/main/examples), the [name server](https://github.com/lambdaclass/spawned/tree/main/examples/name_server). +The name server is a test of the `GenServer` abstraction using `tasks` implementation, and is based on Joe's Armstrong book: Programming Erlang, Second edition, Section 22.1 - The Road to the Generic Server. + +We would like to have a server that listens and responds to the following types of messages: + +```rust +#[derive(Debug, Clone)] +pub enum NameServerInMessage { + Add { key: String, value: String }, + Find { key: String }, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq)] +pub enum NameServerOutMessage { + Ok, + Found { value: String }, + NotFound, + Error, +} +``` + +To write our server code, we first need to define the type for our name server's state, and it's handle: +```rust +type NameServerHandle = GenServerHandle; + +pub struct NameServer { + inner: HashMap, +} + +impl NameServer { + pub fn new() -> Self { + NameServer { + inner: HashMap::new(), + } + } +} +``` + +Our name server's API has two async functions: `add`, and `find`, which correspond to the `NameServerInMessage` variants. Note that these map to the return messages' type: + +```rust +impl NameServer { + pub async fn add(server: &mut NameServerHandle, key: String, value: String) -> OutMessage { + match server.call(InMessage::Add { key, value }).await { + Ok(_) => OutMessage::Ok, + Err(_) => OutMessage::Error, + } + } + + pub async fn find(server: &mut NameServerHandle, key: String) -> OutMessage { + server + .call(InMessage::Find { key }) + .await + .unwrap_or(OutMessage::Error) + } +} +``` + +Now that our base state type is defined, we can implement the `GenServer` trait for our name server. Since the only thing we want to do differently than the defaults is how we handle `call` messages, we implement the async `handle_call` function and it's associated types: +```rust +impl GenServer for NameServer { + type CallMsg = InMessage; + type CastMsg = Unused; + type OutMsg = OutMessage; + type Error = std::fmt::Error; + + async fn handle_call( + &mut self, + message: Self::CallMsg, + _handle: &NameServerHandle, + ) -> CallResponse { + match message.clone() { + Self::CallMsg::Add { key, value } => { + self.inner.insert(key, value); + CallResponse::Reply(Self::OutMsg::Ok) + } + Self::CallMsg::Find { key } => match self.inner.get(&key) { + Some(result) => { + let value = result.to_string(); + CallResponse::Reply(Self::OutMsg::Found { value }) + } + None => CallResponse::Reply(Self::OutMsg::NotFound), + }, + } + } +} +``` + +Finally, we can write our `main` function: + +```rust +fn main() { + rt::run(async { + let mut name_server = NameServer::new().start(); + + let result = + NameServer::add(&mut name_server, "Joe".to_string(), "At Home".to_string()).await; + tracing::info!("Storing value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::Ok); + + let result = NameServer::find(&mut name_server, "Joe".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!( + result, + NameServerOutMessage::Found { + value: "At Home".to_string() + } + ); + + let result = NameServer::find(&mut name_server, "Bob".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::NotFound); + }) +} +``` + +If you run `cargo run --bin name_server` this should produce: +``` +2025-10-17T22:33:41.004784Z INFO name_server: Storing value result: Ok +2025-10-17T22:33:41.004902Z INFO name_server: Retrieving value result: Found { value: "At Home" } +2025-10-17T22:33:41.004940Z INFO name_server: Retrieving value result: NotFound +``` + +## Notes +There are currently two implementations: - threads: no use of async/await. Just IO threads code - tasks: a runtime is required to run async/await code. It uses `spawned_rt::tasks` module that abstracts the runtime. diff --git a/rt/README.md b/rt/README.md index 6791106..ef9b77a 100644 --- a/rt/README.md +++ b/rt/README.md @@ -1,6 +1,136 @@ # Spawned runtime Runtime wrapper to remove dependencies from code. Using this library will allow to set a tokio runtime (or any other runtime, once implemented) just by changing the enabled runtime feature. + +This crate is part of [spawned](https://github.com/lambdaclass/spawned). To understand usage, we encourage you to read the [workspace README.md](https://github.com/lambdaclass/spawned/blob/main/README.md) but we reproduce a motivating example here. + +## Example: hit the ground running +Let's take a look at one of the examples in the [examples folder](https://github.com/lambdaclass/spawned/tree/main/examples), the [name server](https://github.com/lambdaclass/spawned/tree/main/examples/name_server). +The name server is a test of the `GenServer` abstraction using `tasks` implementation, and is based on Joe's Armstrong book: Programming Erlang, Second edition, Section 22.1 - The Road to the Generic Server. + +We would like to have a server that listens and responds to the following types of messages: + +```rust +#[derive(Debug, Clone)] +pub enum NameServerInMessage { + Add { key: String, value: String }, + Find { key: String }, +} + +#[allow(dead_code)] +#[derive(Debug, Clone, PartialEq)] +pub enum NameServerOutMessage { + Ok, + Found { value: String }, + NotFound, + Error, +} +``` + +To write our server code, we first need to define the type for our name server's state, and it's handle: +```rust +type NameServerHandle = GenServerHandle; + +pub struct NameServer { + inner: HashMap, +} + +impl NameServer { + pub fn new() -> Self { + NameServer { + inner: HashMap::new(), + } + } +} +``` + +Our name server's API has two async functions: `add`, and `find`, which correspond to the `NameServerInMessage` variants. Note that these map to the return messages' type: + +```rust +impl NameServer { + pub async fn add(server: &mut NameServerHandle, key: String, value: String) -> OutMessage { + match server.call(InMessage::Add { key, value }).await { + Ok(_) => OutMessage::Ok, + Err(_) => OutMessage::Error, + } + } + + pub async fn find(server: &mut NameServerHandle, key: String) -> OutMessage { + server + .call(InMessage::Find { key }) + .await + .unwrap_or(OutMessage::Error) + } +} +``` + +Now that our base state type is defined, we can implement the `GenServer` trait for our name server. Since the only thing we want to do differently than the defaults is how we handle `call` messages, we implement the async `handle_call` function and it's associated types: +```rust +impl GenServer for NameServer { + type CallMsg = InMessage; + type CastMsg = Unused; + type OutMsg = OutMessage; + type Error = std::fmt::Error; + + async fn handle_call( + &mut self, + message: Self::CallMsg, + _handle: &NameServerHandle, + ) -> CallResponse { + match message.clone() { + Self::CallMsg::Add { key, value } => { + self.inner.insert(key, value); + CallResponse::Reply(Self::OutMsg::Ok) + } + Self::CallMsg::Find { key } => match self.inner.get(&key) { + Some(result) => { + let value = result.to_string(); + CallResponse::Reply(Self::OutMsg::Found { value }) + } + None => CallResponse::Reply(Self::OutMsg::NotFound), + }, + } + } +} +``` + +Finally, we can write our `main` function: + +```rust +fn main() { + rt::run(async { + let mut name_server = NameServer::new().start(); + + let result = + NameServer::add(&mut name_server, "Joe".to_string(), "At Home".to_string()).await; + tracing::info!("Storing value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::Ok); + + let result = NameServer::find(&mut name_server, "Joe".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!( + result, + NameServerOutMessage::Found { + value: "At Home".to_string() + } + ); + + let result = NameServer::find(&mut name_server, "Bob".to_string()).await; + tracing::info!("Retrieving value result: {result:?}"); + assert_eq!(result, NameServerOutMessage::NotFound); + }) +} +``` + +If you run `cargo run --bin name_server` this should produce: +``` +2025-10-17T22:33:41.004784Z INFO name_server: Storing value result: Ok +2025-10-17T22:33:41.004902Z INFO name_server: Retrieving value result: Found { value: "At Home" } +2025-10-17T22:33:41.004940Z INFO name_server: Retrieving value result: NotFound +``` + +## Notes We may implement a `deterministic` version based on comonware.xyz's runtime: + https://github.com/commonwarexyz/monorepo/blob/main/runtime/src/deterministic.rs Currently, only a very limited set of tokio functionality is reexported. We may want to extend this functionality as needed. \ No newline at end of file