spawned-concurrency 0.4.3

Spawned Concurrency
Documentation

Spawned concurrency

Some traits and structs to implement à-la-Erlang concurrent code.

This crate is part of spawned. To understand usage, we encourage you to read the workspace 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, the 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:

#[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:

type NameServerHandle = GenServerHandle<NameServer>;

pub struct NameServer {
    inner: HashMap<String, String>,
}

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:

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:

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<Self> {
        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:

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.