Distributed System challenges in Rust - Echo

by Anirudh on 24 Sep '23

Previous blog post - Distributed System challenges in Rust

Echo


This is the first challenge that we have to do.


Problem Statement

The Maelstrom server will send an echo message of the following format:

{
"src": "c1",
"dest": "n1",
"body": {
  "type": "echo",
  "msg_id": 1,
  "echo": "Please echo 35"
  }
}

and we need to reply with the following message:


{
  "src": "n1",
  "dest": "c1",
  "body": {
    "type": "echo_ok",
    "msg_id": 1,
    "in_reply_to": 1,
    "echo": "Please echo 35"
  }
}


In the given format, there are a couple of things that we need to keep in mind:

  • Nodes & clients are sequentially numbered (e.g. n1, n2, etc).
  • Nodes are prefixed with "n" and external clients are prefixed with "c".
  • Message IDs are unique per source node

The third point here is something that is handled by the library (along with a helpful into_reply function that converts a message into the format that a "reply" should be in.)


Solution


To start off, we define the structure of the incoming and outgoing messages that we'll be handling.


#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
enum EchoPayload {
    Echo { echo: String },
    EchoOk { echo: String },
}

Serde automatically handles discriminated/polymorphic deserialization. By defining the tag field, we tell serde to pick the EchoPayload enum variant which matches the name provided in the type field in the message that it is operating on. For type = "echo", it would pick the EchoPayload::Echo variant, and for type = "echo_ok", it would pick the EchoPayload::EchoOk variant.


The next thing to do, is to define an EchoNode which implements the Node trait provided by the library. This node would be given all incoming messages, and be expected to respond appropriately. It will maintain any kind of state that is relevant to it.


  struct EchoNode {
    echo_message: String,
  }

The first problem requires a very simple node. It just needs to store the echo_message it received, and then spit out the same message in a reply.


Our step function looks something like this:


fn step(
        &mut self,
        input: Event<EchoPayload, ()>,
        output: &mut std::io::StdoutLock,
    ) -> anyhow::Result<()> {
        match input {
            Event::Message(message) => {
                match message.body.payload {
                    EchoPayload::Echo { echo } => {
                        self.echo_message = echo;
                        let message_id = message.body.id;
                        let message = Message {
                            src: message.src,
                            dest: message.dest,
                            body: Body {
                                id: message.body.id,
                                in_reply_to: message.body.in_reply_to,
                                payload: EchoPayload::EchoOk {
                                    echo: self.echo_message.clone(),
                                },
                            },
                        };

                        message.into_reply(Some(&mut message_id.unwrap())).send(output)?;
                    }
                    EchoPayload::EchoOk { echo } => {}
                }
            },

            Event::Injected(injected) => {},
            Event::EOF => {}
        }
        Ok(())
    }

We begin by extracting the payload out of the incoming message, creating a response message which includes the same string that we got in the echo payload, and then writing that to stdout.


I ignore most of the error states for this problem -

  • We don't need to perform any actions if some other node sends us an echo_ok.
  • I don't yet know the relevance of the injected variant
  • We don't need to do anything special on EOF

With this done, we run our maelstrom test to verify that everything works.


CLI Tests for Echo using Maelstrom

This one was a pretty simple problem to solve, as it was intended to be the "introduction" to the Maelstrom runtime and the architecture of the library. You can find the code for my solution on my GitHub here.


In the next blog post, I'll work on solving the next challenge - Unique ID Generation!